diff --git a/infrastructure/azure/aks/README.md b/infrastructure/azure/aks/README.md
index c11e610d..80d9a91a 100644
--- a/infrastructure/azure/aks/README.md
+++ b/infrastructure/azure/aks/README.md
@@ -6,7 +6,7 @@ Deploys an Azure Kubernetes Service (AKS) cluster with system and user node pool
## Architecture
-The module wraps the Azure/aks/azurerm community module (version 11.0.0) and uses azurerm_client_config data source to retrieve the current tenant ID for AAD RBAC configuration. It provisions an AKS cluster with a system node pool and a separate autoscaling user node pool (nodepool), both attached to the provided vnet_subnet_id. The cluster has OIDC issuer and Workload Identity enabled, and the module conditionally attaches an ACR pull role by passing acr_id into attached_acr_id_map when provided. Outputs expose the cluster endpoint, CA certificate, client credentials, OIDC issuer URL, and node resource group for downstream consumption.
+The module wraps the Azure/aks/azurerm community module (version 11.0.0) and uses azurerm_client_config data source to retrieve the current tenant ID for AAD RBAC configuration. It provisions an AKS cluster with a system node pool and a separate autoscaling user node pool (nodepool), both attached to the provided vnet_subnet_id. The cluster has OIDC issuer and Workload Identity enabled, and the module attaches an ACR pull role for acr_id; by default (attach_acr null) it attaches whenever acr_id is non-null, and setting attach_acr to true keeps the assignment plan-stable when acr_id is only known after apply. Outputs expose the cluster endpoint, CA certificate, client credentials, OIDC issuer URL, and node resource group for downstream consumption.
## Features
@@ -14,7 +14,7 @@ The module wraps the Azure/aks/azurerm community module (version 11.0.0) and use
- Enables OIDC issuer and Workload Identity on the cluster for pod-level Azure identity federation
- Configures Azure RBAC and AAD tenant integration using the current client tenant ID from azurerm_client_config
- Assigns Network Contributor role to the AKS identity on the provided VNet subnet for CNI networking
-- Optionally attaches an Azure Container Registry by granting AcrPull role when acr_id is provided
+- Attaches an Azure Container Registry by granting AcrPull role on acr_id; defaults to attaching whenever acr_id is non-null (attach_acr null), with attach_acr=true keeping the assignment plan-stable when acr_id is known only after apply
- Supports private cluster mode and API server authorized IP range restrictions for network security
- Exposes cluster credentials, CA certificate, OIDC issuer URL, and node resource group as outputs
@@ -45,32 +45,35 @@ resource "example_resource" "this" {
## Requirements
| Name | Version |
-|------|---------|
+| ---- | ------- |
| [terraform](#requirement\_terraform) | ~> 1.6 |
| [azurerm](#requirement\_azurerm) | ~> 4.0 |
## Providers
| Name | Version |
-|------|---------|
+| ---- | ------- |
| [azurerm](#provider\_azurerm) | 4.41.0 |
+| [terraform](#provider\_terraform) | n/a |
## Modules
| Name | Source | Version |
-|------|--------|---------|
+| ---- | ------ | ------- |
| [aks](#module\_aks) | Azure/aks/azurerm | 11.0.0 |
## Resources
| Name | Type |
-|------|------|
+| ---- | ---- |
+| [terraform_data.validations](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/resources/data) | resource |
## Inputs
| Name | Description | Type | Default | Required |
-|------|-------------|------|---------|:--------:|
+| ---- | ----------- | ---- | ------- | :------: |
| [acr\_id](#input\_acr\_id) | The ID of the Azure Container Registry. If provided, AKS will be granted AcrPull role to pull images. | `string` | `null` | no |
+| [attach\_acr](#input\_attach\_acr) | Whether to grant AKS the AcrPull role on acr\_id. Null (default) preserves the legacy behaviour of attaching whenever acr\_id is non-null. Set to true for a greenfield single-apply where acr\_id is known only after apply (keeps the for\_each key set plan-stable); set to false to disable. | `bool` | `null` | no |
| [authorized\_ip\_ranges](#input\_authorized\_ip\_ranges) | The set of authorized IP ranges allowed to access the Kubernetes API server | `set(string)` | `null` | no |
| [cluster\_name](#input\_cluster\_name) | The name of the AKS cluster | `string` | n/a | yes |
| [environment](#input\_environment) | The environment name used for tagging and naming purposes | `string` | `"nullplatform"` | no |
@@ -88,7 +91,7 @@ resource "example_resource" "this" {
## Outputs
| Name | Description |
-|------|-------------|
+| ---- | ----------- |
| [admin\_client\_certificate](#output\_admin\_client\_certificate) | The admin client certificate for authentication |
| [admin\_client\_key](#output\_admin\_client\_key) | The admin client key for authentication |
| [admin\_cluster\_ca\_certificate](#output\_admin\_cluster\_ca\_certificate) | The admin cluster CA certificate in base64 |
@@ -105,13 +108,13 @@ resource "example_resource" "this" {
{
"name": "aks",
"description": "Deploys an Azure Kubernetes Service (AKS) cluster with system and user node pools, OIDC/Workload Identity, and optional ACR integration using the official Azure/aks/azurerm Terraform module",
- "architecture": "The module wraps the Azure/aks/azurerm community module (version 11.0.0) and uses azurerm_client_config data source to retrieve the current tenant ID for AAD RBAC configuration. It provisions an AKS cluster with a system node pool and a separate autoscaling user node pool (nodepool), both attached to the provided vnet_subnet_id. The cluster has OIDC issuer and Workload Identity enabled, and the module conditionally attaches an ACR pull role by passing acr_id into attached_acr_id_map when provided. Outputs expose the cluster endpoint, CA certificate, client credentials, OIDC issuer URL, and node resource group for downstream consumption.",
+ "architecture": "The module wraps the Azure/aks/azurerm community module (version 11.0.0) and uses azurerm_client_config data source to retrieve the current tenant ID for AAD RBAC configuration. It provisions an AKS cluster with a system node pool and a separate autoscaling user node pool (nodepool), both attached to the provided vnet_subnet_id. The cluster has OIDC issuer and Workload Identity enabled, and the module attaches an ACR pull role for acr_id; by default (attach_acr null) it attaches whenever acr_id is non-null, and setting attach_acr to true keeps the assignment plan-stable when acr_id is only known after apply. Outputs expose the cluster endpoint, CA certificate, client credentials, OIDC issuer URL, and node resource group for downstream consumption.",
"features": [
"Creates an AKS cluster with a dedicated system node pool and a separate autoscaling user node pool spanning three availability zones",
"Enables OIDC issuer and Workload Identity on the cluster for pod-level Azure identity federation",
"Configures Azure RBAC and AAD tenant integration using the current client tenant ID from azurerm_client_config",
"Assigns Network Contributor role to the AKS identity on the provided VNet subnet for CNI networking",
- "Optionally attaches an Azure Container Registry by granting AcrPull role when acr_id is provided",
+ "Attaches an Azure Container Registry by granting AcrPull role on acr_id; defaults to attaching whenever acr_id is non-null (attach_acr null), with attach_acr=true keeping the assignment plan-stable when acr_id is known only after apply",
"Supports private cluster mode and API server authorized IP range restrictions for network security",
"Exposes cluster credentials, CA certificate, OIDC issuer URL, and node resource group as outputs"
],
@@ -185,6 +188,11 @@ resource "example_resource" "this" {
"name": "acr_id",
"description": "The ID of the Azure Container Registry. If provided, AKS will be granted AcrPull role to pull images.",
"required": false
+ },
+ {
+ "name": "attach_acr",
+ "description": "Whether to grant AKS the AcrPull role on acr_id. Null (default) preserves the legacy behaviour of attaching whenever acr_id is non-null. Set to true for a greenfield single-apply where acr_id is known only after apply (keeps the for_each key set plan-stable); set to false to disable.",
+ "required": false
}
],
"outputs": [
diff --git a/infrastructure/azure/aks/main.tf b/infrastructure/azure/aks/main.tf
index 60874e2e..bfb943a8 100644
--- a/infrastructure/azure/aks/main.tf
+++ b/infrastructure/azure/aks/main.tf
@@ -68,7 +68,9 @@ module "aks" {
}
- attached_acr_id_map = var.acr_id != null ? { acr = var.acr_id } : {}
+ # attach_acr null (default) keeps the legacy "attach iff acr_id is non-null" behaviour;
+ # setting it true makes the for_each key set plan-stable when acr_id is known-after-apply.
+ attached_acr_id_map = (var.attach_acr != null ? var.attach_acr : var.acr_id != null) ? { acr = var.acr_id } : {}
network_contributor_role_assigned_subnet_ids = { subnet = var.vnet_subnet_id }
############################################
diff --git a/infrastructure/azure/aks/validations.tf b/infrastructure/azure/aks/validations.tf
new file mode 100644
index 00000000..974228a5
--- /dev/null
+++ b/infrastructure/azure/aks/validations.tf
@@ -0,0 +1,13 @@
+resource "terraform_data" "validations" {
+ lifecycle {
+ # Only an explicit attach_acr=true forces the attachment, so only then is acr_id required;
+ # null (legacy, gated on acr_id itself) and false need no id. Guards against the { acr = null }
+ # map that would feed null into azurerm_role_assignment's required scope. When acr_id is
+ # known-after-apply the condition is unknown and OpenTofu defers the check to apply time,
+ # so the greenfield single-apply path is unaffected.
+ precondition {
+ condition = var.attach_acr != true || var.acr_id != null
+ error_message = "acr_id is required when attach_acr is true. Leave attach_acr null (legacy) or set it false for clusters without an ACR."
+ }
+ }
+}
diff --git a/infrastructure/azure/aks/variables.tf b/infrastructure/azure/aks/variables.tf
index 23e7b635..7dd55560 100644
--- a/infrastructure/azure/aks/variables.tf
+++ b/infrastructure/azure/aks/variables.tf
@@ -104,3 +104,9 @@ variable "acr_id" {
description = "The ID of the Azure Container Registry. If provided, AKS will be granted AcrPull role to pull images."
default = null
}
+
+variable "attach_acr" {
+ type = bool
+ description = "Whether to grant AKS the AcrPull role on acr_id. Null (default) preserves the legacy behaviour of attaching whenever acr_id is non-null. Set to true for a greenfield single-apply where acr_id is known only after apply (keeps the for_each key set plan-stable); set to false to disable."
+ default = null
+}