Skip to content
CAMPUX Cloud Bootcamp Lab · Class 27b · AKS ← All labs
Hands-On Lab · Beginner
Time varies · AKS Free management tier · Cloud Shell
Billable resources · cleanup required
Class 27b · Workloads & Config

One workload. Two memory rules.

Run a modest memory workload without a container limit, then set a lower limit and inspect its OOM kill. Next, generate CPU load and observe how a HorizontalPodAutoscaler requests more replicas. The lab uses billable Azure resources and ends with verified cleanup.

Fig. 1 · Same memory request, two container limits · scroll to compare on a narrow screen
A memory limit bounds one container's use; node pressure and neighbouring workload health require separate checks. no limit set node pod · leak no limit pod · other app no ceiling — nothing capped how far this could have grown requests + limits set node pod · leak limit: 100Mi pod · other monitor too only this pod is OOMKilled — check node health
Start

Run one step, then check it

Read the step before copying. Bash commands belong in Git Bash on Windows or Azure Cloud Shell set to Bash; PowerShell uses different syntax. A JSON, YAML, Bicep or application-code example belongs in the named file, not directly at the terminal prompt. Follow any shell-specific instructions below.

Keep the same terminal open so earlier variables remain available. Copy one complete command block without the terminal's $ prompt. A backslash continues a command onto the next line. If Bash shows a lone >, it is waiting for the rest of a quote, command or file block. Finish the complete block; if the paste was incomplete, press Ctrl+C and copy it again. Do not paste the next step into an unfinished command.

A line such as RG="training-group" stores a value without printing it. Output captured with $(...) is also stored; a blank screen alone does not prove failure. A sleep deliberately waits, and -o none suppresses normal output. Wait for your normal prompt, then run the step's checkpoint. After an error, stop and keep the exact command and message; later steps cannot repair a missing resource. Never share passwords, access tokens or secret values in a screenshot.

Why

A container limit changes where memory pressure is handled

Without a container memory limit, usage can grow until other constraints intervene, including node memory pressure and pod eviction. A limit gives this container a cgroup ceiling; crossing it can terminate the container with reason OOMKilled. That helps bound one workload, but it does not guarantee neighbouring pods or the node will never be affected. This exercise uses only a modest allocation and does not try to exhaust the node.

You will deploy a small demo container using stress, first with no memory limit and then with a 100Mi limit. Afterward, a separate example app demonstrates CPU-based HPA. Metrics, scheduling and stabilization can affect the observed timing and replica count.

A memory limit bounds one container; it does not replace capacity planning.

Before you begin — one-time setup

You need an Azure subscription with AKS and VM quota, permission to create and delete resources, and the Azure CLI (az) in Bash. The setup page covers installation and sign-in. Azure Cloud Shell (Bash) includes az and kubectl; check the selected subscription before creating anything.

Cost & teardown

This lab creates its own uniquely named cluster so cleanup cannot accidentally delete the first-cluster lab or another project. AKS Free has no cluster-management fee, but node compute and other resources can bill. Check current pricing, quota, and spending alerts; keep the resource names in the same Bash session and verify both the lab and managed node groups are deleted.

Step 1

The cluster

Use a dedicated cluster for this exercise. Run these Bash commands in Cloud Shell or a local Bash terminal, and keep the printed names for teardown. Creation time and available VM sizes vary by subscription and region.

# Windows/Git Bash: stop it mangling /subscriptions/... arguments (harmless on macOS/Linux)
export MSYS_NO_PATHCONV=1

az account show --query '{name:name,subscriptionId:id}' -o table
RG="rg-cx27b-aks-$RANDOM-$RANDOM"
CLUSTER="cx27b-aks-$RANDOM"
printf 'Group: %s\nCluster: %s\n' "$RG" "$CLUSTER"
az group create -n "$RG" -l eastus

# Free management tier; node and related resources can bill
# if Azure says this size "is not allowed in your subscription", the same error lists the sizes
# that are; pick a 2-vCPU one from that list (Standard_D2as_v4 worked for us in October 2026)
az aks create -n "$CLUSTER" -g "$RG" \
  --tier free \
  --node-count 1 \
  --node-vm-size Standard_D2as_v4 \
  --enable-managed-identity \
  --generate-ssh-keys

az aks get-credentials -n "$CLUSTER" -g "$RG"
kubectl get nodes
Checkpoint kubectl get nodes lists one node, status Ready. AKS ships a metrics-server by default, so kubectl top node and kubectl top pod already work — no extra install.
Step 2

Observe a modest workload without a container limit

Deploy a demo pod that asks to allocate roughly 250 MB, without a container resource limit. This is deliberately modest; do not increase it to test node exhaustion. The result shows that this container has no configured ceiling, not that Kubernetes lacks node-pressure safeguards.

kubectl create namespace leak-demo

cat > leak-unbounded.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: leak-unbounded
  namespace: leak-demo
spec:
  containers:
  - name: leak
    image: polinux/stress
    command: ["stress"]
    args: ["--vm", "1", "--vm-bytes", "250M", "--vm-hang", "1"]
EOF

kubectl apply -f leak-unbounded.yaml
kubectl get pod leak-unbounded -n leak-demo -w   # Ctrl-C once it is Running

While it runs, watch the node's memory climb and the pod's own report:

# run repeatedly, or in a second Cloud Shell tab
kubectl top node
kubectl top pod -n leak-demo
Checkpoint After the pod reaches Running, kubectl top pod -n leak-demo should show a higher memory reading than before. It may differ from 250Mi because the tool argument, process overhead and reporting units differ. If the pod fails, inspect kubectl describe pod leak-unbounded -n leak-demo rather than assuming it ran successfully.

Commands inside $(...) save their output in a variable. Check the printed identifier or the checkpoint before continuing.

kubectl describe node $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') | grep -A3 Conditions
kubectl describe pod leak-unbounded -n leak-demo | tail -20
Checkpoint Check MemoryPressure and the pod's events. The goal is to observe memory use without pressuring the node. If the node reports pressure, stop the demo and delete the pod. Node-pressure eviction remains possible even when a container has no configured memory limit.
kubectl delete pod leak-unbounded -n leak-demo
Step 3

Requests and limits: the same leak, contained

Now run the identical leak, but give it a memory request (what the scheduler reserves) and a limit (the hard ceiling the kubelet enforces). The container will try to allocate the same 250Mi it did before; this time it cannot get past 100Mi.

cat > leak-limited.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: leak-limited
  namespace: leak-demo
spec:
  containers:
  - name: leak
    image: polinux/stress
    command: ["stress"]
    args: ["--vm", "1", "--vm-bytes", "250M", "--vm-hang", "1"]
    resources:
      requests:
        memory: "50Mi"
        cpu: "50m"
      limits:
        memory: "100Mi"
        cpu: "250m"
EOF

kubectl apply -f leak-limited.yaml
kubectl get pod leak-limited -n leak-demo -w   # Ctrl-C once you see it restart
Checkpoint The container should exceed its 100Mi limit and terminate with reason OOMKilled. Its default pod restart policy then restarts it, so Restart Count can rise. Confirm the reason in kubectl describe pod; a short kubectl get -w view may miss the brief terminated state. The limit bounds this container's memory use but does not prove other workloads were unaffected.
kubectl describe pod leak-limited -n leak-demo | grep -A6 "Last State"
kubectl top node
What just happened

The two runs use the same allocation command. The second container has a 100Mi cgroup memory limit, so the kernel can OOM-kill it when it tries to use more. The restart follows the pod's restart policy. In production, choose requests and limits from measured use and the workload's failure tolerance; a too-low limit creates its own crash loop.

kubectl delete namespace leak-demo
Step 4

Add a HorizontalPodAutoscaler

Deploy the Kubernetes example app, which includes a CPU request. CPU-utilization HPA needs that request to calculate its percentage. The example manifest is fetched from Kubernetes documentation; inspect it before applying it in any cluster that holds real workloads.

kubectl apply -f https://k8s.io/examples/application/php-apache.yaml
kubectl get deployment php-apache
kubectl get pod -l run=php-apache

# target 50% average CPU utilization across 1-5 replicas
kubectl autoscale deployment php-apache --cpu=50% --min=1 --max=5
kubectl get hpa php-apache -o yaml | grep apiVersion
Checkpoint kubectl get hpa php-apache should eventually show a current/target CPU percentage and a replica count. If the current metric remains unknown, inspect HPA events and confirm metrics are available. The API version comes from the installed client and cluster; verify the printed value rather than assuming it.
Step 5

Drive load, watch it scale up — then back down

In one terminal, generate sustained load against the service. In another, watch the HPA react.

This block includes a deliberate wait. Leave this terminal open until the normal prompt returns; a wait is not proof that access has propagated.

# terminal A — hammer the service with requests until you stop it
kubectl run -i --tty load-generator --rm --image=busybox:1.28 --restart=Never -- \
  /bin/sh -c "while sleep 0.01; do wget -q -O- http://php-apache; done"
# terminal B — watch replicas climb as CPU utilization crosses 50%
kubectl get hpa php-apache --watch
Checkpoint Watch TARGETS and REPLICAS for several minutes. The HPA may raise desired replicas when measured CPU stays above the target; scale-up depends on available metrics, and new pods may remain Pending if this one-node cluster lacks capacity. Stop the load generator with Ctrl-C, then confirm its pod is gone. Scale-down can lag because of a stabilization window; inspect the HPA configuration and events if it does not return to one replica.
What just happened

The HPA compares measured CPU utilization with the 50% target and adjusts the Deployment's desired replica count. A stabilization window can delay scale-down; the usual default is 300 seconds, but a cluster can configure another value. HPA changes the desired count, while the scheduler still needs capacity to run the new pods.

kubectl delete hpa php-apache
kubectl delete -f https://k8s.io/examples/application/php-apache.yaml
Down

Tear it down

Use the same Bash session so $RG and $CLUSTER still name this lab. Stop any watch or load-generator commands with Ctrl-C. Record the AKS-managed node group before deleting the lab group, then verify both are gone.

Commands inside $(...) save their output in a variable. Check the printed identifier or the checkpoint before continuing.

printf 'Delete lab group: %s\n' "$RG"
az group show -n "$RG" --query '{name:name,resourceId:id}' -o table
NODE_RG=$(az aks show -n "$CLUSTER" -g "$RG" --query nodeResourceGroup -o tsv 2>/dev/null || true)
printf 'Managed node group: %s\n' "$NODE_RG"
az group delete -n "$RG" --yes
az group exists -n "$RG"             # expected: false
if [ -n "$NODE_RG" ]; then az group exists -n "$NODE_RG"; fi  # expected: false
Checkpoint Both known groups should be absent. If creation failed before AKS reported a managed group, inspect the subscription's remaining resource groups and cost view. If a group remains after deletion, investigate it in the Azure portal before assuming billing has stopped.
End

What you can now honestly claim

You compared the same modest allocation with and without a container memory limit, confirmed an OOM kill under the lower limit, and watched an HPA respond to CPU load if metrics and capacity allowed. You also verified cleanup of the billable lab resources. The transferable skill is to read pod events, measured use, HPA status and node capacity before changing a production workload.