Scale the API under load. Survive node drains. Blue/green deploys with one Helm value.
We can now see latency, errors, traffic, saturation. The four golden signals are live in Grafana. Alerts fire before users notice.
HPA scales replicas when traffic climbs. PDB protects capacity during node drains. Topology spread keeps replicas off the same host. Blue/green flips traffic with one Helm value.
HorizontalPodAutoscaler watches a metric and adjusts the Deployment’s replicas between minReplicas and maxReplicas to keep the metric at target. Two metric sources:
resource — CPU or memory from metrics-server.external / pods — custom metrics (e.g., our http_requests_total from L12 via prometheus-adapter).apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata: { name: api }
spec:
scaleTargetRef: { kind: Deployment, name: url-shortener-api }
minReplicas: 2
maxReplicas: 8
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }
Target 70% CPU utilisation, scale 2–8. Requires requests.cpu set on the Deployment — otherwise “utilisation” is undefined and the HPA does nothing.
metrics:
- type: Pods
pods:
metric: { name: http_requests_per_second }
target: { type: AverageValue, averageValue: "100" }
“100 rps per pod” reads directly from real traffic. Requires the prometheus-adapter to expose http_requests_per_second via the Kubernetes metrics API. CPU lags traffic by 30–60 s on bursty workloads; request rate doesn’t.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: api }
spec:
minAvailable: 50%
selector: { matchLabels: { app: api } }
When a node drains, kubectl drain honours the PDB and refuses to evict pods that would drop available count below 50%. Without a PDB, a rolling node upgrade can take you to 0 replicas.
minReplicas: 1 + PDB minAvailable: 1 — drain blocks forever because evicting the only pod violates the budget.
topologySpreadConstraints — don’t co-locate replicastopologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector: { matchLabels: { app: api } }
Prefer to put pods in different AZs; if not possible, still schedule (ScheduleAnyway vs DoNotSchedule). One zone failure no longer eats all your replicas.
kubernetes.io/hostname for node-level spread. Both constraints use ScheduleAnyway so they work on a single-node kind cluster.
# values-prod.yaml
api:
image:
repository: ghcr.io/me/url-shortener-api
tag: blue # flip to green for cut-over
service:
api:
selector: { app: api, color: blue } # also flipped
Two Deployments (api-blue, api-green), one Service whose selector picks one. The flip is --set api.color=green. Rollback is --set api.color=blue. No Ingress change; no DNS change.
minReplicas and maxReplicasminReplicas: 2 minimum, always, in prod. One pod = one node failure = downtime.maxReplicas should cover 2× peak observed traffic with headroom — but not arbitrarily large. maxReplicas × requests.cpu is the upper bound on cluster capacity you can reserve.maxReplicas, you have a cluster-autoscaler problem, not an HPA problem.requests vs limits for the HPAHPA reads CPU utilisation = used / requests. If requests.cpu = 100m and the pod uses 500m, utilisation = 500% — HPA scales out aggressively. If requests.cpu = 1000m and the pod uses 500m, utilisation = 50% — HPA may scale in.
Tune requests to the steady-state usage, then HPA’s averageUtilization is meaningful.
limits.cpu below requests.cpu. Many platforms ban limits.cpu outright — it causes CPU throttling under burst even when nodes have spare capacity.
sample-app/helm/templates/
api-deployment.yaml # + topologySpreadConstraints, + color label, split blue/green
worker-deployment.yaml # + topologySpreadConstraints
hpa.yaml # NEW: HPA on api, CPU + request-rate metrics
pdb.yaml # NEW: PDB minAvailable=50% on api + worker
api-service.yaml # selector includes color: {{ .Values.api.color }}
sample-app/helm/values.yaml
api: { color: blue, minReplicas: 2, maxReplicas: 8 }
sample-app/helm/values-prod.yaml
api: { color: blue } # flip here for cut-over
canary-weight.minReplicas: ≥ 2topologySpreadConstraints against zone and hostnamerequests.cpu set realisticallybehavior.scaleUp bounded to ≤ 100% per 60 s (no stampedes)behavior.scaleDown.stabilizationWindowSeconds: 300 (no flapping)color labelshelm upgrade by hand; git push is the only deploy. Drift detection. Sync waves. App-of-Apps.