AI EngineeringZero to ProductionHome·About·Contact
Kubernetes Orchestration · Part 10

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.

⏱️ ~110 min🚦 Delivery & mesh🎯 Advanced→Production
🌱 Honesty up frontAll YAML/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.

StrategyHow it exposes the new versionRollbackCost / caveat
RollingReplace replicas gradually; all users mix old+newRoll back the DeploymentNo metric gate; bad version reaches everyone
CanarySend a small % of traffic to v2, watch, then rampShift traffic back to 0%Needs traffic-splitting + metric analysis
Blue-greenRun v2 fully in parallel; flip 100% at onceFlip back instantlyDouble the resources during the window
Deploy v2 (0%) stable=v1 Shift 10% canary=v2 Analyze SLIs error/latency Ramp or abort promote / rollback
Canary = risk control; blue-green = instant switch/rollbackPick by what you fear. Canary limits blast radius — only a few users see a bad v2 before the gate catches it; ideal for user-facing services with good metrics. Blue-green gives an instant, atomic switch and instant rollback (just flip the router), at the cost of running two full stacks; ideal when you can't have mixed versions live (e.g. an incompatible schema step) and can afford the double capacity.

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:

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
yaml · an Argo Rollout with a canary ramp and analysis gates (needs a cluster; verify CRD vs docs)
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
yaml · an AnalysisTemplate that promotes only if success rate stays high (verify vs docs)
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]))
The analysis query ties back to K8's SLOsProgressive delivery is only as good as its metric gate. That Prometheus query is a K8 SLI — success rate, p95 latency, error-budget burn. If the canary's SLIs stay healthy, Rollouts promotes; if they degrade, it auto-aborts and shifts traffic back to the stable version, usually before a human notices. This is why K8 (observability) comes before K10: no metrics, no safe automation. Verify Rollouts CRD fields vs docs.

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:

python · the promote-or-abort decision, offline-runnable (no cluster needed)
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')
Give the canary enough traffic to judge, and beware Simpson's paradoxTwo statistical traps. (1) A canary at 1% of low traffic may see too few requests to distinguish a real regression from noise — hold each step long enough (or weight by request count, not just time). (2) If the canary lands on different (e.g. faster) nodes, or serves a skewed traffic slice, its metrics can mislead. Compare like-for-like and require the signal to persist across the pause window before promoting.

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.

MechanismGranularityNeeds
Replica weightingCoarse — ratio of pod counts, connection-levelNothing extra (just a Service)
Ingress traffic splitRequest-level % at the edgeAn ingress that supports weighting (NGINX/ALB/Gateway API)
Service mesh splitRequest-level % anywhere, plus per-header routingA mesh (Istio/Linkerd) sidecars
This is why mesh + progressive delivery are the same lessonFine-grained canaries (route exactly 5%, or route only users with a header, and mirror traffic) need request-level traffic control — which is exactly what a service mesh (or Gateway API) provides. Argo Rollouts and Flagger integrate with these to do the shifting. So progressive delivery can work with just replica weighting, but the powerful version rides on a mesh — hence they belong together.

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:

Service A app code sidecar proxy mTLS out sidecar proxy mTLS in Service B app code
CapabilityWhat the mesh doesWhy it's hard without one
mTLSAuto mutual-TLS + identity between all services (zero-trust)Every app would implement certs/rotation itself
Traffic policyRequest-level splitting, retries, timeouts, circuit-breaking, mirroringApp-level, inconsistent, re-done per service
ObservabilityUniform golden metrics + traces for every hop, freePer-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.

yaml · Istio: request-level traffic split for a canary (needs a cluster + mesh; verify vs docs)
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
mTLS is the security payoff, not just trafficBeyond canaries, the biggest reason teams adopt a mesh is automatic mTLS: every service gets a workload identity and all in-cluster traffic is mutually authenticated and encrypted, with cert rotation handled for you. Combined with authorization policies ('service A may call B, nobody else'), that's real zero-trust networking — complementary to the NetworkPolicy of K9. Verify Istio/Linkerd APIs vs current docs.

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 servicesYou have a handful of services and edge TLS is enough
You want uniform retries/timeouts/mirroring org-wideA library or ingress already covers your traffic needs
You do frequent, fine-grained canariesReplica-weighted canaries are good enough
You have many teams/languages needing consistent policyOne small team that can just instrument its apps
Don't adopt a mesh for one feature you can get cheaperThe classic mistake is installing a full mesh to get one capability — say, canary traffic splitting — that the Gateway API or your ingress already provides, or retries you could add in a client library. A mesh pays off when you need several of its capabilities across many services (especially mTLS at scale). If you need one thing for a few services, buy that one thing. Start without a mesh; adopt it when the pain is real and broad. Verify current mesh overhead/benchmarks against their docs.

✓ 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.
✓ Knowledge check

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
Health checks (liveness/readiness) only prove the process is up and accepting traffic — they say nothing about answer quality. A canary with an analysis gate would have sent, say, 10% of traffic to v2 and measured a real SLI (error rate, a quality/eval signal, or a downstream success metric); seeing it degrade, the controller would auto-abort and shift traffic back to stable before most users were affected. The lesson: canaries let you gate on outcomes users feel, not just process liveness — so choose an analysis metric that actually reflects quality, not just HTTP 200s.
✓ Knowledge check

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
Canary splitting alone doesn't justify a full mesh — the Gateway API or your ingress (or Argo Rollouts with a supported provider) can do traffic weighting without the per-Pod sidecar overhead, extra latency hop, and a whole control plane to operate and upgrade. Push back with: do we also need cluster-wide mTLS/zero-trust? Uniform retries/timeouts/circuit-breaking across many languages? Rich per-hop observability we can't get otherwise? If we need several of those across many services, a mesh earns its cost. If we only need canaries, adopt the cheaper traffic-splitting mechanism and skip the mesh — or start with sidecarless/ambient to cut overhead.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Choose the deployment strategyBeginner

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.

Exercise 2 · Write a canary Rollout with stepsIntermediate

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: 100

The 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.

Exercise 3 · Add an analysis gate tied to an SLIAdvanced

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.

Exercise 4 · Model the promote-or-abort decisionExpert

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.

Exercise 5 · Design request-level canaries with a meshProfessional

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.

Exercise 6 · Progressive delivery + mesh decision for a platformIndustry scenario

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.

© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in