Argo Rollouts Canary Deployment - The Complete Production Guide
If you’ve ever shipped a bad release straight to 100% of your users and then spent the next hour frantically rolling back while Slack lights up with angry messages, you already understand why canary deployments exist. A plain Kubernetes Deployment gives you rolling updates, but it has no idea whether your new version is actually healthy — it just keeps replacing pods on a timer and hopes for the best. Argo Rollouts fixes that gap by giving you a real progressive delivery controller: gradual traffic shifting, automated metric analysis, and one-click rollback, all as native Kubernetes resources.
In this guide, we’re going to go far beyond a “hello world” canary. We’ll build a production-grade setup: a Rollout backed by real traffic-shaping through a service mesh, automated analysis using Prometheus metrics, manual and automatic promotion gates, and a CI/CD pipeline that ties it all together. By the end, you’ll have a canary pipeline you could actually trust with a real production service.
What You’ll Learn
By the end of this tutorial, you will know how to:
- Install and configure Argo Rollouts on a Kubernetes cluster
- Convert an existing Deployment into a Rollout resource
- Define canary steps with weighted traffic shifting and pauses
- Wire up an AnalysisTemplate that queries Prometheus and automatically aborts bad releases
- Integrate Argo Rollouts with a traffic-management layer (Istio and NGINX Ingress examples)
- Use the
kubectl argo rolloutsplugin and dashboard to watch a canary roll out live - Automate promotion and rollback inside a CI/CD pipeline
- Apply production best practices around resource limits, RBAC, and failure handling
Prerequisites
Before we start, make sure you have:
- A running Kubernetes cluster (1.24+), local (kind/minikube) or cloud-managed
kubectlconfigured to talk to your cluster- Helm 3 installed
- Basic familiarity with Kubernetes Deployments and Services
- (Optional but recommended) Prometheus already running in-cluster for metric-based analysis
- (Optional) Istio or NGINX Ingress Controller if you want real traffic splitting rather than replica-based approximation
Why Not Just Use a Regular Deployment?
A standard rolling update shifts traffic by replacing pods, which only approximates a percentage split, and it has zero concept of “is this actually working.” If your new pods are crash-looping or leaking memory, Kubernetes will happily keep rolling forward. Argo Rollouts introduces a custom resource, the Rollout, that is a drop-in superset of Deployment but adds:
- Weighted canary steps — send exactly 5%, then 20%, then 50% of traffic to the new version
- Automated analysis — query Prometheus, Datadog, New Relic, or a custom webhook after each step, and abort automatically if error rates or latency spike
- Native traffic routing integration — plug into Istio VirtualServices, NGINX canary annotations, AWS ALB, SMI, or Traefik for real percentage-based splitting instead of pod-count approximation
- Instant rollback — abort a bad rollout and Argo Rollouts snaps traffic back to the stable ReplicaSet in seconds
Installing Argo Rollouts
Install the controller and CRDs into a dedicated namespace:
1 | kubectl create namespace argo-rollouts |
Verify the controller is running:
1 | kubectl get pods -n argo-rollouts |
You should see the argo-rollouts deployment pod in Running state. Next, install the kubectl-argo-rollouts plugin, which gives you a live terminal dashboard for watching rollouts in progress:
1 | curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64 |
Converting a Deployment into a Rollout
Here’s a typical Deployment for a service called checkout-api:
1 | apiVersion: apps/v1 |
To turn this into a canary-capable Rollout, change kind to Rollout, and add a strategy block. The apiVersion changes to argoproj.io/v1alpha1:
1 | apiVersion: argoproj.io/v1alpha1 |
That’s the minimum viable canary: 5% of traffic for 2 minutes, then 20% for 5 minutes, then 50% for 5 minutes, then full rollout. Every pause without a duration waits indefinitely for a human to run kubectl argo rollouts promote.
Apply it, then trigger a new version by updating the image:
1 | kubectl apply -f checkout-api-rollout.yaml |
Watch it progress live:
1 | kubectl argo rollouts get rollout checkout-api --watch |
You’ll see a real-time ASCII visualization of the stable and canary ReplicaSets, their weights, and pod health.
Real Traffic Splitting with Istio
Replica-count-based canaries are a decent approximation but not exact — with 5 replicas, you can’t actually hit “5% of traffic.” For precise, production-grade traffic shifting, integrate with a service mesh. Here’s the Istio setup.
First, define the two Services Argo Rollouts will manage (stable and canary):
1 | apiVersion: v1 |
Then a VirtualService that Argo Rollouts will patch automatically as weights shift:
1 | apiVersion: networking.istio.io/v1beta1 |
Now update the Rollout’s strategy to reference the Istio traffic routing:
1 | spec: |
From here on, Argo Rollouts edits the VirtualService weights directly at every step — no approximation, exact percentage-based traffic splitting.
If you’re on NGINX Ingress instead of Istio, the equivalent block is
trafficRouting.nginx.stableIngress: checkout-api-ingress, and Argo Rollouts manages thenginx.ingress.kubernetes.io/canary-weightannotation for you automatically.
Other Traffic Routing Providers
Istio and NGINX are the most common, but Argo Rollouts supports several other production traffic layers. Pick whichever matches your existing infrastructure — you don’t need to adopt a mesh just to get canary support.
AWS ALB Ingress Controller — Argo Rollouts manages weighted target groups behind an Application Load Balancer directly:
1 | spec: |
SMI (Service Mesh Interface) — useful for Linkerd or other SMI-compatible meshes:
1 | spec: |
Traefik — via the TraefikService CRD:
1 | spec: |
In every case, the pattern is identical: define stable/canary Services, point trafficRouting at your provider’s resource, and Argo Rollouts takes care of patching the weights at each step. This means you can swap the underlying networking layer later without touching your canary steps or analysis templates.
Locking Down RBAC
Never install Argo Rollouts with cluster-admin. The controller only needs to manage Rollouts, ReplicaSets, and the specific traffic-routing resources you use. Here’s a scoped ClusterRole that covers the Istio + Prometheus setup from above:
1 | apiVersion: rbac.authorization.k8s.io/v1 |
Bind it to the argo-rollouts ServiceAccount in the argo-rollouts namespace only, and if you’re running multi-tenant, prefer namespaced Role/RoleBinding pairs per team instead of a single cluster-wide grant.
Automated Analysis: Let Metrics Decide, Not the Clock
Timed pauses are better than nothing, but the real power of Argo Rollouts is automated analysis — querying real metrics after each step and aborting automatically if something looks wrong. This is what makes canary deployments genuinely production-safe instead of a fancy progress bar.
Define an AnalysisTemplate that queries Prometheus for the error rate of the canary pods:
1 | apiVersion: argoproj.io/v1alpha1 |
This checks every minute, five times, that the 5xx error ratio stays under 5%. If it fails twice, the whole analysis run is marked as failed and Argo Rollouts automatically aborts and rolls back.
Now wire this template into the canary steps:
1 | spec: |
Now every stage of the rollout has an actual health gate. If error rates spike at 20%, the rollout aborts before it ever touches half your traffic.
Background Analysis (Running Checks the Whole Time, Not Just Between Steps)
Step-scoped analysis blocks only run when the rollout passes through them. For metrics you want monitored continuously for the entire duration of the rollout — not just at discrete checkpoints — use analysis.templates at the top of the canary spec instead of inside steps:
1 | spec: |
startingStep: 1 means background analysis kicks in as soon as the first setWeight step completes, and it keeps re-querying on its own interval for as long as the rollout is in progress — so a regression that only shows up 4 minutes into a 5-minute pause still gets caught.
Getting Paged: Argo Rollouts Notifications
Install the notifications controller (bundled with the standard manifest) and configure triggers so a failed or successful rollout reaches Slack, PagerDuty, or a webhook without anyone watching a terminal:
1 | apiVersion: v1 |
Then annotate the Rollout to subscribe it to that trigger:
1 | metadata: |
Now a Degraded rollout pages the platform-alerts Slack channel the moment the analysis gate fails — the same second traffic snaps back to stable.
GitOps: Driving Rollouts with Argo CD
Most production setups don’t kubectl apply manifests by hand — they run through Argo CD (or Flux) so the cluster state is always a reflection of Git. Argo Rollouts and Argo CD are built by the same project and integrate natively: Argo CD syncs the Rollout manifest like any other resource, and the Argo CD UI understands Rollout health status out of the box (you’ll see the canary progress bar directly in the Argo CD application view).
A typical GitOps flow looks like this:
- CI builds and pushes a new image tag, then updates the image reference in a Git repo (via a tool like Kustomize image overrides or a templated Helm values file).
- Argo CD detects the Git change and syncs the updated
Rolloutmanifest to the cluster. - The Argo Rollouts controller sees the new
spec.template.spec.containers[].imageand starts the canary steps automatically — Argo CD doesn’t need to know anything about canaries, it just syncs the spec. - Argo Rollouts’ own analysis gates decide whether the rollout finishes or aborts; Argo CD reflects that as the Application’s health.
1 | apiVersion: argoproj.io/v1alpha1 |
Because selfHeal: true reconciles the live Rollout back to what’s in Git, don’t kubectl argo rollouts promote manual pauses this way in a GitOps setup — Argo CD will fight you. Instead, either keep manual pauses out of GitOps-managed Rollouts (use pure automated-analysis gating), or manage promotion through Argo CD’s own resource actions, which understand Rollout’s promote/abort as first-class Application actions in the UI and CLI (argocd app actions run checkout-api promote-full --kind Rollout).
Compatibility with Horizontal Pod Autoscaling
A Rollout works with a standard HorizontalPodAutoscaler exactly like a Deployment does — just target it as the scale subject:
1 | apiVersion: autoscaling/v2 |
One subtlety: during a canary, both the stable and canary ReplicaSets share the total replica count that the HPA controls, split according to the current weight (rounded up so at least one canary pod exists once weight > 0). If your HPA is actively scaling while a canary is mid-rollout, set spec.strategy.canary.dynamicStableScale: true so the stable ReplicaSet scales down as the canary scales up, instead of both sitting at full size simultaneously and doubling your resource footprint.
Watching and Controlling a Rollout
The plugin gives you full manual control whenever you need to step in:
1 | # Watch progress live |
If you’d rather have a GUI, run the built-in dashboard:
1 | kubectl argo rollouts dashboard |
Then open http://localhost:3100 for a visual view of every Rollout in the cluster, including live traffic weight graphs.
Integrating with CI/CD
In production you don’t want humans typing promote at 2 AM. Here’s a minimal GitHub Actions job that deploys a new version and lets Argo Rollouts’ own analysis gates decide whether it succeeds, failing the pipeline if the rollout aborts:
1 | name: Deploy Canary |
kubectl argo rollouts status --watch blocks until the Rollout is either Healthy (all steps and analyses passed, 100% traffic) or Degraded (an analysis failed and it aborted) — and it returns a non-zero exit code in the failure case, which fails your CI job automatically. No manual babysitting required.
Monitoring the Rollout Process Itself
Beyond analyzing your application’s metrics, it’s worth monitoring Argo Rollouts’ own health. The controller exposes Prometheus metrics on port 8090 by default — scrape them and build a dashboard around:
rollout_info_replicas_available— track available replicas per Rollout over timerollout_phase— alert if any Rollout sits inDegradedorPaused(indefinite) for longer than expectedcontroller_clientset_k8s_request_total— catch API throttling if you’re running many Rollouts cluster-wide
A minimal ServiceMonitor (if you’re using the Prometheus Operator):
1 | apiVersion: monitoring.coreos.com/v1 |
Import Argo Rollouts’ official Grafana dashboard (ID 19870 on grafana.com) for an out-of-the-box view of rollout phases, step progress, and analysis run pass/fail rates across every Rollout in the cluster — genuinely useful once you have more than two or three services on canary.
Incident Walkthrough: What Actually Happens When a Canary Fails
It helps to walk through a real failure end to end so the pieces above click together. Say checkout-api:1.1.0 ships with a bug that only appears under load — checkout starts throwing 500s once the database connection pool saturates.
- Rollout begins.
setWeight: 5fires, 5% of traffic (or exactly 5% via the Istio VirtualService) hits the canary pods. - First analysis window opens. The
checkout-api-error-rateAnalysisRun starts querying Prometheus every 1 minute. - Error rate climbs. Under the small 5% slice, load might be light enough that nothing fails yet — this is exactly why later steps matter, not just the first one.
setWeight: 20fires, analysis restarts (or, with background analysis, keeps running continuously). Connection pool saturation starts kicking in at this higher volume; the error-rate query returns0.09(9%) against asuccessConditionof<= 0.05.- First failed measurement recorded.
failureLimit: 2means one more bad reading and the AnalysisRun fails. - Second measurement, one minute later, also fails. The AnalysisRun’s status flips to
Failed. - Argo Rollouts marks the Rollout
Degradedand automatically sets the Istio VirtualService weight back to 100% stable / 0% canary — this happens in seconds, not minutes. - The bad ReplicaSet is scaled down (or left at a minimal
scaleDownDelaySecondsfor debugging, if you’ve configured one). - Notifications fire — the Slack trigger from the
on-rollout-step-completedtemplate above posts toplatform-alertsthe instant the phase becomesDegraded. - CI/CD reflects the failure. The
kubectl argo rollouts status --watchcommand in the GitHub Actions job exits non-zero, and the pipeline goes red — nobody had to notice the Slack message for the deploy to be blocked.
Total blast radius: roughly 20% of traffic for a couple of minutes, automatically contained, with the team paged before a human even opened a dashboard. That’s the entire value proposition of production canary deployments in one sequence.
If you want to inspect exactly what happened after the fact:
1 | kubectl argo rollouts get rollout checkout-api |
Production Best Practices
A handful of things separate a “canary that works in a demo” from one you can trust in production:
- Set
progressDeadlineSecondson the Rollout so a stuck rollout (pods that never become ready) fails fast instead of hanging forever. - Always pair
setWeightsteps with ananalysisstep, not just a timed pause — clocks don’t know if your service is broken, metrics do. - Scope RBAC tightly. The Argo Rollouts controller needs permissions to patch VirtualServices/Ingresses; don’t grant it cluster-admin. Use the namespaced install and a scoped ClusterRole.
- Keep
failureLimitlow on AnalysisTemplates (1–2) — you want fast aborts, not five failed checks before anyone notices. - Version your AnalysisTemplates alongside your Rollouts in Git so a rollback of the manifest also rolls back the safety gates.
- Test the abort path deliberately — deploy a known-bad image in staging and confirm the rollout actually aborts and traffic actually returns to stable. Don’t find out your metrics query has a typo during a real incident.
- Alert on
Degradedrollouts — pipe Argo Rollouts’ Kubernetes events into your alerting stack so a failed canary pages someone immediately. - Use
restartAtordynamicStableScalecarefully on high-traffic services to avoid over-provisioning during long canary windows. - Prefer background analysis over step-only analysis for anything with load-dependent failure modes (connection pools, memory leaks, cache warm-up) — problems that only appear a few minutes into a step will be missed if analysis only samples right at the transition.
- If you’re on GitOps (Argo CD/Flux), avoid indefinite manual pauses in Rollouts managed with
selfHeal: true, or drive promotion through the GitOps tool’s own resource actions instead of rawkubectl argo rollouts promote. - Set
dynamicStableScale: truewhenever an HPA is attached to the Rollout, so you don’t silently double your pod count for the duration of every canary. - Wire up Notifications before you need them. A Degraded rollout that nobody sees until the next morning defeats most of the point of automated analysis.
- Scrape the controller’s own metrics. If the controller itself is throttled or falling behind on many concurrent Rollouts, step timing and analysis intervals silently drift.
Common Pitfalls
- Forgetting
stableService/canaryService— without both, traffic routing integrations silently do nothing and you get replica-based approximation instead of real weighted splitting. - Prometheus query returns no data — if
http_requests_totalisn’t labeled the way your query expects, the AnalysisRun will error out (not fail gracefully), stalling the rollout. Test queries directly in Prometheus first. - Analysis interval shorter than metric scrape interval — you’ll just be re-querying stale data. Match
intervalto your actual scrape/aggregation window. - Mutating the Rollout mid-flight — changing
spec.templatewhile a rollout is in progress starts an entirely new rollout on top of the old one. Let one finish (or abort it) before pushing another change. - GitOps self-heal fighting manual promotion — if Argo CD has
selfHeal: trueand the live Rollout drifts from Git (e.g., you manually promoted past a pause), Argo CD will try to resync and can interfere. Handle promotion through Argo CD’s resource actions in GitOps setups, not rawkubectlcommands. - HPA and canary both scaling replicas at once — without
dynamicStableScale: true, a canary during an autoscaling event can temporarily run stable and canary ReplicaSets at combined full size, spiking resource usage well above what you’d expect. - AnalysisRun stuck in
Inconclusive— this happens when a metric provider is unreachable (Prometheus down, network policy blocking the controller) rather than when the metric itself fails the condition. Check controller logs, not just the AnalysisRun status, to tell the difference. - Notifications never fire — the subscription annotation and the ConfigMap trigger name must match exactly; a typo in
notifications.argoproj.io/subscribe.<trigger>.<service>silently does nothing with no error surfaced anywhere.
Wrapping Up
Argo Rollouts turns “deploy and hope” into “deploy and verify” — every step in the process now has a real, measurable gate instead of a fixed timer. Once you’ve got weighted traffic splitting through a mesh and automated Prometheus-backed analysis in place, a bad release gets caught and rolled back automatically before most of your users ever see it, and your CI pipeline doesn’t need a human standing by to make the call.
From here, natural next steps are exploring BlueGreen strategy for services that can’t tolerate mixed-version traffic at all, or Experiment resources for running two versions side-by-side purely for comparison without shifting production traffic. Both build on exactly the same Rollout foundation you just set up.





