Episode 2 — Provision a Cluster and Design Node Pools
Last episode gave you the map: control plane (Azure's) vs nodes (yours). Now we build the "yours" part — create a real cluster from the CLI, connect kubectl, and design the node pools that everything else runs on.
Learning objectives
By the end of this episode you can:
- Create an AKS cluster with the Azure CLI and connect
kubectlto it. - Explain the difference between system and user node pools and why you separate them.
- Add node pools, and use mode, taints, and labels to steer where pods land.
- Recall the key production sizing rules and the immutable settings you must get right at creation time.
Why it matters
Node pools are the one design decision you can't fully undo later — VM size and maxPods are immutable per pool. Get the split and sizing right now and the rest of the module (scaling, upgrades, cost) becomes routine; get it wrong and you're recreating pools in production.
1. The four commands that create a working cluster
The entire "hello cluster" loop is four commands. Memorize this shape — you'll type it hundreds of times.
# 1. A resource group to hold the cluster object
az group create --name myAKSResourceGroup --location westus2
# 2. Create the cluster (system-assigned managed identity by default)
az aks create \
--resource-group myAKSResourceGroup \
--name myAKSCluster \
--node-count 3 \
--generate-ssh-keys
# 3. Download credentials and point kubectl at the cluster
az aks get-credentials --resource-group myAKSResourceGroup --name myAKSCluster
# 4. Verify you can talk to the API server
kubectl get nodesThings worth internalizing:
az aks createdefaults to 3 nodes if you omit--node-count; the quickstart uses 1 only to save money. Production wants 3+.- The initial pool is always a
Systemmode pool and its OS type must be Linux — you cannot create a Windows-only cluster. az aks get-credentialsmerges cluster access into your~/.kube/config; after that,kubectl"just works."- 💡 Confirm success with
az aks show ... --query provisioningState -o tsv→ should readSucceeded.
⚠️ Cleanup pitfall: deleting the cluster's resource group with az group delete also deletes the hidden MC_ node resource group and everything in it. Great for tearing down labs; catastrophic if you put unrelated resources in that RG.
2. System vs user node pools — the core design split
Every AKS cluster has at least one system node pool; you add user node pools for your apps.
| System node pool | User node pool | |
|---|---|---|
| Purpose | Host critical cluster pods (CoreDNS, metrics-server, konnectivity) | Host your application pods |
| OS type | Linux only | Linux or Windows |
| Minimum nodes | ≥ 2 (recommended 3) | 0 or more |
| AKS label | kubernetes.azure.com/mode: system | kubernetes.azure.com/mode: user |
| Min VM SKU | ≥ 4 vCPU / 4 GB, no B-series | Workload-dependent |
| Can scale to zero? | No (min 2) | Yes (min 0) |
🧠 Mnemonic: System pool keeps the cluster alive; user pool keeps the app alive. Never let a rogue app pod crowd out CoreDNS — that's why you separate them.
Why the separation matters: if an application pod runs on the system pool and consumes all resources, it can destabilize CoreDNS and break DNS for the whole cluster. Isolation prevents "one bad deployment takes down everything."
Enforcing isolation with a dedicated system pool
The system label only makes AKS prefer scheduling system pods there — it doesn't keep your apps out. To truly dedicate a system pool, add the well-known taint:
az aks nodepool add \
--resource-group myResourceGroup --cluster-name myAKSCluster \
--name systempool --node-count 3 \
--node-taints CriticalAddonsOnly=true:NoSchedule \
--mode SystemCriticalAddonsOnly=true:NoSchedule means "no pod schedules here unless it tolerates this taint" — and system add-ons carry that toleration. Your app pods don't, so they're pushed to user pools.
3. Adding and steering node pools
New pools created with az aks nodepool add default to User mode unless you pass --mode System. A typical two-pool production shape:
# System pool created with the cluster (mode System, 3 nodes)
# Add a user pool for applications:
az aks nodepool add \
--resource-group myResourceGroup --cluster-name myAKSCluster \
--name userpool1 \
--node-vm-size Standard_DS2_v2 \
--os-type Linux --os-sku Ubuntu \
--node-count 3 \
--mode User
# See what you have:
az aks nodepool list --resource-group myResourceGroup --cluster-name myAKSCluster \
--query "[].{Name:name, Mode:mode, Count:count}" -o tableThree tools to control where pods run
| Mechanism | Set on | Effect | Use for |
|---|---|---|---|
Mode (System/User) | Node pool | Marks purpose; drives default system-pod placement | Separating cluster vs app workloads |
Taint (e.g. sku=gpu:NoSchedule) | Node pool | Repels pods that don't tolerate it | Reserving nodes (GPU, dedicated tenants) |
Label (e.g. disktype=ssd) | Node pool | Attracts pods via nodeSelector/nodeAffinity | Targeting pods to matching nodes |
A pod combines these: it needs a toleration to get onto a tainted pool, and a nodeSelector/affinity to choose a labeled pool. The AKS quickstart app, for example, pins itself to user nodes with:
nodeSelector:
kubernetes.io/os: linux
kubernetes.azure.com/mode: user # land on the user pool, not the system pool💡 Tip: apply taints/labels to the pool, not individual nodes — AKS replaces nodes on upgrades/scaling, and per-node settings are lost. Pool-level properties survive.
4. Immutable settings — decisions you can't take back
Some node-pool properties are fixed at creation. To change them you must create a new pool, migrate workloads, delete the old pool:
- ⚠️ VM size (
--node-vm-size) — cannot change after creation. - ⚠️ Max pods per node (
--max-pods) — cannot change after creation. - ⚠️ Windows support — the cluster needs a
windowsProfileat creation (--windows-admin-username/password). You cannot add Windows to a Linux-only cluster later. - OS SKU choices per pool: Ubuntu (default), Azure Linux, Windows Server 2022/2025.
Other cluster-wide constraints to remember: all node pools use Virtual Machine Scale Sets, need the Standard SKU load balancer for multiple pools, and must live in the same virtual network. Pool names are lowercase alphanumeric, ≤ 12 chars (Linux) / ≤ 6 chars (Windows).
🧠 Mnemonic — "SVM is forever": SKU/VM size and Max-pods are set once. Plan them before you type create.
Key takeaways
- The create loop is 4 commands:
group create→aks create→get-credentials→kubectl get nodes. - The first pool is a Linux
Systempool; production uses 3+ nodes and Standard tier. - System pools run cluster services (Linux, ≥2 nodes, ≥4 vCPU/4 GB); user pools run your apps (Linux/Windows, 0+ nodes).
- Steer pods with mode (purpose), taints (repel), and labels (attract) — always at the pool level.
- VM size, max-pods, and Windows support are immutable — get them right at creation or recreate the pool.
Quick recall
- Q: Which command writes your kubeconfig? A:
az aks get-credentials. - Q: Minimum nodes for a production system pool? A: At least 2, 3 recommended.
- Q: Can a user node pool scale to zero? A: Yes; a system pool cannot (min 2).
- Q: You want to reserve a pool for GPU workloads only — taint or label? A: Taint (repel everything that doesn't tolerate it); pair with a label to attract the GPU pods.
- Q: You need to change a pool's VM size — how? A: You can't in place; create a new pool, migrate, delete the old one.
- Q: What taint dedicates a system pool? A:
CriticalAddonsOnly=true:NoSchedule.
Exercises
Exercise 2.1 — Build a production-shaped cluster.
Write the CLI commands to create a cluster named prodcluster in prod-rg (region eastus) with a 3-node system pool, then add a 3-node Ubuntu user pool named apps.
Worked solution
az group create --name prod-rg --location eastus
az aks create \
--resource-group prod-rg --name prodcluster \
--node-count 3 --generate-ssh-keys # system pool, 3 nodes
az aks get-credentials --resource-group prod-rg --name prodcluster
az aks nodepool add \
--resource-group prod-rg --cluster-name prodcluster \
--name apps --os-type Linux --os-sku Ubuntu \
--node-count 3 --mode User # user pool for apps(For real production you'd also set --tier standard, availability zones, and CNI options — covered in later episodes.)
Exercise 2.2 — Diagnose a scheduling problem.
You added CriticalAddonsOnly=true:NoSchedule to your only system pool, and now your application pods are stuck Pending. What happened, and what are two ways to fix it?
Worked solution
The taint repels every pod that doesn't tolerate it — including your apps — and you have no user pool for them to land on. Fixes:
- Add a user node pool (
--mode User) so app pods have somewhere to schedule (the recommended fix). - Or remove/relax the taint so the system pool accepts app pods again (not recommended for production isolation).
Exercise 2.3 — Spot the immutable mistake.
A colleague provisioned a pool with --node-vm-size Standard_DS2_v2 and --max-pods 30, and now needs Standard_D8s_v5 nodes with --max-pods 110. Can they az aks nodepool update these in place? What's the correct procedure?
Worked solution
No — both VM size and max-pods are immutable. Correct procedure: create a new node pool with the desired --node-vm-size and --max-pods, migrate the workloads (cordon/drain the old pool or reschedule via labels), then delete the old pool. Plan these values before creation to avoid this.
Coming up
Your cluster is running and node pools are designed. In Episode 3 we put workloads on them: the trio every admin lives with — Pods, Deployments, and Services — including rolling updates, rollbacks, and how traffic reaches your app.