Argo Rollouts — Progressive Delivery in Kubernetes
Argo Rollouts extends Kubernetes Deployments with canary and blue-green strategies, automated analysis, and traffic management integration.
Each major section below ends with a quick knowledge check — try to answer before revealing:
Why Not Plain Kubernetes Rolling Updates?
| K8s RollingUpdate | Argo Rollouts | |
|---|---|---|
| Traffic control | None — replicas = traffic | Precise % split (1% → 5% → 50% → 100%) |
| Automated analysis | No | Yes — query Prometheus/Datadog, pause on errors |
| Pause/resume | No | Yes — manual gate or metric gate |
| Blue-green | No | Yes — full traffic switch |
| Rollback trigger | Manual only | Automatic on metric degradation |
| Preview URL | No | Yes — separate Service for canary |
With a plain Kubernetes RollingUpdate, the new version starts throwing errors right after rollout. What triggers a rollback?
Architecture
graph TD
ROLLOUT["Rollout CRD<br/>(replaces Deployment)"] --> RS_STABLE["ReplicaSet: stable<br/>(current version)"]
ROLLOUT --> RS_CANARY["ReplicaSet: canary<br/>(new version)"]
ROLLOUT --> ANAL["AnalysisRun<br/>(queries Prometheus/Datadog)"]
ROLLOUT --> SVC_STABLE["Service: stable<br/>(100% --> reduces as canary grows)"]
ROLLOUT --> SVC_CANARY["Service: canary<br/>(0% --> grows)"]
SVC_STABLE --> LB["Ingress / AWS ALB<br/>weighted routing"]
SVC_CANARY --> LB
CTRL["Argo Rollouts Controller<br/>(watches Rollout CRDs)"] --> ROLLOUT
Do you keep your existing Deployment object and add a Rollout alongside it, or does the Rollout replace the Deployment entirely?
Install
kubectl create namespace argo-rollouts
kubectl apply -n argo-rollouts \
-f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml
# kubectl plugin for managing rollouts
brew install argoproj/tap/kubectl-argo-rollouts
Canary Rollout
Basic canary
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: my-app
spec:
replicas: 10
selector:
matchLabels:
app: my-app
template: # same as Deployment pod template
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:v2
ports:
- containerPort: 8080
strategy:
canary:
# Canary traffic goes to this service (separate from stable)
canaryService: my-app-canary
stableService: my-app-stable
steps:
- setWeight: 5 # send 5% traffic to canary
- pause: {duration: 5m} # wait 5 min
- setWeight: 20
- pause: {} # pause indefinitely — manual promotion required
- setWeight: 50
- pause: {duration: 5m}
- setWeight: 100 # full rollout
That steps list is a timeline, not a one-shot config — walk through what the rollout is actually doing at each stage:
kubectl argo rollouts promote my-app. This is the manual gate.
With automated Prometheus analysis
strategy:
canary:
canaryService: my-app-canary
stableService: my-app-stable
steps:
- setWeight: 10
- analysis:
templates:
- templateName: success-rate # ← AnalysisTemplate defined below
args:
- name: service-name
value: my-app-canary
- pause: {duration: 5m}
- setWeight: 50
- pause: {duration: 5m}
- setWeight: 100
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 1m
successCondition: result[0] >= 0.95 # 95% success rate required
failureLimit: 3 # 3 consecutive failures = abort rollout
provider:
prometheus:
address: http://prometheus:9090
query: |
sum(rate(http_requests_total{
service="{{args.service-name}}",
status!~"5.."
}[2m])) /
sum(rate(http_requests_total{
service="{{args.service-name}}"
}[2m]))
What happens on failure: If the success rate drops below 95% for 3 consecutive checks, the rollout automatically aborts — scales canary to 0, promotes nothing, and the stable version continues serving 100%.
ALB weighted routing (AWS)
For precise traffic splitting without replica-count tricks:
strategy:
canary:
canaryService: my-app-canary
stableService: my-app-stable
trafficRouting:
alb:
ingress: my-app-ingress # the ALB Ingress resource
servicePort: 8080
steps:
- setWeight: 1 # 1% canary — impossible with just replicas
- pause: {duration: 10m}
- setWeight: 10
- pause: {}
- setWeight: 100
# The Ingress — Argo Rollouts manages the weights automatically
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-app-ingress
annotations:
kubernetes.io/ingress.class: alb
spec:
rules:
- http:
paths:
- path: /
backend:
service:
name: my-app-stable # Rollouts controller updates weights on ALB
port:
number: 8080
A canary step has successCondition: result[0] >= 0.95 and failureLimit: 3. The success rate dips below 0.95 for 2 consecutive checks, then recovers. Does the rollout abort?
failureLimit: 3 counts consecutive failures — it takes 3 in a row before Argo Rollouts aborts (scales canary to 0, stable keeps serving 100%). A metric that dips for 2 checks and then recovers never hits that threshold, so the rollout just continues.Blue-Green Rollout
flowchart LR
subgraph Before
ACTIVE["active Service --> v1 pods<br/>(100% traffic)"]
PREVIEW["preview Service --> v2 pods<br/>(0% traffic, internal only)"]
end
subgraph After["After promotion"]
ACTIVE2["active Service --> v2 pods<br/>(100% traffic)"]
OLD["v1 pods kept 30s<br/>then scaled down"]
end
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: my-app-bg
spec:
replicas: 5
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-app:v2
strategy:
blueGreen:
activeService: my-app-active # production traffic
previewService: my-app-preview # test traffic (internal, QA)
autoPromotionEnabled: false # require manual promotion
# autoPromotionSeconds: 60 # or auto-promote after 60s
prePromotionAnalysis: # run analysis before switching traffic
templates:
- templateName: success-rate
args:
- name: service-name
value: my-app-preview
scaleDownDelaySeconds: 30 # keep old (blue) pods 30s after switch
Promotion:
# Manual promotion — switch active service to new version
kubectl argo rollouts promote my-app-bg
# Or via ArgoCD UI / CLI
argocd app sync my-app # if using ArgoCD to manage the Rollout
The Before/After diagram above is really five distinct moments — step through the actual cutover:
activeService routes 100% of production traffic to v1 pods. previewService points at v2 pods, but only internal/QA traffic reaches them — no production user has seen v2 yet.
success-rate AnalysisTemplate queries Prometheus against my-app-preview — validating v2 before any real traffic goes near it.
autoPromotionEnabled: false, nothing switches until kubectl argo rollouts promote my-app-bg (or an ArgoCD sync) runs.
activeService is repointed to the v2 pods. Production traffic goes from 0% to 100% on v2 in one shot — no gradual ramp like canary.
scaleDownDelaySeconds: 30 keeps them warm for 30s. A rollback in that window is just flipping activeService back; the v1 pods are still there to receive traffic.
Right after kubectl argo rollouts promote completes on a blue-green rollout with scaleDownDelaySeconds: 30, are the old v1 pods gone?
activeService switches to v2 immediately, but the v1 pods are kept running for 30 more seconds before being scaled down. That grace window is what makes a rollback "instant" — it's just flipping the Service back while the old pods are still warm, not waiting for new pods to start.kubectl Commands
# Watch rollout progress (live)
kubectl argo rollouts get rollout my-app --watch
# Promote to next step (unpause a manual pause)
kubectl argo rollouts promote my-app
# Abort rollout (scale canary to 0, stable remains)
kubectl argo rollouts abort my-app
# Manually retry after abort
kubectl argo rollouts retry rollout my-app
# Set image (triggers new rollout)
kubectl argo rollouts set image my-app app=my-app:v3
# Pause a running rollout
kubectl argo rollouts pause my-app
# View analysis run results
kubectl argo rollouts get rollout my-app -o json | jq '.status.currentStepAnalysisRunStatus'
ArgoCD + Argo Rollouts Integration
ArgoCD manages the Rollout CRD the same way it manages Deployment. Install the ArgoCD Rollouts extension so ArgoCD understands Rollout health:
# ArgoCD needs a custom health check for Rollout objects (Lua script in argocd-cm)
# Most setups use the official Rollouts plugin:
kubectl apply -n argocd \
-f https://raw.githubusercontent.com/argoproj-labs/argocd-extensions/main/examples/rollout/rollout-extension.yaml
# argocd-cm ConfigMap — custom health check for Rollout
resource.customizations.health.argoproj.io_Rollout: |
hs = {}
if obj.status ~= nil then
if obj.status.phase == "Healthy" then
hs.status = "Healthy"
elseif obj.status.phase == "Paused" then
hs.status = "Suspended"
hs.message = "Rollout is paused"
elseif obj.status.phase == "Degraded" then
hs.status = "Degraded"
hs.message = obj.status.message
else
hs.status = "Progressing"
end
end
return hs
GitOps flow with Argo Rollouts:
- Developer bumps image tag in git
- ArgoCD syncs → creates new
Rolloutrevision - Argo Rollouts controller starts canary steps
- Analysis queries Prometheus automatically
- On success: full promotion. On failure: auto-rollback to previous revision in git.
In this GitOps flow, who decides whether to abort a failing canary — someone watching the ArgoCD UI, or something automatic?
Rollout resource synced with git — the progressive-delivery decision-making happens inside Argo Rollouts, not ArgoCD.Canary vs Blue-Green Decision
| Canary | Blue-Green | |
|---|---|---|
| Traffic split | Gradual (1% → 100%) | Instant switch |
| Resource cost | Low (only a few canary pods) | 2× during transition |
| Rollback speed | Fast (shift traffic back) | Instant (flip service) |
| Best for | Stateless services, high traffic | Services needing zero-downtime switch, stateful |
| DB migrations | Risky — both versions run simultaneously | Safer — preview env for testing |
| Real user validation | Yes — real traffic on canary | No — preview is internal only |
You're rolling out a change to a stateful service that also touches the database schema. Which strategy gives you an isolated environment to validate the new version against before real users see it?