diff --git a/infrastructure/azure/iam/main.tf b/infrastructure/azure/iam/main.tf new file mode 100644 index 00000000..9e9aab75 --- /dev/null +++ b/infrastructure/azure/iam/main.tf @@ -0,0 +1,21 @@ +resource "azurerm_user_assigned_identity" "this" { + name = var.name + resource_group_name = var.resource_group_name + location = var.location + tags = var.tags +} + +resource "azurerm_federated_identity_credential" "this" { + name = "${var.name}-federated" + resource_group_name = var.resource_group_name + audience = ["api://AzureADTokenExchange"] + issuer = var.oidc_issuer_url + parent_id = azurerm_user_assigned_identity.this.id + subject = "system:serviceaccount:${var.namespace}:${var.service_account_name}" +} + +resource "azurerm_role_assignment" "this" { + scope = var.scope + role_definition_name = var.role_definition_name + principal_id = azurerm_user_assigned_identity.this.principal_id +} diff --git a/infrastructure/azure/iam/output.tf b/infrastructure/azure/iam/output.tf new file mode 100644 index 00000000..bf23f4ef --- /dev/null +++ b/infrastructure/azure/iam/output.tf @@ -0,0 +1,14 @@ +output "client_id" { + description = "The client ID of the user-assigned managed identity" + value = azurerm_user_assigned_identity.this.client_id +} + +output "principal_id" { + description = "The principal ID of the user-assigned managed identity" + value = azurerm_user_assigned_identity.this.principal_id +} + +output "id" { + description = "The resource ID of the user-assigned managed identity" + value = azurerm_user_assigned_identity.this.id +} diff --git a/infrastructure/azure/iam/provider.tf b/infrastructure/azure/iam/provider.tf new file mode 100644 index 00000000..e5e1bad9 --- /dev/null +++ b/infrastructure/azure/iam/provider.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.0" + } + } +} diff --git a/infrastructure/azure/iam/variables.tf b/infrastructure/azure/iam/variables.tf new file mode 100644 index 00000000..2c5516f2 --- /dev/null +++ b/infrastructure/azure/iam/variables.tf @@ -0,0 +1,53 @@ +############################################################################### +# REQUIRED VARIABLES +############################################################################### + +variable "resource_group_name" { + type = string + description = "The name of the resource group where the managed identity will be created" +} + +variable "location" { + type = string + description = "The Azure region where the managed identity will be created" +} + +variable "name" { + type = string + description = "The name of the user-assigned managed identity" +} + +variable "oidc_issuer_url" { + type = string + description = "The OIDC issuer URL of the AKS cluster for federated identity" +} + +variable "namespace" { + type = string + description = "The Kubernetes namespace of the service account to federate" +} + +variable "service_account_name" { + type = string + description = "The Kubernetes service account name to federate with the managed identity" +} + +variable "role_definition_name" { + type = string + description = "The Azure role definition to assign to the managed identity (e.g., 'DNS Zone Contributor')" +} + +variable "scope" { + type = string + description = "The scope at which the role assignment is applied (e.g., DNS zone resource ID)" +} + +############################################################################### +# OPTIONAL VARIABLES +############################################################################### + +variable "tags" { + type = map(string) + description = "A mapping of tags to assign to the managed identity" + default = {} +} diff --git a/infrastructure/commons/cert_manager/locals.tf b/infrastructure/commons/cert_manager/locals.tf index 884bbf29..5a4ad10b 100644 --- a/infrastructure/commons/cert_manager/locals.tf +++ b/infrastructure/commons/cert_manager/locals.tf @@ -87,6 +87,9 @@ locals { lookup(local.annotations_by_provider, var.cloud_provider, {}) ) } + podLabels = var.cloud_provider == "azure" ? { + "azure.workload.identity/use" = "true" + } : {} dns01RecursiveNameservers = "8.8.8.8:53,1.1.1.1:53" dns01RecursiveNameserversOnly = true } diff --git a/infrastructure/commons/cert_manager/templates/cert_manager_azure_values.tmpl.yaml b/infrastructure/commons/cert_manager/templates/cert_manager_azure_values.tmpl.yaml index 1e172c8b..9cd81552 100644 --- a/infrastructure/commons/cert_manager/templates/cert_manager_azure_values.tmpl.yaml +++ b/infrastructure/commons/cert_manager/templates/cert_manager_azure_values.tmpl.yaml @@ -4,3 +4,4 @@ azure: clientID: "${client_id}" tenantID: "${tenant_id}" hostedZoneName: "${hosted_zone_name}" + useWorkloadIdentity: true diff --git a/infrastructure/commons/cert_manager/variables.tf b/infrastructure/commons/cert_manager/variables.tf index f02fa16e..b7a87d45 100644 --- a/infrastructure/commons/cert_manager/variables.tf +++ b/infrastructure/commons/cert_manager/variables.tf @@ -74,7 +74,7 @@ variable "cert_manager_namespace" { variable "cert_manager_config_version" { description = "The version of the cert-manager configuration Helm chart" type = string - default = "2.34.0" + default = "2.35.0" } variable "hosted_zone_name" { diff --git a/infrastructure/commons/external_dns/locals.tf b/infrastructure/commons/external_dns/locals.tf index 63cda0d8..f646eab4 100644 --- a/infrastructure/commons/external_dns/locals.tf +++ b/infrastructure/commons/external_dns/locals.tf @@ -88,10 +88,39 @@ locals { ] } + azure_config = { + provider = { name = "azure" } + serviceAccount = { + create = true + annotations = { + "azure.workload.identity/client-id" = var.azure_client_id + } + } + podLabels = { + "azure.workload.identity/use" = "true" + } + extraVolumes = [ + { + name = "azure-config" + secret = { + secretName = "external-dns-azure-config" + } + } + ] + extraVolumeMounts = [ + { + name = "azure-config" + mountPath = "/etc/kubernetes" + readOnly = true + } + ] + } + provider_configs = { cloudflare = local.cloudflare_config aws = local.route53_config oci = local.oci_config + azure = local.azure_config } external_dns_values = merge(local.base_config, local.provider_configs[var.dns_provider_name]) diff --git a/infrastructure/commons/external_dns/main.tf b/infrastructure/commons/external_dns/main.tf index 939972de..ea1b6304 100644 --- a/infrastructure/commons/external_dns/main.tf +++ b/infrastructure/commons/external_dns/main.tf @@ -31,7 +31,8 @@ resource "helm_release" "external_dns" { depends_on = [ kubernetes_secret_v1.external_dns_cloudflare, - kubernetes_secret_v1.external_dns_oci_config + kubernetes_secret_v1.external_dns_oci_config, + kubernetes_secret_v1.external_dns_azure_config, ] } diff --git a/infrastructure/commons/external_dns/secret.tf b/infrastructure/commons/external_dns/secret.tf index b1bb4330..7f1a4d64 100644 --- a/infrastructure/commons/external_dns/secret.tf +++ b/infrastructure/commons/external_dns/secret.tf @@ -10,6 +10,26 @@ resource "kubernetes_secret_v1" "external_dns_cloudflare" { } } +resource "kubernetes_secret_v1" "external_dns_azure_config" { + count = var.dns_provider_name == "azure" ? 1 : 0 + + metadata { + name = "external-dns-azure-config" + namespace = var.external_dns_namespace + } + + data = { + "azure.json" = jsonencode({ + tenantId = var.azure_tenant_id + subscriptionId = var.azure_subscription_id + resourceGroup = var.azure_resource_group + useWorkloadIdentityExtension = true + }) + } + + depends_on = [kubernetes_namespace_v1.external_dns] +} + resource "kubernetes_secret_v1" "external_dns_oci_config" { count = var.dns_provider_name == "oci" ? 1 : 0 diff --git a/infrastructure/commons/external_dns/tests/external_dns_cross_provider.tftest.hcl b/infrastructure/commons/external_dns/tests/external_dns_cross_provider.tftest.hcl index d3dc9ecc..3f8990d1 100644 --- a/infrastructure/commons/external_dns/tests/external_dns_cross_provider.tftest.hcl +++ b/infrastructure/commons/external_dns/tests/external_dns_cross_provider.tftest.hcl @@ -10,7 +10,7 @@ run "rejects_invalid_provider" { command = plan variables { - dns_provider_name = "azure" + dns_provider_name = "invalid-provider" domain_filters = "myorg.example.com" external_dns_namespace = "external-dns" } diff --git a/infrastructure/commons/external_dns/variables.tf b/infrastructure/commons/external_dns/variables.tf index 298d20b7..9e518beb 100644 --- a/infrastructure/commons/external_dns/variables.tf +++ b/infrastructure/commons/external_dns/variables.tf @@ -172,7 +172,52 @@ variable "dns_provider_name" { type = string description = "The DNS provider to use with ExternalDNS " validation { - condition = contains(["cloudflare", "aws", "oci"], var.dns_provider_name) - error_message = "dns_provider_name must be either 'cloudflare', 'aws', or 'oci'." + condition = contains(["cloudflare", "aws", "oci", "azure"], var.dns_provider_name) + error_message = "dns_provider_name must be either 'cloudflare', 'aws', 'oci', or 'azure'." } } + +############################################################################### +# AZURE CONFIGURATION +############################################################################### + +variable "azure_client_id" { + description = "Client ID of the Azure Managed Identity for Workload Identity (required when dns_provider_name is 'azure')" + type = string + default = null + validation { + condition = var.dns_provider_name != "azure" || var.azure_client_id != null + error_message = "azure_client_id is required when dns_provider_name is 'azure'." + } +} + +variable "azure_subscription_id" { + description = "Azure subscription ID where the DNS zone is located (required when dns_provider_name is 'azure')" + type = string + default = null + validation { + condition = var.dns_provider_name != "azure" || var.azure_subscription_id != null + error_message = "azure_subscription_id is required when dns_provider_name is 'azure'." + } +} + +variable "azure_resource_group" { + description = "Azure resource group containing the DNS zone (required when dns_provider_name is 'azure')" + type = string + default = null + validation { + condition = var.dns_provider_name != "azure" || var.azure_resource_group != null + error_message = "azure_resource_group is required when dns_provider_name is 'azure'." + } +} + +variable "azure_tenant_id" { + description = "Azure tenant ID (required when dns_provider_name is 'azure')" + type = string + default = null + validation { + condition = var.dns_provider_name != "azure" || var.azure_tenant_id != null + error_message = "azure_tenant_id is required when dns_provider_name is 'azure'." + } +} +