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