03 - Canary and Blue/Green Traffic Splitting
Why this matters
You've now met the same problem — ship a new version safely by shifting a
controlled slice of traffic to it — in three different worlds: ACA revisions
(track 06), Argo Rollouts (track 10), and now Istio. This module makes you
fluent in the Istio mechanism (weighted VirtualService routing) and, just
as importantly, able to say why you'd reach for each of the three. Canary
and blue/green are the highest-value everyday use of the traffic-management
you learned in module 02.
Concepts
Canary vs. blue/green, in Istio terms
Both are just weight schedules over the v1/v2 subsets from module 02:
- Canary: gradually shift weight —
100/0 → 95/5 → 80/20 → 50/50 → 0/100, pausing at each step to watch metrics, so only a small blast radius is exposed to a bad version at any moment. In Istio this is editing theweightfields of aVirtualServicebetween steps. - Blue/green: keep both versions fully deployed but send 100% to one
("blue"), then flip 100% to the other ("green") in a single step, with an
instant flip back if it misbehaves. In Istio this is
100/0then0/100on the same VirtualService — no redeploy, because both subsets are already running (exactly the "old revision is still there" instant-rollback property you saw with ACA revisions in track 06 module 05).
The key Istio property: because weight is a routing decision made by the sidecars, changing it is a config edit that takes effect in seconds, and rollback is just restoring the previous weights — no image pull, no rescheduling.
Mesh weighting vs. replica-ratio weighting (the track 10 tie-in)
Track 10 module 06 built canaries with Argo Rollouts and, without a
traffic provider, used replica-based weighting — approximating "10%" with
pod counts, which it explicitly warned "lies to you" because pod count isn't
traffic share. It also said the fix was a traffic provider such as a service
mesh, and that track 13 would upgrade it to true mesh-based weighting.
This module is that upgrade. With Istio, weight: 10 means 10% of requests
regardless of how many Pods back each subset — one v2 Pod can take exactly 10%
while ten v1 Pods share the other 90%. That precision is the entire reason
Rollouts integrates with a mesh.
Crucially, the two compose rather than compete: Argo Rollouts can drive the Istio VirtualService weights for you, adding automated, metric-gated promotion on top of the precise weighting Istio provides. Istio gives the accurate knob; Rollouts (track 10) turns the knob automatically based on analysis. This module turns the knob by hand so you understand what Rollouts would be automating.
Three mechanisms, one problem — when to use which
You now know three ways to do a weighted rollout. They are not interchangeable; they live at different layers:
| Mechanism | Layer | How you set weight | Reach for it when |
|---|---|---|---|
| ACA revisions (track 06) | Managed platform | az containerapp ingress traffic set |
You're on Azure Container Apps and want zero mesh to operate |
| Argo Rollouts (track 10) | K8s controller / GitOps | setWeight steps in a Rollout, auto-gated by analysis |
You want automated metric-gated promotion/rollback, with or without a mesh |
| Istio VirtualService (this track) | Service mesh data plane | weight in a VirtualService |
You already run a mesh and want precise weighting (and to combine it with mTLS/authz/routing) |
The honest guidance: if you already run a mesh, its weighting is the natural choice and it's precise; if you want the rollout to promote itself based on metrics, that's Argo Rollouts' job (which can drive the mesh); if you're on ACA, the platform already does this and you don't operate a mesh at all. The skill this module builds is recognising which world you're in.
Metric-gating: what the mesh gives you for free
A canary is only useful if you watch the canary's health at each step.
Track 10's Argo Rollouts automated this with AnalysisTemplate queries. Even
by hand, Istio helps: because every request flows through the sidecars, you
get per-subset request rate, error rate, and latency without instrumenting
the app (module 06 is the full treatment). So the manual canary loop is:
bump weight → watch the v2 subset's error rate and latency in the mesh
metrics → promote or roll back. This is the same decision Rollouts' analysis
automates; doing it by hand here teaches you what "good" and "bad" look like.
Command reference
| Field / command | What it does | Notes |
|---|---|---|
spec.http[].route[].weight |
Percentage to each destination in a canary step | Edit between steps to advance the canary |
kubectl apply -f <vs>.yaml |
Applies the next weight step | Takes effect in seconds; the rollout "step" |
kubectl patch virtualservice <n> --type=merge -p '...' |
Adjust weights without re-applying the whole file | Handy for quick step changes |
istioctl proxy-config routes <pod> |
Confirm the weights the proxy actually holds | Ground truth per step |
kubectl rollout undo |
Not how you roll back an Istio canary | Rollback is restoring previous VirtualService weights, not a Deployment undo |
watch -n1 'kubectl exec ... curl ...' |
Sample the live split during a step | Quick, crude health/ratio check |
Hands-on exercises
Continue in mesh-demo. You should have backend-v1 and backend-v2
Deployments, the backend Service, and the backend DestinationRule with
v1/v2 subsets from module 02.
1. Start from a clean 100/0 (all on v1)
# canary-step0.yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: backend
namespace: mesh-demo
spec:
hosts: [backend]
http:
- route:
- destination: {host: backend, subset: v1}
weight: 100
- destination: {host: backend, subset: v2}
weight: 0
kubectl apply -f canary-step0.yaml
for i in $(seq 1 20); do kubectl exec -n mesh-demo deploy/frontend -- curl -s http://backend; echo; done | sort | uniq -c
Expected: 20 backend v1, 0 v2 — the stable starting point of a canary.
2. Step the canary to 5%
kubectl patch virtualservice backend -n mesh-demo --type=merge -p \
'{"spec":{"http":[{"route":[{"destination":{"host":"backend","subset":"v1"},"weight":95},{"destination":{"host":"backend","subset":"v2"},"weight":5}]}]}}'
for i in $(seq 1 40); do kubectl exec -n mesh-demo deploy/frontend -- curl -s http://backend; echo; done | sort | uniq -c
Expected: roughly 38 v1 / 2 v2 — about 5% on the canary. Note this is a
precise 5%, not "one pod out of twenty," even though both subsets have one
Pod. This is the difference from replica-ratio weighting.
3. Progress through the canary
Repeat the patch with weights 80/20, then 50/50, sampling 40 requests
after each and confirming the ratio tracks the weights. At each step you would,
in production, be watching the v2 subset's error rate/latency (module 06)
before advancing.
# 20%:
kubectl patch virtualservice backend -n mesh-demo --type=merge -p \
'{"spec":{"http":[{"route":[{"destination":{"host":"backend","subset":"v1"},"weight":80},{"destination":{"host":"backend","subset":"v2"},"weight":20}]}]}}'
Expected: sampled ratios follow 80/20 then 50/50 as you step. This is the
manual version of the setWeight steps a Rollout walks automatically.
4. Complete the canary: 0/100
kubectl patch virtualservice backend -n mesh-demo --type=merge -p \
'{"spec":{"http":[{"route":[{"destination":{"host":"backend","subset":"v1"},"weight":0},{"destination":{"host":"backend","subset":"v2"},"weight":100}]}]}}'
for i in $(seq 1 20); do kubectl exec -n mesh-demo deploy/frontend -- curl -s http://backend; echo; done | sort | uniq -c
Expected: 20 v2, 0 v1 — v2 is now stable. The canary is complete; v1 can
be retired.
5. Instant rollback (the payoff)
Imagine v2 misbehaves after full promotion. Roll back with a single weight restore — no redeploy, because v1 is still running:
kubectl apply -f canary-step0.yaml
for i in $(seq 1 20); do kubectl exec -n mesh-demo deploy/frontend -- curl -s http://backend; echo; done | sort | uniq -c
Expected: back to 100% v1 in seconds. This is the same instant-rollback
property as ACA revisions (track 06 module 05) — the old version never left,
so rollback is a routing change, not a deployment.
6. Blue/green flip
Blue/green is the degenerate canary: no intermediate steps, just flip. With
canary-step0.yaml (blue=v1 at 100%) applied, flip green in one shot:
kubectl patch virtualservice backend -n mesh-demo --type=merge -p \
'{"spec":{"http":[{"route":[{"destination":{"host":"backend","subset":"v1"},"weight":0},{"destination":{"host":"backend","subset":"v2"},"weight":100}]}]}}'
Expected: 100% cut to v2 instantly, with an instant flip back available. The difference from canary is purely the schedule (one big step vs. many small ones), not the mechanism.
7. Header-pinned "canary for me only"
A subtler pattern: keep 100% of public traffic on v1, but let you reach v2 by header — a canary you can dogfood before exposing anyone. Combine module 02's header match with the weighted default:
# canary-dogfood.yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: backend
namespace: mesh-demo
spec:
hosts: [backend]
http:
- match:
- headers:
x-canary: {exact: "true"}
route:
- destination: {host: backend, subset: v2}
- route:
- destination: {host: backend, subset: v1}
weight: 100
- destination: {host: backend, subset: v2}
weight: 0
kubectl apply -f canary-dogfood.yaml
kubectl exec -n mesh-demo deploy/frontend -- curl -s http://backend
kubectl exec -n mesh-demo deploy/frontend -- curl -s -H "x-canary: true" http://backend
Expected: default request → v1; x-canary: true → v2. You can validate
v2 in production with zero public exposure — something replica-ratio canaries
can't do at all. (Remember module 02's lesson: the header match must come
before the weighted default.)
8. Diagnose and fix: weights that send everyone to the wrong version
A colleague "adjusted the canary to 10%" but everyone's on v2. Reproduce:
# canary-broken.yaml
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
name: backend
namespace: mesh-demo
spec:
hosts: [backend]
http:
- route:
- destination: {host: backend, subset: v1}
weight: 10
- destination: {host: backend, subset: v2}
weight: 90
kubectl apply -f canary-broken.yaml
for i in $(seq 1 20); do kubectl exec -n mesh-demo deploy/frontend -- curl -s http://backend; echo; done | sort | uniq -c
Expected: ~18 v2 / 2 v1 — the weights are inverted. The intent was "10%
to the new v2," but the weights read "10% to v1, 90% to v2," so the canary is
actually a near-full rollout. This is the weighted-routing analogue of module
02's ordering typo — it applies cleanly and fails silently by routing the
wrong ratio. Diagnose by reading the actual weights:
kubectl get virtualservice backend -n mesh-demo -o yaml | grep -A2 weight
Expected: you see subset: v1 weight: 10 and subset: v2 weight: 90 — the
numbers are on the wrong subsets. Fix by restoring the correct 90/10 split:
kubectl patch virtualservice backend -n mesh-demo --type=merge -p \
'{"spec":{"http":[{"route":[{"destination":{"host":"backend","subset":"v1"},"weight":90},{"destination":{"host":"backend","subset":"v2"},"weight":10}]}]}}'
for i in $(seq 1 20); do kubectl exec -n mesh-demo deploy/frontend -- curl -s http://backend; echo; done | sort | uniq -c
Expected: ~18 v1 / 2 v2 — a real 10% canary. Lesson: always confirm which
subset each weight is attached to, not just that the numbers sum to 100.
9. Reset for the next module
kubectl apply -f canary-step0.yaml
Expected: 100% v1. Leave the app and DestinationRule in place — module 04 adds mTLS on top.
Independent challenge
No step-by-step given — draw on this module, track 10 module 06, and track 06 module 05.
Task: Run a full five-step canary (100/0 → 95/5 → 75/25 → 50/50 → 0/100) of a payments service from v1 to v2, sampling the live split at each
step to confirm it tracks the weights, and practise a mid-canary abort:
partway through (say at 50/50), pretend v2's error rate spiked and roll all
the way back to 100/0 in one action, then explain why that rollback needed
no redeploy. Finally, write three sentences contrasting how you'd have done
this same rollout with Argo Rollouts (track 10) and with ACA revisions
(track 06), naming the one thing each mechanism does that the other two don't.
Stuck? One hint
The rollback is just re-applying the step-0 VirtualService (or a patch back to
100/0) — the whole point is that v1 never stopped running, so it's a routing
change, not a deploy. For the contrast: Argo Rollouts' distinguishing feature
is automated metric-gated promotion/rollback; ACA revisions' is zero mesh
to operate on a managed platform; Istio's is precise weighting composable
with mTLS/authz on a mesh you already run.
Common mistakes & troubleshooting
- Inverting the weights. Attaching the small weight to the wrong subset turns a 10% canary into a 90% rollout (exercise 8). Always check which subset each weight sits on, not just that they sum to 100.
- Thinking rollback needs a redeploy. With both subsets running, rollback
is restoring the previous VirtualService weights — seconds, no image pull.
Reaching for
kubectl rollout undohere is the wrong tool. - Canary weight without health-watching. Stepping weights without watching the canary subset's error rate/latency (module 06) is just a slow blind rollout — the point of a canary is the gate, which by hand you supply.
- Header canary rule ordered after the weighted default. Same first-match hazard as module 02 — the header match must come before the catch-all or it never runs.
- Confusing the three mechanisms' layers. Istio weighting, Argo Rollouts, and ACA revisions solve the same problem at different layers; using Istio's weights doesn't give you Rollouts' automated gating, and vice versa — combine them if you want both.
- Assuming precise weight means precise for tiny sample sizes.
weight: 5is 5% over many requests; a handful ofcurls can look off just from randomness — sample enough to see the ratio.
Checkpoint quiz
Write down your answer to each question before expanding it — checking without attempting first is the single easiest way to fool yourself into thinking you've learned this.
- In Istio terms, what is the only difference between a canary and a blue/green rollout?
- Why is rolling back an Istio canary instant and redeploy-free?
- Track 10's Argo Rollouts used replica-based weighting by default. What does Istio weighting give you that replica ratios can't, and how do the two tools combine rather than compete?
- You have three ways to do a weighted rollout (ACA revisions, Argo Rollouts, Istio). Give the one deciding factor that would send you to each.
- What does a mesh give you "for free" that makes metric-gating a canary possible without touching the app?
- In exercise 8, the weights summed to 100 and the YAML applied cleanly, yet the canary was wrong. What was the bug, and why didn't anything error?
- What is the header-pinned "dogfood" canary pattern, and what can it do that a purely weighted canary cannot?
Show answers
- The weight schedule: canary shifts weight gradually through intermediate steps with health checks between them; blue/green flips 100% in a single step. Same VirtualService mechanism, different step plan.
- Both subsets stay fully deployed the whole time, so rollback is just
restoring the previous
weightvalues in the VirtualService — a routing change the sidecars apply in seconds, with no image pull or rescheduling. - Istio sets a real traffic percentage independent of pod count (one canary Pod can take exactly 5%), whereas replica ratios conflate pods with traffic and are coarse. They combine: Argo Rollouts can drive the Istio VirtualService weights, adding automated metric-gated promotion on top of Istio's precise knob.
- ACA revisions: you're on Azure Container Apps and don't want to operate a mesh. Argo Rollouts: you want automated, metric-gated promotion/rollback. Istio: you already run a mesh and want precise weighting composable with mTLS/authz/routing.
- Per-request golden-signal metrics (request rate, error rate, latency) for each subset, because every request passes through the sidecars — so you can judge the canary's health with no app instrumentation.
- The weights were attached to the wrong subsets (10% to v1, 90% to v2 instead of the reverse), so a "10% canary" was actually a 90% rollout. The API only validates schema, not intent — the object was structurally valid, so it applied silently, exactly like the ordering typo in module 02.
- A VirtualService that routes requests with a chosen header (e.g.
x-canary: true) to v2 while keeping 100% of default traffic on v1. It lets you validate the new version in production with zero public exposure — something a purely weighted canary, which always exposes some real users, cannot do.
Cumulative review
Closed-book. Don't reopen earlier modules while attempting these — the point is to find out what actually stuck, across this track and the tracks it builds on.
- A meshed Pod is
2/2and appears inistioctl proxy-status, but aVirtualServicerouting 100% tosubset: v2sends everyone to v1 anyway. Give the two most likely causes (one from module 01, one from module 02) and how you'd tell them apart. - Explain, using the data-plane/control-plane split from module 00, why
editing a
VirtualServiceweight takes effect in seconds and why traffic keeps flowing even ifistiodis briefly down. - A
Serviceselector typo (track 03 module 04), a NetworkPolicy label typo (track 03 module 11), a missing injection label (module 01), and an inverted canary weight (module 03) are all the "same class" of bug. State the class in one sentence and what all four share at the moment of failure. - You need "5% of traffic to v2." Describe how you'd achieve it with (a) a
plain
Service(track 03), (b) Argo Rollouts without a mesh (track 10), and (c) Istio — and rank the three by how truthfully each delivers exactly 5%. - A DestinationRule defines subsets
v1/v2, but a Pod meant to bev2is labelledversion: V2(capital V). What breaks, what doesn't error, and which command surfaces it? - Blue/green in ACA revisions (track 06) and blue/green in Istio both give "instant rollback." Explain the shared reason rollback is instant in both, despite the completely different underlying platforms.
- Why does a canary without watching per-subset metrics reduce to "a slow blind rollout," and which module gives you the metrics that make it a real canary — with no app code change?
- You want only your own requests to hit the new version while 100% of public traffic stays on the old one. Which two module-02/03 primitives do you combine, in which order, and why does the order matter?
- A teammate says "just use
kubectl rollout undoto roll back the canary." Explain why that's the wrong tool for an Istio weighted canary and what the correct rollback actually is. - Map each of these to the right layer:
Service,VirtualService,DestinationRule, NetworkPolicy, injection label. For each, one sentence on what stops working if it's misconfigured.
Show answers
- (module 02) The VirtualService routes are mis-ordered or the subset doesn't
match — e.g. a catch-all above the intended route, or the
v2subset's label doesn't match the Pods; (module 01) the client Pod isn't actually meshed (1/1, absent fromproxy-status) so its sidecar applies no routing. Tell them apart: confirm the caller is2/2and inproxy-status(rules out module 01), thenistioctl proxy-config routeson the caller to see the actual programmed routes (finds the module-02 issue). - You write the weight to the control plane (a CRD
istiodwatches);istiodcomputes and pushes new Envoy config to the sidecars (data plane) within seconds. Traffic is carried by the sidecars using their last pushed config, so ifistiodis briefly down, existing traffic keeps flowing — only new config changes stall until it returns. - The class: a label/selector/weight mismatch that is schema-valid, so it applies with no error but silently does the wrong thing. All four share that at failure time nothing errors — the object was accepted — and the symptom is "traffic goes nowhere / the wrong place," found only by comparing the configured selector/weight against reality.
- (a) You can't set 5% with a
Service— it splits evenly across endpoints; the closest is a replica ratio, which is coarse. (b) Argo Rollouts without a mesh approximates 5% with pod counts — also coarse. (c) Istio setsweight: 5as a true 5% of requests. Truthfulness ranking: Istio (exact) > Rollouts/replica (approximate) ≈ Service/replica (approximate); the two replica-based ones can't be exactly 5% unless pod counts happen to allow it. - The
v2subset selectsversion: v2but the Pod carriesversion: V2, so the Pod belongs to no subset and any route tov2has no endpoints — calls tov2fail, but nothing errors at apply time because labels are free-form strings.istioctl analyze(or checkingget pods --show-labelsagainst the subset) surfaces it; same selector-mismatch class as track 03. - In both, the old version stays fully deployed and running the whole time,
so "rollback" is just redirecting 100% of traffic back to it — a routing
change (weights in Istio,
ingress traffic setin ACA), not a redeploy or image pull — which is why it's near-instant on both platforms. - Because the whole value of a canary is the decision gate at each step; without watching the canary's error rate/latency you just advance on a timer, exposing more users to a possibly-bad version blindly. Module 06 (mesh observability) supplies per-subset golden-signal metrics with no app change, because every request crosses the sidecars.
- A header
matchroute to the new subset placed before a weighted default route (100% old). Order matters because VirtualService routes are first-match-wins — if the weighted default came first it would catch every request and the header rule would never run. kubectl rollout undoreverts a Deployment's pod template to a previous revision; an Istio canary doesn't change the Deployment at all — both versions stay deployed and only the VirtualService weights change. Correct rollback: restore the previous VirtualService weights (e.g. back to100/0).Service— discovery/endpoints; misconfig (selector typo) → no endpoints, routes nowhere.VirtualService— per-request routing; misconfig (ordering/weights) → traffic to the wrong version.DestinationRule— subset/policy definitions; misconfig (bad labels/missing subset) → routes have no valid target. NetworkPolicy — L3/4 allow/deny; misconfig → legit traffic blocked or everything open. Injection label — mesh membership; missing → Pod runs unmeshed and all mesh config silently ignores it.
Next
04-mtls-and-zero-trust — everything you've routed so far has already been encrypted with mTLS by default; now make that explicit, enforce it STRICTly, and see how it layers with the NetworkPolicy you know from track 03.