production-platform-Azure

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

Context

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:

Facts read from the AKS documentation:

Decision

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:

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.

Result of the first create

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:

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:

Limitations of this plan:

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.

Consequences

Open questions

Alternatives considered