Argo Rollouts
A plain Kubernetes Deployment only knows one trick during a rolling update: replace pods a few at a time and check that each new one passes its readiness probe. It has no concept of a traffic percentage, no way to pause for a human to check a dashboard, and no reverse gear — a version that starts cleanly and then quietly returns bad responses to a third of requests sails straight through to 100%. Argo Rollouts is the CNCF-graduated controller, one of the four projects the CAPA exam covers, that fills exactly that gap. A Rollout resource stands in for a Deployment and adds a strategy: canary, ramping traffic up in authored steps with pauses and metric checks between them, or blue-green, running the new version fully live behind a preview service before flipping real users onto it. At every gate, an AnalysisRun can query Prometheus and abort the release on its own — and this page covers both strategies, the resources you write, the commands you drive them with, and what an abort actually leaves behind.
Picture a theme park opening a brand-new roller coaster. Nobody sends every visitor on it at once — first a handful of test riders go, while staff in the control booth watch a camera feed and a heart-rate monitor before deciding anything. If the test riders come off grinning, a few more people get let on, then a few more, until eventually everyone's riding the new coaster and the old one gets retired for good. But if a test rider goes green partway through the loop, the staff don't wait around to see if it passes — they stop letting anyone new on immediately and send everyone straight back to the old, boring, reliable coaster. Argo Rollouts is that control booth: it decides how many riders go first, watches the readings between each batch, and pulls the plug the instant the readings look wrong.
Rollout manifests this course keeps coming back to.The gap a Deployment leaves open
☺ Like you're 10: A Deployment can only ask "did the new pod start okay?" — it can never ask "is the new pod actually any good?"
A Deployment running strategy.type: RollingUpdate gives you exactly two dials, maxSurge and maxUnavailable, and both only control the pace pods get swapped at — never whether the thing being swapped in is trustworthy. Its one health signal is the readiness probe, which a version can pass perfectly while still returning HTTP 500 to a slice of real traffic or taking ten times longer to respond than it should. There is no built-in way to say "hold at 10% for five minutes" or "only continue if the error rate stays under 1%," and no way to move traffic independently of pod count at all.
Argo Rollouts closes that gap by swapping kind: Deployment for kind: Rollout. Everything familiar carries over unchanged — replicas, selector, the entire pod template — and what's new is that spec.strategy now takes a canary or blueGreen block instead of a plain rollingUpdate one. Underneath, the controller still manages ReplicaSets the way a Deployment would; it just refuses to walk all the way to 100% without stopping where you told it to. If rewriting every Deployment as a Rollout feels like too much up front, spec.workloadRef lets a Rollout point at an existing Deployment and borrow its pod template instead of owning one directly — the Deployment stays exactly as your Helm chart or Kustomize base renders it, and the Rollout is a thin wrapper the platform adds on top.
Argo Rollouts exists to split deploy (the new version is running somewhere in the cluster) from release (real users are actually being sent to it) and to put a measurement in the gap between them. The unit of progress stops being "pods replaced" and becomes "percentage of traffic that survived an analysis" — which is also exactly why GitOps Philosophy treats a Rollout's changing replica counts mid-release as something a reconciler's selfHeal must be told to leave alone, not drift to correct.
Two strategies: canary and blue-green
☺ Like you're 10: Canary lets a handful of visitors try the new ride first while everyone else stays on the old one; blue-green builds the whole new ride fully working next door, then just moves the queue over.
A canary strategy is an authored list of steps the controller walks in order: setWeight: N sends N% of live traffic to the canary; pause: { duration: 5m } holds for a fixed soak, while a bare pause: {} holds indefinitely until a human runs promote; analysis runs a template inline and blocks that step until it passes; and setCanaryScale decouples pod count from traffic weight, useful when 5% of traffic needs more than one canary pod to produce a statistically meaningful signal. Real traffic splitting needs two named services — canaryService and stableService — plus a trafficRouting block naming the router that will actually honor the weight; without either, setWeight is only approximated by the ratio of canary to stable pod counts.
A blue-green strategy skips traffic weights entirely and works by re-pointing service selectors instead. You name an activeService — what users hit — and, usually, a previewService that smoke tests can hit while the new version has zero real traffic. autoPromotionEnabled then decides what happens once the new pods are ready: left at its default of true, the active selector flips the instant they pass readiness, which is a slower Deployment wearing a disguise, not a gate. Set it to false and promotion waits for an explicit human or automated promote. prePromotionAnalysis runs against the preview service before the flip; postPromotionAnalysis runs against the newly-active service after it; and scaleDownDelaySeconds (default 30) keeps the old ReplicaSet alive for a window afterward, so a rollback is a selector flip, not a cold start.
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: mission-api
namespace: mission
spec:
replicas: 10
revisionHistoryLimit: 3
selector:
matchLabels: { app: mission-api }
template: # identical to a Deployment's pod template
metadata:
labels: { app: mission-api }
spec:
containers:
- name: mission-api
image: registry.kubestronaut.dev/mission-api:1.4.3
readinessProbe:
httpGet: { path: /healthz, port: 8080 }
strategy:
canary:
canaryService: mission-api-canary # selectors the controller manages for you
stableService: mission-api-stable
trafficRouting:
istio:
virtualService:
name: mission-api-vsvc # you create this; the controller only edits weights
routes: [ primary ]
analysis: # BACKGROUND — runs for the whole rollout
templates:
- templateName: success-rate
clusterScope: true
startingStep: 1
args:
- name: service-name
value: mission-api-canary.mission.svc.cluster.local
steps:
- setWeight: 10
- pause: { duration: 5m } # timed soak
- analysis: # INLINE — blocks this one step
templates:
- templateName: latency-p99
args:
- name: service-name
value: mission-api-canary.mission.svc.cluster.local
- setWeight: 50
- pause: {} # indefinite — waits for a human `promote`Note the pattern in that last step: after setWeight: 50 the rollout parks itself until someone promotes it past the final gate, and only after that does it move to 100% and retire the old ReplicaSet. Nothing about that pause is passive — the background analysis declared above the steps is still watching the whole time.
AnalysisTemplate vs. AnalysisRun: recipe and dish
☺ Like you're 10: The template is the school nurse's checklist for what "healthy" means; the AnalysisRun is one actual check-up, done once, with real numbers in it.
An AnalysisTemplate — or a ClusterAnalysisTemplate, the same thing published once by a platform team for every namespace to reference — declares one or more metrics. Each names a provider (Prometheus is the common one, but also Datadog, CloudWatch, a generic web call to any URL returning JSON, or a job that runs an arbitrary Kubernetes Job and reads its exit code), an interval and count of measurements to take, and a successCondition evaluated against the result. failureLimit is how many failed measurements the run will tolerate before it gives up — it defaults to 0, so a template with no explicit failureLimit fails the whole run on the very first bad reading. An AnalysisRun is the instance the controller creates the moment a rollout reaches an analysis step: it carries its own live status, and reading it with kubectl describe analysisrun is how you find out exactly which measurement vetoed a release.
apiVersion: argoproj.io/v1alpha1
kind: ClusterAnalysisTemplate # published once, referenced by any namespace
metadata:
name: success-rate
spec:
args:
- name: service-name # supplied by whichever Rollout uses this
metrics:
- name: success-rate
interval: 60s
count: 5 # take 5 measurements, then stop
initialDelay: 60s # let the canary warm up before judging it
successCondition: result[0] >= 0.99
failureLimit: 2 # TOLERATE 2 bad readings; the 3rd fails the run
provider:
prometheus:
address: http://prometheus.monitoring.svc.cluster.local:9090
query: |
sum(rate(http_requests_total{
service="{{args.service-name}}", code!~"5.."}[2m]))
/
sum(rate(http_requests_total{
service="{{args.service-name}}"}[2m]))Two details cost people marks and outages equally. A template's declared args carry no value until a caller supplies one — either the referencing Rollout, or a default written directly on the arg — so a template applied on its own with a missing arg fails immediately, not silently. And referencing a cluster-scoped template needs clusterScope: true on that reference; leave it off and the controller goes looking for a namespaced AnalysisTemplate of the same name instead and fails the run when it isn't there. Analysis also comes in two flavors worth telling apart: inline (an analysis entry inside steps) blocks only that one step, while background (declared at strategy.canary.analysis, outside steps) starts the moment the canary gets any traffic and keeps running for the whole rollout — so a regression that only shows up at 50% still gets caught, not just the one the step-level gate happened to check.
Traffic routers: who actually moves the packets
☺ Like you're 10: Argo Rollouts decides the percentage on a whiteboard — it never touches a single packet itself, a router does that part.
Every "weight" in the manifests above is a number the controller writes somewhere else; something has to read that number and actually steer traffic. That's a traffic router, and Argo Rollouts is deliberately agnostic about which one you use.
| Router | What the Rollout writes | Notes |
|---|---|---|
| Istio | Weight fields on an existing VirtualService's named route | You create the VirtualService once; the controller only ever edits the weights inside it — see Service Mesh Architecture. |
| NGINX Ingress | A second, controller-managed canary Ingress alongside your stableIngress | You point at the stable Ingress; Rollouts creates and owns the canary one entirely. |
| AWS ALB | Weighted target groups behind a shared listener rule | Common on EKS platforms already standardized on the AWS load balancer controller. |
| Gateway API | HTTPRoute backend weights | Ships as a separate Argo Rollouts Gateway API plugin, not built into the core controller. |
With no trafficRouting block configured at all, setWeight: 10 is only approximated by the ratio of canary to stable pods — with 10 total replicas that's honest enough, but with 3 replicas a "10%" canary is really running at 33%, and whichever pod a given connection lands on is sticky for its lifetime rather than genuinely probabilistic. Blue-green needs none of this: because it re-points service selectors instead of splitting a live percentage, no router is required at all, which is one reason teams without a mesh or an ingress controller sophisticated enough for weighted routing often reach for blue-green first.
Aborting a bad rollout
☺ Like you're 10: Hitting the stop button sends everyone back to the old ride immediately — but it doesn't fix the broken ride, and someone still has to notice and deal with it.
When an analysis fails, or a human runs kubectl argo rollouts abort mission-api -n mission, the effect is immediate and safe: traffic snaps straight back to the stable ReplicaSet, and the canary (or preview) ReplicaSet scales down. What happens next is the part that catches people off guard. The Rollout object does not revert to its old spec, does not retry on its own, and does not quietly disappear — it keeps holding the new, still-broken spec and parks itself in a Degraded status with status.abort: true. Nothing about that state resolves itself. Getting a Rollout out of it takes one of three deliberate moves: kubectl argo rollouts retry rollout mission-api -n mission to clear the abort and re-run the same steps, a genuinely fixed image pushed as a new revision, or kubectl argo rollouts undo mission-api -n mission --to-revision=N to fall back to a known-good one.
An Argo CD Application's sync status only asks "does the live spec match Git?" — and the Rollout object's spec still matches whatever Git says, abort or not, because the abort is recorded in status, not spec. That means the Application can sit perfectly Synced for days while the Rollout underneath it is Degraded and production traffic never left the old version. Alert on Rollout health, not Application sync status, and see GitOps Philosophy for why sync and health are separate questions on every GitOps-managed resource, not just this one.
One command deserves a second look before you reach for it under pressure: kubectl argo rollouts promote --full skips every remaining step and every remaining analysis, jumping straight to 100%. It's a legitimate incident tool for the moment you already know the fix and just need it live — but scripted into a pipeline, it quietly turns a Rollout back into a plain Deployment with extra YAML around it.
Day-to-day commands
☺ Like you're 10: Once the extra plugin is installed, nearly everything you'll do is watch, promote, or abort.
# the controller + its CRDs
$ kubectl create namespace argo-rollouts
$ kubectl apply -n argo-rollouts \
-f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
# the kubectl plugin — a SEPARATE download, not bundled with kubectl or the controller
$ curl -fSL -o kubectl-argo-rollouts \
https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
$ chmod +x kubectl-argo-rollouts && sudo mv kubectl-argo-rollouts /usr/local/bin/
$ kubectl argo rollouts get rollout mission-api -n mission --watch # the command you'll live in
$ kubectl argo rollouts list rollouts -n mission
$ kubectl argo rollouts status mission-api -n mission # blocks until Healthy/Degraded — CI-friendly
$ kubectl argo rollouts set image mission-api mission-api=registry.kubestronaut.dev/mission-api:1.4.4 -n mission
$ kubectl argo rollouts promote mission-api -n mission # step past the current pause
$ kubectl argo rollouts promote mission-api -n mission --full # SKIP remaining steps + analysis
$ kubectl argo rollouts pause mission-api -n mission # freeze right where it is
$ kubectl argo rollouts abort mission-api -n mission # traffic back to stable, now
$ kubectl argo rollouts retry rollout mission-api -n mission # clear the abort, re-run the steps
$ kubectl argo rollouts undo mission-api -n mission --to-revision=3
$ kubectl argo rollouts dashboard # local UI on :3100, can promote/abort tooEvery one of those is a thin wrapper over the Kubernetes API, so plain kubectl always works when a lab box doesn't have the plugin installed — kubectl get rollout mission-api -o wide, kubectl describe rollout mission-api for the events explaining why it paused, and kubectl get analysisrun -n mission to see who vetoed a release. Abort itself is just a boolean on the object's status subresource, so even that survives without the plugin: kubectl patch rollout mission-api -n mission --type merge --subresource status -p '{"status":{"abort":true}}'. Promoting past a step-level pause is the one operation without a tidy fallback — the controller tracks it in status.pauseConditions, which the plugin clears for you — so the plugin is worth installing for that alone.
Gotchas worth knowing before they bite
☺ Like you're 10: Three more surprises, besides the abort trap above, that catch almost everyone exactly once.
The very first rollout skips the steps entirely
Create a brand-new Rollout and it goes straight to 100% on its first apply, because there is no stable version yet to canary against. And only a change to the pod template starts a new revision and re-runs the strategy — editing replicas alone does not. Engineers testing a step recipe for the first time often watch that initial deploy sail through and conclude the steps are broken, when really they were never invoked.
A canary with no analysis is just a slower bad deploy
setWeight: 10; pause: { duration: 5m }; setWeight: 100 looks careful and accomplishes almost nothing — it ships the identical bug to everyone, five minutes later, while giving the team a false sense of rigor. A canary only earns its complexity once something actually looks at the numbers and can say no; an indefinite pause: {} with a human watching a dashboard is honest, a timed pause with nobody watching is theater.
Two controllers can fight over the same fields
Anything Argo Rollouts generates and owns — a managed canary Ingress, service selectors it flips, a VirtualService's weights — has to be excluded from your GitOps reconciler's own diffing, via Argo CD's ignoreDifferences or the Flux equivalent. Skip that and selfHeal reverts the weight the Rollout just set, the Rollout sets it right back, and you get a flapping loop that looks like a networking bug and is really a governance one — see GitOps Philosophy for the general shape of this trap.
"The first time I watched a canary abort, I panicked a little — I re-applied the exact same Git commit expecting it to try again, and nothing happened. Turned out the abort lives in status, not spec, so re-applying an identical spec is a no-op as far as the controller's concerned. What actually got it moving was retry rollout. Now the first thing I check on any stuck release isn't the Deployment logs, it's whether the Rollout is sitting in Degraded waiting for exactly that."
Argo Rollouts vs. the alternatives
☺ Like you're 10: A few other tools solve this exact same problem, each trading away a different piece of control.
| Option | Shape | Choose it when… |
|---|---|---|
| Argo Rollouts | Rollout replaces the Deployment (or wraps it via workloadRef); an authored step list | You want an explicit, hand-written recipe — mixed timed and manual gates, an uneven weight curve — and you already run Argo CD. |
| Flagger | A separate Canary object wraps an untouched Deployment | You'd rather not change the workload kind at all, live in a Flux shop, and prefer one team-wide convention over an authored recipe per app. |
| Plain Deployment + feature flags | Normal RollingUpdate; release risk controlled entirely in application code | Risk is per-feature rather than per-binary, or request-level traffic control simply isn't available to you. |
| Hand-edited mesh weights | You patch VirtualService/HTTPRoute weights yourself, on demand | A genuine one-off migration or experiment — never a repeated pattern, since you'd be hand-rolling the controller. |
The CAPA blueprint weights Argo Rollouts at 18% of that exam, behind Workflows and Argo CD but ahead of Events, and two of its three named competencies are exactly this page's spine: "Use Common Progressive Rollout Strategies" is the canary/blue-green section above, and "Describe Analysis Template and AnalysisRun" is the section right after it — the third, "Understand Argo Rollouts Fundamentals," is the gap-a-Deployment-leaves-open section this page opens with. The CGOA blueprint touches the same ground from the specification side, under its progressive-delivery patterns competency, without asking you to write a single line of this page's YAML. And the capstone's Progressive Delivery lab is where all of this stops being reading and starts being a canary you deploy, watch, and — on purpose — break.
Foxy: We've got a canary now — setWeight: 10, wait two minutes, setWeight: 100. Progressive delivery, done.
Benny: Who's watching during those two minutes, Foxy?
Foxy: …the pause is watching?
Benny: A pause is a nap, not a nurse. Without an analysis step you've built a slower bad deploy — same bug, everyone, two minutes later.
Gizmo: Or — hear me out — promote --full on every release. Skips the steps and the analysis. So much faster! 🤑
Timmy: That flag exists for the incident where you already know the fix, Gizmo. Wire it into a pipeline and you've paid for a Rollout to behave like a plain Deployment.
Ellie: And whatever the AnalysisRun decides is only as honest as the metrics feeding it. Break my scrape config and the canary promotes a broken build very politely.
1. Name the four canary step types and what each one does. 2. What does a Rollout give you that a Deployment's RollingUpdate fundamentally cannot? 3. What is the difference between an AnalysisTemplate and an AnalysisRun? 4. What does autoPromotionEnabled default to in a blueGreen strategy, and why does that matter? 5. After an analysis-triggered abort, what is serving traffic, and what status does the Rollout sit in? 6. Why can an Argo CD Application show Synced while a Rollout underneath it is stuck Degraded? 7. Without any trafficRouting block configured, how is setWeight actually enforced?
Check your answers
setWeight(send N% of traffic to the canary),pause(timed or indefinite hold),analysis(run a template inline and block until it passes), andsetCanaryScale(decouple pod count from traffic weight).- Traffic-percentage control independent of pod counts, plus a measured gate between steps and an automatic abort — a RollingUpdate only ever checks readiness probes and never stops itself on its own initiative.
- An AnalysisTemplate (or ClusterAnalysisTemplate) is the reusable definition — the metric, its provider, its success condition. An AnalysisRun is the instance the controller creates when a rollout reaches an analysis step, carrying the live measurement values.
- It defaults to true. Left unset, a blueGreen rollout flips to the new version the instant preview pods pass readiness — a slower Deployment with extra steps, not an actual gate.
- The stable ReplicaSet is serving 100% of traffic and the canary is scaled down, but the
Rolloutobject still holds the new spec and sits inDegradedstatus withabort: true. It will not retry on its own. - Sync status only compares the applied spec against Git — and the abort lives in the Rollout's status, not its spec, so the spec Argo CD applied still matches Git perfectly even while production traffic never moved off the old version.
- By the ratio of canary to stable pod counts — an approximation only, imprecise at low replica counts and sticky per connection, not a genuine percentage of requests.