04 · Azure Kubernetes Service (AKS) Basics¶
Azure Kubernetes Service (AKS) is Azure's managed Kubernetes control plane — Azure runs and patches the API server and etcd for you (free of charge); you manage and pay only for the worker node VMs. This module covers standing up a cluster, deploying a containerized app, and basic scaling — Level 2, Module 07 covers the container image side (building/pushing to ACR) that feeds into this.
Create a cluster¶
az group create --name rg-aks-demo --location eastus
az aks create \
--resource-group rg-aks-demo \
--name aks-demo \
--node-count 2 \
--node-vm-size Standard_B2s \
--generate-ssh-keys \
--enable-managed-identity
az aks get-credentials \
--resource-group rg-aks-demo \
--name aks-demo
az aks get-credentials merges the cluster's connection details into your
local ~/.kube/config, so kubectl immediately targets the right cluster.
--enable-managed-identity gives the cluster its own identity for talking
to other Azure resources (like ACR) without a stored secret — the same
managed identity concept covered in
Module 06.
Gotcha: cluster creation takes 5-10 minutes, and each node is a real VM
billed at its normal compute rate the moment it exists — a 2-node
Standard_B2s cluster keeps costing money whether or not you deploy
anything to it, until the cluster (or resource group) is deleted.
kubectl get nodes
# NAME STATUS ROLES AGE VERSION
# aks-nodepool1-12345678-vmss000000 Ready <none> 4m v1.29.2
# aks-nodepool1-12345678-vmss000001 Ready <none> 4m v1.29.2
Deploy a containerized workload¶
Kubernetes objects are usually described declaratively in YAML and applied
with kubectl apply — the same "declare desired state, let the system
reconcile" idea as Bicep, just for a different control plane.
deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: hello-aks
spec:
replicas: 3
selector:
matchLabels:
app: hello-aks
template:
metadata:
labels:
app: hello-aks
spec:
containers:
- name: hello-aks
image: mcr.microsoft.com/azuredocs/aks-helloworld:v1
ports:
- containerPort: 80
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "250m"
memory: "256Mi"
---
apiVersion: v1
kind: Service
metadata:
name: hello-aks-svc
spec:
type: LoadBalancer
ports:
- port: 80
selector:
app: hello-aks
kubectl apply -f deployment.yaml
kubectl get deployments
kubectl get pods
kubectl get service hello-aks-svc
# Watch EXTERNAL-IP go from <pending> to a real public IP
The Deployment keeps 3 pod replicas running (restarting any that
crash); the Service of type: LoadBalancer provisions an actual Azure
Load Balancer with a public IP in front of them, using the same Load
Balancer resource from Module 03 under the
hood — Kubernetes and Azure networking meet at this point.
Gotcha: resources.requests/limits aren't optional in any real
cluster — without them, one greedy pod can starve every other pod on the
same node, and the scheduler can't make sensible bin-packing decisions.
Always set them, even generously, from the first deployment.
Scaling¶
Manual pod scaling changes replica count immediately:
Horizontal Pod Autoscaler (HPA) adjusts replica count automatically based on observed CPU/memory:
Cluster autoscaler (node-level, not pod-level) adds or removes nodes when pods can't be scheduled due to insufficient capacity:
az aks update \
--resource-group rg-aks-demo \
--name aks-demo \
--enable-cluster-autoscaler \
--min-count 1 \
--max-count 5
Gotcha: HPA and the cluster autoscaler solve different problems and you usually want both — HPA reacts fast by adding pods to existing node capacity; the cluster autoscaler reacts slower (a new node takes a minute or two to provision and join) to add capacity when there's no room left for HPA to add more pods.
Logs, exec, and troubleshooting¶
kubectl logs deployment/hello-aks
kubectl logs -f pod/<pod-name> # follow/tail
kubectl exec -it pod/<pod-name> -- sh # shell into a running container
kubectl describe pod <pod-name> # events: scheduling failures, image pull errors, OOMKills
kubectl describe is usually the first thing to run when a pod is stuck
Pending or CrashLoopBackOff — the Events section at the bottom
tells you why (can't pull the image, no node has enough CPU/memory free,
liveness probe failing, and so on).
How It Actually Works¶
An AKS cluster's control plane (API server, etcd, scheduler,
controller-manager) is fully managed by Azure and runs on Microsoft-owned
infrastructure invisible to you — az aks show never returns a VM for it
because there isn't one you're billed for or can SSH into; what you do see
and pay for is the node pool, which is a Virtual Machine Scale Set
(VMSS) that AKS provisions and keeps registered with that hidden control
plane via the kubelet running on each node. When you run kubectl apply,
the request goes to the managed API server, gets persisted to etcd (also
hidden), and the scheduler picks a node by matching the pod's resource
requests against each node's reported allocatable capacity — kubelet then
pulls the container image and starts it via the node's container runtime
(containerd), all of which is standard upstream Kubernetes; Azure's
contribution is entirely in provisioning/patching/scaling the
infrastructure underneath, not in a modified control plane.
Networking inside AKS depends on the CNI plugin chosen at cluster creation:
kubenet assigns pod IPs from a separate, non-routable range and relies
on Azure UDRs plus NAT to get pods talking to the VNet, while Azure CNI
assigns each pod a real IP directly from your VNet's subnet — a materially
different mechanism that's why Azure CNI pods can be reached directly by
other VNet resources but consume subnet address space per pod (this is the
actual cause of AKS subnets needing to be sized much larger than the pod
count would suggest). kubectl logs/exec don't reach the pod directly —
they're proxied through the managed API server, which opens a connection
back to the kubelet on the node hosting that pod, which is why cluster
logs/exec commands fail specifically when the API server can't reach a
node's kubelet (e.g. NSG blocking the control plane's outbound IP range),
not when your own client's network is fine.
Cheat sheet¶
| Command | Purpose |
|---|---|
az aks create --node-count --generate-ssh-keys |
Create an AKS cluster. |
az aks get-credentials |
Merge cluster access into local ~/.kube/config. |
kubectl apply -f <file>.yaml |
Create/update objects from a manifest. |
kubectl get nodes/pods/deployments/service |
List cluster objects. |
kubectl scale deployment --replicas |
Manually change replica count. |
kubectl autoscale deployment --cpu-percent --min --max |
Create an HPA (pod-level autoscale). |
az aks update --enable-cluster-autoscaler --min-count --max-count |
Enable node-level autoscale. |
kubectl logs / exec -it / describe pod |
Debug a running or failing pod. |
Exercise¶
- Create a 2-node AKS cluster and confirm
kubectl get nodesshows both asReady. - Deploy the
hello-aksDeployment + LoadBalancer Service above, and confirm you cancurlthe app at its external IP once it's assigned. - Scale to 5 replicas manually, then remove the manual scale and instead
create an HPA with
min=2 max=10targeting 70% CPU. - Deliberately request more CPU than any node has free in a test pod, and
use
kubectl describe podto find the scheduling failure event explaining why it's stuckPending. - Delete the resource group when finished.