diff --git a/infrastructure/commons/cert_manager/README.md b/infrastructure/commons/cert_manager/README.md index d3cf4410..2eb9fd1b 100644 --- a/infrastructure/commons/cert_manager/README.md +++ b/infrastructure/commons/cert_manager/README.md @@ -96,10 +96,12 @@ module "cert_manager" { | [aws\_region](#input\_aws\_region) | The AWS region. | `string` | `""` | no | | [aws\_sa\_arn](#input\_aws\_sa\_arn) | The AWS IAM role ARN for cert-manager. | `string` | `""` | no | | [azure\_client\_id](#input\_azure\_client\_id) | The Azure client ID for cert-manager. | `string` | `""` | no | +| [azure\_client\_secret](#input\_azure\_client\_secret) | The Azure client secret for Service Principal authentication. | `string` | `""` | no | | [azure\_hosted\_zone\_name](#input\_azure\_hosted\_zone\_name) | The hosted zone name in Azure DNS. | `string` | `""` | no | | [azure\_resource\_group\_name](#input\_azure\_resource\_group\_name) | The name of the Azure resource group that contains the DNS zone. | `string` | `""` | no | | [azure\_subscription\_id](#input\_azure\_subscription\_id) | The Azure subscription ID. | `string` | `""` | no | | [azure\_tenant\_id](#input\_azure\_tenant\_id) | The Azure tenant ID. | `string` | `""` | no | +| [azure\_use\_workload\_identity](#input\_azure\_use\_workload\_identity) | Whether to use Azure Workload Identity (true) or Service Principal (false). | `bool` | `false` | no | | [cert\_manager\_config\_version](#input\_cert\_manager\_config\_version) | The version of the cert-manager configuration Helm chart | `string` | `"2.29.2"` | no | | [cert\_manager\_namespace](#input\_cert\_manager\_namespace) | The Kubernetes namespace where cert-manager will be deployed | `string` | `"cert-manager"` | no | | [cert\_manager\_version](#input\_cert\_manager\_version) | The version of cert-manager Helm chart to deploy | `string` | `"1.18.2"` | no | diff --git a/infrastructure/commons/cert_manager/locals.tf b/infrastructure/commons/cert_manager/locals.tf index 30584fdc..7bea9647 100644 --- a/infrastructure/commons/cert_manager/locals.tf +++ b/infrastructure/commons/cert_manager/locals.tf @@ -25,6 +25,7 @@ locals { client_id = var.azure_client_id tenant_id = var.azure_tenant_id hosted_zone_name = var.azure_hosted_zone_name + client_secret = var.azure_client_secret } : {}, var.cloud_provider == "aws" ? { 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..f6936e2b 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,6 @@ azure: clientID: "${client_id}" tenantID: "${tenant_id}" hostedZoneName: "${hosted_zone_name}" +%{ if client_secret != "" ~} + clientSecret: "${client_secret}" +%{ endif ~} diff --git a/infrastructure/commons/cert_manager/variables.tf b/infrastructure/commons/cert_manager/variables.tf index 4d2187b3..9cb8404c 100644 --- a/infrastructure/commons/cert_manager/variables.tf +++ b/infrastructure/commons/cert_manager/variables.tf @@ -132,6 +132,19 @@ variable "azure_hosted_zone_name" { } } +variable "azure_client_secret" { + description = "The Azure client secret for Service Principal authentication." + type = string + sensitive = true + default = "" +} + +variable "azure_use_workload_identity" { + description = "Whether to use Azure Workload Identity (true) or Service Principal (false)." + type = bool + default = false +} + ############################################################################### # CLOUDFLARE CONFIGURATION ############################################################################### diff --git a/infrastructure/commons/external_dns/README.md b/infrastructure/commons/external_dns/README.md index 1f4db685..ce4f4ac3 100644 --- a/infrastructure/commons/external_dns/README.md +++ b/infrastructure/commons/external_dns/README.md @@ -2,7 +2,7 @@ # Module: external_dns This OpenTofu module installs **ExternalDNS** using a Helm chart, enabling dynamic DNS record management through -either **AWS Route53** or **Cloudflare** as your DNS provider. +**AWS Route53**, **Cloudflare**, **OCI DNS**, or **Azure DNS** as your DNS provider. ## Usage @@ -35,6 +35,39 @@ module "external_dns" { } ``` +### Azure DNS example (with Workload Identity - recommended for AKS) + +```hcl +module "external_dns" { + source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/commons/external_dns?ref=v1.0.0" + + dns_provider_name = "azure" + azure_resource_group = var.azure_resource_group + azure_tenant_id = var.azure_tenant_id + azure_subscription_id = var.azure_subscription_id + azure_client_id = var.azure_managed_identity_client_id + azure_use_workload_identity = true + domain_filters = var.domain_filters +} +``` + +### Azure DNS example (with Service Principal) + +```hcl +module "external_dns" { + source = "git::https://github.com/nullplatform/tofu-modules.git//infrastructure/commons/external_dns?ref=v1.0.0" + + dns_provider_name = "azure" + azure_resource_group = var.azure_resource_group + azure_tenant_id = var.azure_tenant_id + azure_subscription_id = var.azure_subscription_id + azure_client_id = var.azure_client_id + azure_client_secret = var.azure_client_secret + azure_use_workload_identity = false + domain_filters = var.domain_filters +} +``` + ## Requirements @@ -55,6 +88,7 @@ module "external_dns" { |------|------| | [helm_release.external_dns](https://registry.terraform.io/providers/hashicorp/helm/latest/docs/resources/release) | resource | | [kubernetes_namespace_v1.external_dns](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace_v1) | resource | +| [kubernetes_secret_v1.external_dns_azure](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret_v1) | resource | | [kubernetes_secret_v1.external_dns_cloudflare](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret_v1) | resource | | [kubernetes_secret_v1.external_dns_oci_config](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret_v1) | resource | @@ -64,6 +98,12 @@ module "external_dns" { |------|-------------|------|---------|:--------:| | [aws\_iam\_role\_arn](#input\_aws\_iam\_role\_arn) | The IAM role ARN for ExternalDNS to assume for Route53 access (required when dns\_provider\_name is 'aws') | `string` | `null` | no | | [aws\_region](#input\_aws\_region) | The AWS region where the Route53 hosted zones are located | `string` | `null` | no | +| [azure\_client\_id](#input\_azure\_client\_id) | The Azure client ID (application ID) for service principal authentication. Required when azure\_use\_workload\_identity is false. | `string` | `null` | no | +| [azure\_client\_secret](#input\_azure\_client\_secret) | The Azure client secret for service principal authentication. Required when azure\_use\_workload\_identity is false. | `string` | `null` | no | +| [azure\_resource\_group](#input\_azure\_resource\_group) | The Azure resource group containing the DNS zone (required when dns\_provider\_name is 'azure') | `string` | `null` | no | +| [azure\_subscription\_id](#input\_azure\_subscription\_id) | The Azure subscription ID containing the DNS zone (required when dns\_provider\_name is 'azure') | `string` | `null` | no | +| [azure\_tenant\_id](#input\_azure\_tenant\_id) | The Azure tenant ID for authentication (required when dns\_provider\_name is 'azure') | `string` | `null` | no | +| [azure\_use\_workload\_identity](#input\_azure\_use\_workload\_identity) | Whether to use Azure Workload Identity for authentication (recommended for AKS) | `bool` | `true` | no | | [cloudflare\_token](#input\_cloudflare\_token) | The Cloudflare API token for DNS management (required when dns\_provider\_name is 'cloudflare') | `string` | `null` | no | | [dns\_provider\_name](#input\_dns\_provider\_name) | The DNS provider to use with ExternalDNS | `string` | n/a | yes | | [domain\_filters](#input\_domain\_filters) | The domain filter to limit ExternalDNS to manage DNS records only for specific domains | `string` | n/a | yes | diff --git a/infrastructure/commons/external_dns/locals.tf b/infrastructure/commons/external_dns/locals.tf index 8c85c475..bc0f8d18 100644 --- a/infrastructure/commons/external_dns/locals.tf +++ b/infrastructure/commons/external_dns/locals.tf @@ -25,12 +25,12 @@ locals { provider = { name = "aws" } env = [{ name = "AWS_DEFAULT_REGION" - value = var.aws_region + value = var.aws_region != null ? var.aws_region : "" }] serviceAccount = { create = true annotations = { - "eks.amazonaws.com/role-arn" = var.aws_iam_role_arn + "eks.amazonaws.com/role-arn" = var.aws_iam_role_arn != null ? var.aws_iam_role_arn : "" } } rbac = { @@ -62,7 +62,7 @@ locals { } ] extraArgs = [ - "--oci-compartment-ocid=${var.oci_compartment_ocid}", + "--oci-compartment-ocid=${var.oci_compartment_ocid != null ? var.oci_compartment_ocid : ""}", "--oci-zone-scope=${var.oci_zone_scope}", "--oci-zones-cache-duration=${var.oci_zones_cache_duration}" ] @@ -83,11 +83,68 @@ locals { ] } + azure_workload_identity_config = { + provider = { name = "azure" } + serviceAccount = { + create = true + labels = { + "azure.workload.identity/use" = "true" + } + annotations = { + "azure.workload.identity/client-id" = var.azure_client_id != null ? var.azure_client_id : "" + } + } + podLabels = { + "azure.workload.identity/use" = "true" + } + env = [ + { + name = "AZURE_TENANT_ID" + value = var.azure_tenant_id != null ? var.azure_tenant_id : "" + }, + { + name = "AZURE_SUBSCRIPTION_ID" + value = var.azure_subscription_id != null ? var.azure_subscription_id : "" + } + ] + extraArgs = [ + "--azure-resource-group=${var.azure_resource_group != null ? var.azure_resource_group : ""}", + "--azure-config-file=" + ] + } + + azure_service_principal_config = { + provider = { name = "azure" } + extraArgs = [ + "--azure-resource-group=${var.azure_resource_group != null ? var.azure_resource_group : ""}" + ] + extraVolumes = [ + { + name = "azure-config" + secret = { + secretName = "external-dns-azure" + } + } + ] + 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_workload_identity_config + azure-sp = local.azure_service_principal_config } - external_dns_values = merge(local.base_config, local.provider_configs[var.dns_provider_name]) + # Determine the actual provider key to use + effective_provider_key = var.dns_provider_name == "azure" && !var.azure_use_workload_identity ? "azure-sp" : var.dns_provider_name + + external_dns_values = merge(local.base_config, local.provider_configs[local.effective_provider_key]) } \ No newline at end of file diff --git a/infrastructure/commons/external_dns/main.tf b/infrastructure/commons/external_dns/main.tf index ceb54873..9085f96b 100644 --- a/infrastructure/commons/external_dns/main.tf +++ b/infrastructure/commons/external_dns/main.tf @@ -32,6 +32,7 @@ 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 ] } diff --git a/infrastructure/commons/external_dns/secret.tf b/infrastructure/commons/external_dns/secret.tf index a647212d..7d2aef8d 100644 --- a/infrastructure/commons/external_dns/secret.tf +++ b/infrastructure/commons/external_dns/secret.tf @@ -8,6 +8,31 @@ resource "kubernetes_secret_v1" "external_dns_cloudflare" { data = { "api-token" = var.cloudflare_token } + + depends_on = [kubernetes_namespace_v1.external_dns] +} + +resource "kubernetes_secret_v1" "external_dns_azure" { + count = var.dns_provider_name == "azure" && !var.azure_use_workload_identity ? 1 : 0 + + metadata { + name = "external-dns-azure" + namespace = var.external_dns_namespace + } + + type = "Opaque" + data = { + "azure.json" = jsonencode({ + tenantId = var.azure_tenant_id + subscriptionId = var.azure_subscription_id + resourceGroup = var.azure_resource_group + aadClientId = var.azure_client_id + aadClientSecret = var.azure_client_secret + useManagedIdentityExtension = false + }) + } + + depends_on = [kubernetes_namespace_v1.external_dns] } resource "kubernetes_secret_v1" "external_dns_oci_config" { diff --git a/infrastructure/commons/external_dns/variables.tf b/infrastructure/commons/external_dns/variables.tf index 14f2eb6f..61a0e41c 100644 --- a/infrastructure/commons/external_dns/variables.tf +++ b/infrastructure/commons/external_dns/variables.tf @@ -112,6 +112,63 @@ variable "zone_type" { } } +############################################################################### +# AZURE CONFIGURATION +############################################################################### + +variable "azure_resource_group" { + description = "The 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 = "The Azure tenant ID for authentication (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'." + } +} + +variable "azure_subscription_id" { + description = "The Azure subscription ID containing the DNS zone (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_use_workload_identity" { + description = "Whether to use Azure Workload Identity for authentication (recommended for AKS)" + type = bool + default = true +} + +variable "azure_client_id" { + description = "The Azure client ID (application ID) for service principal authentication. Required when azure_use_workload_identity is false." + type = string + default = null +} + +variable "azure_client_secret" { + description = "The Azure client secret for service principal authentication. Required when azure_use_workload_identity is false." + type = string + sensitive = true + default = null + validation { + condition = var.dns_provider_name != "azure" || var.azure_use_workload_identity == true || (var.azure_client_id != null && var.azure_client_secret != null) + error_message = "azure_client_id and azure_client_secret are required when dns_provider_name is 'azure' and azure_use_workload_identity is false." + } +} + ############################################################################### # OCI CONFIGURATION ############################################################################### @@ -166,7 +223,7 @@ 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'." } } \ No newline at end of file