Lesson 13 HA & Autoscaling
Theme:

Homework 13 Solutions — HA & Autoscaling

Lesson 13 · Cloud Infrastructure & DevOps

Solution to Task 1 — Worker HPA with behavior

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: url-shortener-worker
  namespace: url-shortener
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: url-shortener-worker
  minReplicas: 1
  maxReplicas: 4
  metrics:
  - type: Pods
    pods:
      metric:
        name: worker_jobs_processed_per_second
      target:
        type: AverageValue
        averageValue: "50"
  behavior:
    scaleUp:
      policies:
        - type: Percent
          value: 100
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 600
      policies:
        - type: Percent
          value: 50
          periodSeconds: 60

The prometheus-adapter rule installed in L13 already exposes worker_jobs_processed_per_second because the source counter (worker_jobs_processed_total) follows the _total suffix naming convention that the rule matches.

Common mistake: putting the HPA on a Pods metric without confirming the adapter is publishing it first. Always verify with kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/url-shortener/pods/*/worker_jobs_processed_per_second" before applying the HPA. An HPA with an unresolvable metric silently stays at minReplicas.

Solution to Task 2 — Canary via nginx-ingress annotations

# Main Ingress — unchanged, points at the api Service (which selects the active colour)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: url-shortener-api
  namespace: url-shortener
spec:
  ingressClassName: nginx
  rules:
  - host: url-shortener.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: url-shortener-api
            port: {number: 80}
---
# Canary Ingress — same host, sends 10% to a green-only Service
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: url-shortener-api-canary
  namespace: url-shortener
  annotations:
    nginx.ingress.kubernetes.io/canary: "true"
    nginx.ingress.kubernetes.io/canary-weight: "10"
spec:
  ingressClassName: nginx
  rules:
  - host: url-shortener.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: url-shortener-api-green   # a Service selecting color=green only
            port: {number: 80}
---
# Green-only Service for the canary
apiVersion: v1
kind: Service
metadata:
  name: url-shortener-api-green
  namespace: url-shortener
spec:
  selector:
    app.kubernetes.io/component: api
    color: green
  ports: [{port: 80, targetPort: 8080}]

Six-line summary:
Blue/green flips 100% of traffic at once — fast, simple, but a bug on green hits every user before you notice. Canary sends a small fraction (e.g., 10%) to the new version — slower cut-over, more infrastructure, but observability gets a small-population signal before full rollout. Use canary when you have automated metric comparison (Argo Rollouts, Flagger) and a user pool large enough to make 10% statistically meaningful. Use blue/green for low-traffic internal services where 10% would only be ~1 user/minute and a fast rollback matters more than a gradual ramp. The key failure mode of canary without metrics analysis is the same as blue/green: you don’t notice the error until the dashboard pings you.

Common mistake: running a canary without comparing the canary’s metrics to the baseline’s metrics. The entire value proposition of canary is “diff blue vs green before committing” — without a metric comparison the canary is just a slow blue/green with extra operational complexity.

Solution to Task 3 — PDB stress test

What happens with minAvailable: 100%: kubectl drain prints:

error when evicting pods: Cannot evict pod as it would violate the pod's disruption budget.

and blocks indefinitely. The HPA may scale up to replace the pod that would be evicted, but because the PDB requires all current replicas to be available, the drain can never find a safe window to evict even one pod. kubectl get events --field-selector reason=EvictionBlocked -n url-shortener confirms the cause.

Bullets — when minAvailable: 100% is the right choice:

  1. One-replica admin tooling where you would rather block the drain than risk a brief downtime (small batch coordinators, scheduler processes, webhook validators that back every API request).
  2. Stateful jobs mid-operation that cannot be safely interrupted and cannot checkpoint — e.g., a long-running data migration that writes to an external system with no idempotency.
  3. Compliance-driven setups where any availability dip is a reportable event regardless of duration (banking, healthcare, regulated financial APIs).

Bullets — when minAvailable: 100% is wrong:

  1. Stateless web APIs with ≥ 2 replicas — blocking drain means you cannot upgrade cluster nodes without coordinated downtime, negating the value of rolling upgrades.
  2. HPA-controlled workloads — 100% of a variable replica count is a moving target. At peak the HPA may hold 8 replicas; the drain can never proceed because it can never guarantee all 8 stay up.
  3. Any service whose clients already retry on 5xx — a well-behaved internal API tolerates one pod restart; the user cost is one client retry, not a visible outage.
Common mistake: copy-pasting minAvailable: 1 from a guide written for a 2-replica Deployment onto a 1-replica service. Draining that node evicts the only pod, dropping available to 0 — which violates a budget of 1. The drain blocks forever. Always sanity-check minAvailable against the actual replicas floor.
Looking ahead: Lesson 14 makes all of the Helm operations you just did happen automatically — a git push triggers ArgoCD, which applies the chart. HPA edits get reverted unless they are in git. Drift is detected within 3 minutes. The same chart, controlled by the same values, with git as the single source of truth.