Foundations
opentofu/azure builds a resource group, a
VNet, an AKS cluster with no CNI and no workload, and its identity, and
outputs what the bootstrap needs to
install Cilium and Flux.
What is decided for you
Section titled “What is decided for you”Enforced: no variable. Default: a variable changes it.
| Position | |
|---|---|
| AKS Standard, Node Auto-Provisioning, never Automatic | enforced |
A system pool of 2 × Standard_D2s_v5 over zones 1–3, tainted CriticalAddonsOnly; NAP provisions the rest | default |
stable upgrade channel | enforced |
| Two maintenance windows of 4 to 24 hours, no default | required |
KubernetesOfficial support, no LTS | enforced |
| Disk and File CSI drivers, AKS-managed | enforced |
| Container Insights (90 days) and Managed Prometheus | enforced, superseded |
| Entra Workload ID and the OIDC issuer | enforced |
| Microsoft-managed keys for etcd, disks and logs | enforced |
network_plugin = "none": Cilium comes with the bootstrap | enforced |
pod_cidr 10.244.0.0/16, passed to Cilium as an output | default |
| Private cluster, no public FQDN | enforced |
One VNet 10.0.0.0/16, one node subnet, one NAT gateway | default |
Egress through the NAT gateway (userAssignedNATGateway) | enforced |
owner, environment, socle-version tags on every resource | enforced |
Under the hood
- The private DNS zone is system-managed.
- Service range
10.1.0.0/16, DNS service10.1.0.10: defaults AKS needs. - Kubernetes RBAC on, system-assigned cluster identity: AKS’s defaults, declared.
upgrade_settings.max_surge = "10%"repeats AKS’s server-side default so the plan converges.kubernetes_versionhas no default and accepts anymajor.minor.
What is deliberately absent
Section titled “What is deliberately absent”- Deployment Safeguards: proposed, no
azurerm4.x attribute.azure_policy_enabled = trueinstalls the add-on and assigns nothing. pod_cidron the cluster, anetwork_policy: neither exists under BYO CNI; Cilium holds both.- Defender for Containers: the client’s subscription setting.
- Any workload identity, Crossplane’s included: bind one to the
oidc_issuer_urloutput. - Entra ID authentication on the API server: local accounts stay on (limits).
- Any credential as input, any key as output:
cluster_ca_certificateis the only certificate, sensitive. - Windows pools, IPv6: NAP supports neither.
- Azure service metrics readers: proposed.
Measured
Section titled “Measured”- 2026-10-05, OpenTofu 1.12.6, plan only: the bootstrap with
cloud = "azure"and noawsprovider block fails withProvider "registry.opentofu.org/hashicorp/aws" requires explicit configuration. With a placeholderawsprovider, the plan is 4 to add (Cilium, the operator, the instance, the socle release). The quickstart carries that block.
Reference
Section titled “Reference”Requirements
Section titled “Requirements”| Name | Version |
|---|---|
| terraform | >= 1.13 |
| azurerm | >= 4.0, < 5.0 |
Modules
Section titled “Modules”No modules.
Resources
Section titled “Resources”Inputs
Section titled “Inputs”| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| cluster_name | Name shared by the resource group (when created), the VNet, the AKS cluster and every resource this module creates around them. | string | n/a | yes |
| environment | Stamped on every billable resource. Also what the factory's upgrade rings (dev/staging/prod) key off. | string | n/a | yes |
| kubernetes_version | AKS control plane version. Required, no default: the module accepts whatever version it is given rather than enforcing a version policy itself — that policy is decided and bumped by the socle Kargo pipelines, not by this module. | string | n/a | yes |
| location | Azure region for every resource this module creates. Required, no default — there is no globally correct region to pick on a client's behalf. | string | n/a | yes |
| maintenance_window_auto_upgrade | Window Kubernetes version auto-upgrades (stable channel) are allowed to run in. At least 4 hours, per AKS's own constraint. | object({ | n/a | yes |
| maintenance_window_node_os | Window node OS security patches are allowed to run in. Separate schedule from maintenance_window_auto_upgrade, so node patching and Kubernetes upgrades never have to share a window. | object({ | n/a | yes |
| owner | Stamped on every billable resource so cost can be attributed and orphans can be found. | string | n/a | yes |
| resource_group_name | Name of the resource group. Used as the name to create when create_resource_group is true, or the name of an existing one to attach to when false. | string | n/a | yes |
| additional_tags | Extra tags merged onto every resource this module creates, on top of owner/environment/socle-version. | map(string) | {} | no |
| create_nat_gateway | Create one NAT Gateway for the VNet's node subnet. Not a toggle for disabling egress outright — the node subnet has no public IP of its own, so without this its nodes reach nothing. Exists only for the create_vnet = false case, where the consumer's existing VNet already manages its own NAT or an alternate egress path. | bool | true | no |
| create_resource_group | Create the resource group, or attach to one the consumer already manages. | bool | true | no |
| create_vnet | Create the VNet and its node subnet, or attach to a subnet the consumer already manages. | bool | true | no |
| dns_service_ip | IP address within service_cidr used for cluster service discovery (kube-dns). | string | "10.1.0.10" | no |
| log_retention_days | Retention for the Container Insights Log Analytics workspace this module creates. | number | 90 | no |
| node_subnet_id | Existing node subnet ID to attach to. Required when create_vnet is false — this module carves its own subnet out of vnet_cidr only when it also creates the VNet. | string | null | no |
| pod_cidr | Range Cilium allocates pod addresses from, as the cluster pool of the Cilium the bootstrap module installs (docs/architecture/cilium-before-flux.md). Not set on the cluster: azurerm refuses pod_cidr under network_plugin = "none" (see cluster.tf), so this module only carries the value to the bootstrap through its output. Must not overlap the VNet, service_cidr or any connected network. The default is AKS's own pod range, which the chart's default (10.0.0.0/8) would not be: that one contains the VNet. | string | "10.244.0.0/16" | no |
| service_cidr | CIDR for Kubernetes service IPs. Must not overlap the VNet or any connected network, and be smaller than /12 — an AKS constraint independent of the BYO CNI choice below. | string | "10.1.0.0/16" | no |
| system_node_pool_node_count | Node count for the mandatory system node pool. Small and fixed rather than autoscaled: this pool exists to satisfy AKS's structural minimum, not to run workloads. | number | 2 | no |
| system_node_pool_vm_size | VM size for the mandatory system node pool. This is a structural AKS requirement, not a Karpenter/NAP-managed pool — kept small and tainted for-system-only by default (only_critical_addons_enabled), since NAP provisions everything workload-shaped. | string | "Standard_D2s_v5" | no |
| vnet_cidr | CIDR for the VNet — and its single node subnet, which covers the whole range — when this module creates it. Arbitrary default (not a research decision), sized generously since it only ever needs to fit nodes: Cilium's own IPAM owns pod addressing entirely, decoupled from the VNet. | string | "10.0.0.0/16" | no |
| vnet_name | Existing VNet name to attach to. Required when create_vnet is false, and incoherent to set when create_vnet is true — this module cannot both create a VNet and attach to a different one. | string | null | no |
| zones | Availability zones the default system node pool spreads across. Azure subnets aren't zone-scoped — zone placement happens on the node pool itself. | list(string) | [ | no |
Outputs
Section titled “Outputs”| Name | Description |
|---|---|
| cluster_ca_certificate | Base64-encoded cluster CA certificate, for building a kubeconfig. |
| cluster_endpoint | The control plane's private FQDN — the only reachable endpoint, since this module always disables the public one. |
| cluster_name | Name of the AKS cluster. |
| helm_kubernetes | Drop-in value for the helm provider's kubernetes attribute, so a root configures it in one line. Carries no credential: kubelogin obtains a short-lived Entra token from the caller's Azure CLI login at call time; the server ID is AKS's well-known Entra application. PRECONDITION NOT YET MET BY THIS MODULE: the exec only authenticates against a cluster with Entra ID authentication enabled (azure_active_directory_role_based_access_control), which this module does not configure yet — local accounts remain its auth path. Until it is enabled, this output is the shape a root will consume, not a working login (docs/clouds/azure/limits.md). |
| location | Region the cluster and its resources were created in. |
| node_subnet_id | ID of the node subnet. |
| oidc_issuer_url | The cluster's OIDC issuer — what a workload identity federation binding built by the layer above (Crossplane's provider, or anything else) will need. |
| pod_cidr | Range the bootstrap module hands Cilium as its cluster pool. Passed through, never set on the cluster: azurerm refuses pod_cidr under BYO CNI. |
| resource_group_name | Name of the resource group the cluster and its resources live in, whether this module created it or not. |
| tags | The standard tag set applied to every billable resource this module creates. |
| vnet_name | Name of the VNet the cluster is attached to, whether this module created it or not. |