Progressive delivery & service mesh
A rolling update (K3) swaps every replica and hopes for the best. Progressive delivery ships change to a slice of real traffic, watches metrics, and promotes or rolls back automatically. This lesson covers canary and blue-green, the controllers that automate them (Argo Rollouts, Flagger) with analysis-driven promotion, and how traffic actually gets shifted. Then the substrate that makes fine-grained traffic control and zero-trust networking possible: a service mesh (Istio / Linkerd) — mTLS, traffic policy, sidecar vs sidecarless — and the honest question of when a mesh is worth it.
kubectl here needs a cluster (minikube/kind local, or EKS) and output is illustrative. Argo Rollouts, Flagger, Istio and Linkerd change CRDs and flags frequently — verify every spec against the current docs before relying on it. The promotion-decision logic (compare canary metrics to a threshold, promote or abort) is plain arithmetic and is given as offline-runnable Python that needs no cluster.Learning objectives
- Contrast a rolling update with canary and blue-green, and when each fits.
- Automate progressive delivery with Argo Rollouts (or Flagger) driven by analysis.
- Explain analysis-based promotion: compare canary SLIs to a threshold, then promote or auto-rollback.
- Understand how traffic shifting works — replica-count weighting vs mesh/ingress weighting.
- Explain what a service mesh gives you: mTLS, traffic policy, observability, and sidecar vs sidecarless.
- Decide when a mesh is (and isn't) worth its operational cost.
1 · Rolling vs canary vs blue-green
The default Deployment strategy is a rolling update: gradually replace old Pods with new ones. It's simple but blunt — every user hits the new version as it rolls, and a bad version is discovered only after it's widely live. Progressive delivery adds a controlled exposure step.
| Strategy | How it exposes the new version | Rollback | Cost / caveat |
|---|---|---|---|
| Rolling | Replace replicas gradually; all users mix old+new | Roll back the Deployment | No metric gate; bad version reaches everyone |
| Canary | Send a small % of traffic to v2, watch, then ramp | Shift traffic back to 0% | Needs traffic-splitting + metric analysis |
| Blue-green | Run v2 fully in parallel; flip 100% at once | Flip back instantly | Double the resources during the window |
2 · Automating it: Argo Rollouts
Doing canary by hand — edit replicas, watch Grafana, decide — doesn't scale and isn't reliable at 3am. Argo Rollouts replaces the Deployment with a Rollout CRD that encodes the steps and the analysis, and drives them automatically. Flagger is the analogous tool (often paired with a mesh). A Rollout's steps describe the canary ramp and where to pause for an analysis gate:
rollout.yamlapiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata: { name: llm-api }
spec:
replicas: 6
selector: { matchLabels: { app: llm-api } }
template: { } # same pod template as a Deployment
strategy:
canary:
steps:
- setWeight: 10 # 10% of traffic to the new version
- pause: { duration: 5m }
- analysis: # gate: run the AnalysisTemplate below
templates: [ { templateName: success-rate } ]
- setWeight: 50
- pause: { duration: 5m }
- setWeight: 100 # full promotion if analysis kept passing
analysis.yamlapiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata: { name: success-rate }
spec:
metrics:
- name: success-rate
interval: 1m
successCondition: result[0] >= 0.99 # abort the rollout if it dips
failureLimit: 2
provider:
prometheus:
address: http://prometheus.monitoring:9090
query: |
sum(rate(http_requests_total{app="llm-api",status!~"5.."}[2m]))
/ sum(rate(http_requests_total{app="llm-api"}[2m]))
3 · Analysis-driven promotion, modeled offline
Strip away the CRDs and the promotion decision is simple arithmetic: after each step, compare the canary's SLIs to the stable's (or to an absolute threshold); promote if healthy, abort if not, with a tolerance so you don't abort on noise. Here it is as offline-runnable Python — the same logic the controller runs:
promotion.pydef evaluate_canary(canary, stable, min_success=0.99, max_latency_ms=800, latency_tol=1.2):
"""Return ('promote'|'abort', reason). canary/stable are dicts of measured SLIs."""
# 1) Absolute success-rate floor (an SLO-style gate).
if canary["success_rate"] < min_success:
return "abort", f"success {canary['success_rate']:.3f} < {min_success}"
# 2) Absolute latency ceiling.
if canary["p95_ms"] > max_latency_ms:
return "abort", f"p95 {canary['p95_ms']}ms > {max_latency_ms}ms"
# 3) Relative guard: canary shouldn't be much worse than stable (catches regressions).
if canary["p95_ms"] > stable["p95_ms"] * latency_tol:
return "abort", f"p95 {canary['p95_ms']}ms >> stable {stable['p95_ms']}ms"
return "promote", "canary healthy"
stable = {"success_rate": 0.999, "p95_ms": 500}
good = {"success_rate": 0.998, "p95_ms": 540}
bad = {"success_rate": 0.995, "p95_ms": 900}
print(evaluate_canary(good, stable)) # promote
print(evaluate_canary(bad, stable)) # abort (latency)
('promote', 'canary healthy')
('abort', 'p95 900ms > 800ms')
4 · How traffic actually shifts
'Send 10% to the canary' can mean two very different mechanisms, and it matters. The basic way is replica weighting: run 1 canary Pod and 9 stable Pods behind one Service, so roughly 10% of connections hit the canary — crude (tied to replica counts, connection-level not request-level). The precise way needs a component that can split at the request level: an ingress controller or a service mesh that routes an exact percentage.
| Mechanism | Granularity | Needs |
|---|---|---|
| Replica weighting | Coarse — ratio of pod counts, connection-level | Nothing extra (just a Service) |
| Ingress traffic split | Request-level % at the edge | An ingress that supports weighting (NGINX/ALB/Gateway API) |
| Service mesh split | Request-level % anywhere, plus per-header routing | A mesh (Istio/Linkerd) sidecars |
5 · Service mesh: mTLS, traffic policy, observability
A service mesh puts a programmable network layer between your services. Classically it injects a sidecar proxy (Envoy in Istio) next to every Pod; all pod-to-pod traffic flows through the proxies, which the mesh control plane configures. That gives you three things without changing app code:
| Capability | What the mesh does | Why it's hard without one |
|---|---|---|
| mTLS | Auto mutual-TLS + identity between all services (zero-trust) | Every app would implement certs/rotation itself |
| Traffic policy | Request-level splitting, retries, timeouts, circuit-breaking, mirroring | App-level, inconsistent, re-done per service |
| Observability | Uniform golden metrics + traces for every hop, free | Per-service instrumentation (see K8) |
Sidecar vs sidecarless. The sidecar-per-Pod model is powerful but costs a proxy container's CPU/memory on every Pod and adds a network hop. Newer designs reduce this: Linkerd uses a deliberately tiny Rust 'micro-proxy'; Istio ambient mode moves L4 to a per-node component ('ztunnel') and makes L7 proxies optional. The trade-off is always feature richness vs overhead and complexity.
virtualservice.yamlapiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata: { name: llm-api }
spec:
hosts: [ llm-api ]
http:
- route:
- destination: { host: llm-api, subset: stable }
weight: 90
- destination: { host: llm-api, subset: canary }
weight: 10 # exact 10% at the request level
6 · When a mesh is — and isn't — worth it
A mesh is powerful and not free: sidecars add resource overhead and a latency hop, the control plane is another critical system to run and upgrade, and debugging gains a layer. The honest engineering question is whether your problems actually need it.
| A mesh earns its keep when… | …and is likely overkill when… |
|---|---|
| You need mTLS/zero-trust across many services | You have a handful of services and edge TLS is enough |
| You want uniform retries/timeouts/mirroring org-wide | A library or ingress already covers your traffic needs |
| You do frequent, fine-grained canaries | Replica-weighted canaries are good enough |
| You have many teams/languages needing consistent policy | One small team that can just instrument its apps |
✓ Checkpoint — you can move on when you can…
- Contrast rolling, canary and blue-green, and pick the right one for a given fear (blast radius vs mixed versions).
- Write an Argo Rollout with a canary ramp and an analysis gate tied to a Prometheus SLI.
- Explain analysis-driven promotion and code the promote-or-abort decision.
- Distinguish replica-weighted from request-level traffic shifting and what each needs.
- Explain the three things a mesh adds (mTLS, traffic policy, observability) and sidecar vs sidecarless.
- Argue when a mesh is worth its cost — and when it's overkill.
A team runs a rolling update to ship a new model server. It passes health checks and rolls out fully, but users report 15% of answers are garbage — the model was mis-loaded. Health checks were green the whole time. How would canary + analysis have caught this, and why didn't health checks?
Show answer
Your platform team proposes rolling out Istio across all 8 microservices, and the headline justification is 'so we can do canary deployments'. What questions would you push back with?
Show answer
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Picking rolling vs canary vs blue-green is a judgment call you'll make on every risky release.
Your task: For three scenarios, choose a strategy and justify it in one line each.
Requirements:
- Scenario A: user-facing chat API, good metrics, want to limit blast radius
- Scenario B: a change with an incompatible data step; no mixed versions allowed
- Scenario C: internal batch job, low risk, simplest possible
- Justify each choice
💡 Hint: Blast radius → canary; atomic switch → blue-green; simplicity → rolling.
Show solution
A → Canary. Good metrics + user-facing + want small blast radius = send 10% to v2, gate on SLIs, ramp or abort. Only a few users ever see a bad version.
B → Blue-green. If old and new can't coexist, you need an atomic switch: bring v2 up fully, flip 100% at once, flip back instantly on trouble. Accept the double capacity for the window.
C → Rolling. Low-risk internal job doesn't justify the machinery; the default rolling update is simplest and fine. Match the strategy's cost to the release's risk.
Context: Argo Rollouts encodes the canary ramp as data so it runs the same way every time.
Your task: Write an Argo Rollout that ramps 10% → 50% → 100% with pauses, and say what replaces the Deployment.
Requirements:
- Use the canary strategy with setWeight steps
- Add pauses between weights
- Note that a Rollout CRD replaces the Deployment
- Label it as needing a cluster
💡 Hint: setWeight + pause steps; the pod template is the same as a Deployment's.
Show solution
kind: Rollout
spec:
replicas: 6
strategy:
canary:
steps:
- setWeight: 10
- pause: { duration: 5m }
- setWeight: 50
- pause: { duration: 5m }
- setWeight: 100The Rollout CRD replaces the Deployment (same pod template, richer strategy). Between weights the rollout pauses so you (or an analysis gate) can judge before ramping. Needs a cluster; verify the Argo Rollouts CRD vs current docs.
Context: A canary without a metric gate is just a slow rolling update.
Your task: Add an AnalysisTemplate so the rollout auto-aborts if success rate drops, and connect it to K8.
Requirements:
- Define a success-rate metric from Prometheus
- Set a successCondition threshold
- Reference the analysis from a rollout step
- Explain the auto-abort behavior and the K8 link
💡 Hint: successCondition is the SLO; failing it shifts traffic back automatically.
Show solution
kind: AnalysisTemplate
metadata: { name: success-rate }
spec:
metrics:
- name: success-rate
successCondition: result[0] >= 0.99
provider:
prometheus:
query: |
sum(rate(http_requests_total{status!~"5.."}[2m]))
/ sum(rate(http_requests_total[2m]))Reference it from a step: - analysis: { templates: [ { templateName: success-rate } ] }. If the measured success rate dips below 0.99, Rollouts auto-aborts and shifts traffic back to stable. The query is a K8 SLI — no observability, no safe automation. Needs a cluster; verify vs docs.
Context: Understanding the decision as arithmetic lets you reason about false promotes/aborts.
Your task: Write offline Python that decides promote vs abort from canary and stable SLIs, with absolute and relative guards.
Requirements:
- Absolute success-rate floor and latency ceiling
- A relative guard vs the stable version
- Return a decision + reason
- Note the statistical traps (too little traffic, skew)
💡 Hint: Compare like-for-like; require the signal to persist across the pause.
Show solution
def evaluate_canary(canary, stable, min_success=0.99, max_latency_ms=800, latency_tol=1.2):
if canary['success_rate'] < min_success:
return 'abort', 'low success'
if canary['p95_ms'] > max_latency_ms:
return 'abort', 'high latency (absolute)'
if canary['p95_ms'] > stable['p95_ms'] * latency_tol:
return 'abort', 'regression vs stable'
return 'promote', 'healthy'Absolute gates enforce the SLO; the relative gate catches a canary that's within SLO but clearly worse than stable (a regression). Traps: at 1% of low traffic the sample may be too small to tell noise from a real dip — hold the step long enough or weight by requests; and ensure the canary isn't on faster nodes or a skewed traffic slice (Simpson's paradox). Runs offline; no cluster needed.
Context: Coarse replica-weighting can't do 'route exactly 5%' or 'only users with this header' — that needs request-level control.
Your task: Design a canary that shifts an exact percentage at the request level and can target a header, and say what it requires.
Requirements:
- Explain why replica weighting is insufficient here
- Use a mesh (or Gateway API) VirtualService-style weight
- Show header-based routing for internal dogfooding
- Tie the shifting to Argo Rollouts/Flagger
💡 Hint: Weights + match on headers live in the mesh's routing resource.
Show solution
Why replica weighting fails: it's tied to pod-count ratios and is connection-level, so you can't get an exact 5% or route by request attributes.
Request-level with a mesh (Istio):
http:
- match: [ { headers: { x-dogfood: { exact: "true" } } } ]
route: [ { destination: { host: llm-api, subset: canary } } ] # dogfooders -> canary
- route:
- { destination: { host: llm-api, subset: stable }, weight: 95 }
- { destination: { host: llm-api, subset: canary }, weight: 5 }The mesh routes an exact 5% at the request level and sends header-tagged internal users to the canary regardless. Argo Rollouts / Flagger drive the weight changes automatically via this resource. Needs a cluster + mesh; verify the mesh API vs current docs.
Context: Representative scenario: you own delivery for an LLM platform on EKS with growing microservices; leadership wants 'safe, automated releases' and someone is pushing to 'just install Istio everywhere'.
Your task: Produce a design for automated progressive delivery and a reasoned decision on whether/what mesh to adopt, with the failure modes each choice defends against.
Requirements:
- Choose canary/blue-green per service class with automation (Argo Rollouts)
- Analysis gates tied to K8 SLIs with auto-rollback
- A traffic-shifting mechanism proportional to actual needs
- A yes/no/partial mesh decision with justification
- Name the failure modes each choice defends against
💡 Hint: Delivery is a risk/automation problem; the mesh is a capability/cost problem — decide them separately.
Show solution
Delivery: default to canary for user-facing services (chat, RAG query) via Argo Rollouts with analysis gates on K8 SLIs (success rate, p95, error-budget burn) so a bad release auto-aborts at 10% before most users are hit. Use blue-green where versions can't coexist (e.g. an incompatible schema/index migration). Internal/low-risk jobs stay on rolling.
Traffic shifting: size it to need — start with ingress/Gateway API weighting (no mesh) if that covers the canaries; move to mesh-based request-level routing only if header-targeting/mirroring is required.
Mesh decision: partial / deferred. Installing Istio 'everywhere' for canaries alone is overkill — the Gateway API + Rollouts cover splitting. A mesh becomes justified when we also need cluster-wide mTLS/zero-trust and uniform retries/timeouts across many services; then adopt it, and prefer sidecarless/ambient or Linkerd to cut per-Pod overhead. Roll out mesh incrementally, not big-bang.
Failure modes defended: analysis gates + canary → a bad version's blast radius is ~10% and auto-reverted (vs everyone on a rolling update); blue-green → no broken mixed-version state during incompatible changes; right-sized traffic mechanism → no needless latency/overhead; deferred mesh → we don't pay a whole control plane's operational cost for one feature. Verify all Rollouts/mesh specs against current docs — these drift.