An Azure platform for running a service on AKS with Terraform, Argo CD, CI, observability and incident handling.
View the Project on GitHub Franklindot04/production-platform-Azure
The target subscription has a regional vCPU quota of 4 in swedencentral (ADR 0003). The cluster has to fit in it, and so does any node that AKS adds during an upgrade.
Facts read with the Azure CLI and the Azure API, without creating anything:
swedencentral, Standard_D2s_v5 (2 vCPU, 8 GB) and Standard_D4s_v5 (4 vCPU, 16 GB) are available. Both are in the DSv5 family, whose quota is 4. The total regional quota is 4. Nothing is in use.northeurope, the standard sizes with 2 or 4 vCPU were not available. Only confidential-computing sizes were available there. That is why the workloads are in swedencentral.swedencentral is 1.36. Its newest patch is 1.36.5. Version 1.37 exists and is not the default.Facts read from the AKS documentation:
The first plan is one system pool and no user pool.
| Setting | Value |
|---|---|
| Pool name and mode | system (system pool) |
| Node size and count | Standard_D2s_v5 × 2, which is 4 vCPU and 16 GB in total |
| Recorded alternative | Standard_D4s_v5 × 1, which is 4 vCPU and 16 GB in total |
| Autoscaler | off |
| OS disk | managed, 32 GB |
| Pool upgrade surge | max_surge = "1" (the default, written down, not exercised) |
| Kubernetes version | 1.36.5, pinned |
| Cluster upgrade channel | not set (see the limitations) |
| Node OS upgrade channel | None |
| Node provisioning | Manual, no default node pools |
| Tier | Free |
| Access | public API with authorized ranges (ADR 0005), Entra ID RBAC not enabled |
| Network | Azure CNI Overlay and the ranges in ADR 0011 |
Both options break an AKS guideline:
Standard_D2s_v5 nodes break the 4 vCPU per node minimum.Standard_D4s_v5 node breaks the two-node minimum.Neither option leaves room for a surge node. Both use the whole quota of 4 vCPU. A surge node cannot start, so an in-place upgrade or a node replacement fails. The policy is to pin the version, leave upgrades off and re-create the cluster to change a version or a node image. Max surge 0 is not available on a system pool, so the default of 1 is kept. It cannot start without free quota.
A plan does not check the VM size, the version or the quota against Azure, so the first real create (#27) tested the pool. The results are in the next section. They cover the create, not an upgrade or a node replacement.
The dev persistent root and then the dev session root were applied once, each from a saved plan that was shown and approved first. The cluster was read back, and then the session layer was destroyed. session.sh check found nothing left after the destroy.
AKS accepted the configuration. Read back with az aks show and kubectl:
| Setting | Result |
|---|---|
| Provisioning state | Succeeded, power state Running |
| Pool | system, mode System, 2 nodes of Standard_D2s_v5, autoscaler off, managed OS disk |
| Kubernetes version | 1.36.5 on the control plane and both nodes |
| Node provisioning | mode Manual, default node pools None |
| Cluster upgrade channel | none, with the Terraform attribute unset |
| Node OS upgrade channel | None |
Pool max_surge |
1. No upgrade was run, so the surge was not exercised. |
| Network | Azure CNI Overlay, pod range 10.244.0.0/16, service range 10.96.0.0/16, outbound through a load balancer |
| Tier and access | Free, public API limited to one authorized range, Entra ID RBAC not enabled, local accounts enabled |
The create needed no role for the cluster identity on the node subnet. Both nodes became Ready. A service of type LoadBalancer in that subnet was not tried.
Capacity of each node, read with kubectl describe nodes right after the create:
| Per node | Both nodes | |
|---|---|---|
| Capacity | 2 vCPU, 8126892 Ki memory | 4 vCPU |
| Allocatable | 1900m CPU, 5927340 Ki memory (about 5.65 GiB) | 3.8 vCPU, about 11.3 GiB |
| Requested by system pods | 767m CPU and 892 Mi on one node, 500m CPU and 666 Mi on the other | 1267m CPU (33% of allocatable), 1558 Mi (about 13%) |
That leaves about 2.5 vCPU and 9.8 GiB of requests on an idle cluster for the tools. The two nodes are separate, so one pod cannot use more than one node’s share.
The cluster ran 20 pods, all Running, all in kube-system:
azure-cns, azure-ip-masq-agent, cloud-node-manager, csi-azuredisk-node, csi-azurefile-node and kube-proxy.coredns and konnectivity-agent, one on each node, and metrics-server, both on the same node.coredns-autoscaler and konnectivity-agent-autoscaler.AKS created these in the node resource group: a virtual machine scale set, a load balancer, a public IP address, a network security group and a user-assigned identity. session.sh check listed all of them while the cluster ran and none after the destroy.
Timing, as reported by Terraform. These are single runs, and the times are not an average:
| Step | Time |
|---|---|
| Persistent apply (one resource group) | 28 s |
| Session apply | about 7 minutes: resource group 28 s, virtual network 11 s, subnet 15 s, cluster 5 min 58 s |
| Session destroy | about 7 minutes: cluster 5 min 47 s, subnet 14 s, virtual network 12 s, resource group 30 s |
What the create did not test:
Sizing against the in-cluster tools:
docs/sessions/session-1-results.md).Limitations of this plan:
automatic_upgrade_channel is left unset because the provider rejected "none". AKS reports the channel as none, so unset means no automatic upgrades. The first real create confirmed this.The plan output is saved in docs/plans/. After the first create, the session layer was destroyed. The persistent resource group remains and is empty.
Free tier has no uptime SLA.Standard_D4s_v5 node? Only the two-node pool was created.Standard_D4s_v5 nodes. Follows both guidelines. Needs 8 vCPU, which is above the quota.northeurope. Not needed, because standard sizes are available in swedencentral. Price not checked.