Kubernetes sites between a set of blue boxes and a set of green boxes. The Kubernetes Logo is like a ship's wheel and Argo the Octopus is there to steer.

Blue/green deployments on Kubernetes with Argo Rollouts

Jubril Oyetunji
Jubril Oyetunji

One of the harder questions to answer at scale is how to ship without a few seconds where your users are getting timeouts or your fleet is split across two image versions.

Kubernetes’s default rolling update strategy gradually deploys new pods and retires old ones, but during the swap, your service runs both versions side by side, and a regression in the new image affects every request that lands on a new pod.

Progressive delivery patterns like blue/green have long existed: you stand the new version up alongside the old, prove it’s healthy on a separate preview endpoint, then flip user traffic across. Blast radius shrinks to nothing in the bad case, rollback is a single command, and your release stops being a held-breath moment.

In this post, you’ll set that up with Argo Rollouts, the controller behind progressive delivery in the Argo ecosystem (which graduated from the CNCF in 2022).

What is Argo Rollouts (and why you need it)

Argo Rollouts is a Kubernetes controller and a set of CRDs that bolt blue/green, canary, and other progressive delivery strategies onto your cluster. The primary CRD is Rollout, a drop-in replacement for the standard Deployment.

You convert an existing Deployment by changing the apiVersion to argoproj.io/v1alpha1 and the kind to Rollout, then adding a strategy.blueGreen or strategy.canary block that describes how a new revision should roll out.

A major reason to reach for it is that there’s no built-in way to do this kind of traffic control in Kubernetes (you can’t decide where requests go independently of which pods are Ready). There’s no easy rollback to the previous version once the update has started.

Argo Rollouts fills in everything around that. It plugs into ingress controllers (Traefik, ALB) and service meshes (Istio, Linkerd, SMI) for real traffic shaping. It can query metrics providers (Prometheus, Datadog, CloudWatch, New Relic) to gate promotions on hard numbers, and it tracks every revision as its own ReplicaSet, so flipping back is instant.

Prerequisites

This tutorial assumes some familiarity with Kubernetes. You’ll also need:

  • A working Kubernetes cluster (EKS, GKE, AKS, or local like Minikube/Kind).
  • kubectl installed and pointed at the cluster (kubectl get nodes should return at least one Ready node)
  • helm v3 installed
  • curl for hitting the demo app

Step 1: Installing the Argo Rollouts controller

Argo Rollouts ships as a controller that runs in its own namespace, along with a kubectl plugin you’ll use on your laptop to inspect and steer rollouts.

To install the controller, follow these steps:

  1. Add the Argo Helm repo and install the controller:

    helm repo add argo https://argoproj.github.io/argo-helm
    helm repo update
    helm install argo-rollouts argo/argo-rollouts \
      --namespace argo-rollouts \
      --create-namespace \
      --wait
  2. Confirm the controller pods are up:

    kubectl -n argo-rollouts get pods

    You should see something like:

    NAME                             READY   STATUS    RESTARTS   AGE  
    argo-rollouts-dcd465dfc-8m2ql    1/1     Running   0          79s  
    argo-rollouts-dcd465dfc-q92k4    1/1     Running   0          79s

Step 2: Installing the kubectl argo rollouts plugin

The controller is running, but the most ergonomic way to drive a rollout — inspecting state, setting images, promoting, rolling back — is the kubectl argo rollouts plugin. It’s a separate binary that drops onto your PATH, and kubectl picks it up automatically.

To install the plugin, follow these steps:

On macOS with Homebrew:

brew install argoproj/tap/kubectl-argo-rollouts

On Linux:

curl -sLO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts-linux-amd64
sudo mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts

Verify it’s wired up:

kubectl argo rollouts version

You should see something like kubectl-argo-rollouts: v1.8.3+.... From here on, we’ll use kubectl argo rollouts … subcommands to drive the rollout.

Step 3: Defining the Rollout

Argo Rollouts’ core idea is that you swap your Deployment for a Rollout resource. The pod template inside it is identical to a Deployment’s.

What changes is the spec.strategy block, which describes how a new revision should roll out.

For blue/green, you need three things:

  1. A Rollout with spec.strategy.blueGreen configured
  2. An active Service, which always points to whichever ReplicaSet is currently serving production traffic
  3. A preview Service, which points at the new ReplicaSet before it gets promoted, so that you can test it in isolation

Argo Rollouts injects the rollouts-pod-template-hash label into each Service’s selector at runtime, which is how it switches traffic without you ever editing the Services.

Write the manifest:


cat > rollout.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: rollouts-demo-active
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  selector:
    app: rollouts-demo
---
apiVersion: v1
kind: Service
metadata:
  name: rollouts-demo-preview
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 8080
      protocol: TCP
  selector:
    app: rollouts-demo
---
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: rollouts-demo
spec:
  replicas: 2
  revisionHistoryLimit: 2
  selector:
    matchLabels:
      app: rollouts-demo
  template:
    metadata:
      labels:
        app: rollouts-demo
    spec:
      containers:
        - name: rollouts-demo
          image: argoproj/rollouts-demo:blue
          imagePullPolicy: IfNotPresent
          ports:
            - name: http
              containerPort: 8080
              protocol: TCP
          resources:
            requests:
              cpu: 25m
              memory: 32Mi
  strategy:
    blueGreen:
      activeService: rollouts-demo-active
      previewService: rollouts-demo-preview
      autoPromotionEnabled: false
      scaleDownDelaySeconds: 30
EOF

A few fields are worth calling out in the blueGreen block:

  • activeService and previewService are the names of the two ClusterIP Services above. Argo Rollouts owns their selectors from here on; you don’t edit them by hand.
  • autoPromotionEnabled: false is what makes this a manual promotion. The new ReplicaSet comes up, you inspect it on the preview Service, and only then do you flip the active Service over. Set it to true (the default) and Argo will auto-promote the moment the new pods are Ready.
  • scaleDownDelaySeconds: 30 keeps the old (blue) ReplicaSet around for 30 seconds after promotion, so if something goes wrong in those first few seconds, you can flip back instantly without rescheduling pods.

Apply it:

kubectl apply -f rollout.yaml

We’re using argoproj/rollouts-demo,, a tiny app published by the Argo team that serves an HTML dashboard and a /color endpoint that reports which tagged image is running (blue, green, yellow, etc.). It’s perfect for seeing the cutover happen in real time.

Step 4: Checking the initial state

Take a look at the rollout:

kubectl argo rollouts get rollout rollouts-demo

Output:

Name:            rollouts-demo  
Namespace:       default  
Status:          ✔ Healthy  
Strategy:        BlueGreen  
Images:          argoproj/rollouts-demo:blue (stable, active)  
Replicas:  
  Desired:       2  
  Current:       2  
  Updated:       2  
  Ready:         2  
  Available:     2  

NAME                                       KIND        STATUS     AGE  INFO  
⟳ rollouts-demo                            Rollout     ✔ Healthy  34s  
└──\# revision:1  
   └──⧉ rollouts-demo-86c957c6d6           ReplicaSet  ✔ Healthy  34s  stable,active  
      ├──□ rollouts-demo-86c957c6d6-72kjf  Pod         ✔ Running  34s  ready:1/1  
      └──□ rollouts-demo-86c957c6d6-nv3zg  Pod         ✔ Running  34s  ready:1/1

One revision, two pods, both stable and active. Both Services currently point at the same ReplicaSet hash. You can confirm with:

kubectl get svc rollouts-demo-active rollouts-demo-preview \
  -o jsonpath='{range .items[*]}{.metadata.name}{" -> hash="}{.spec.selector.rollouts-pod-template-hash}{"\n"}{end}'

Output:

rollouts-demo-active -> hash=86c957c6d6
rollouts-demo-preview -> hash=86c957c6d6

Step 5: Triggering a new version

Now let’s deploy a new revision. We’ll change the image tag from blue to yellow:

kubectl argo rollouts set image rollouts-demo \
  rollouts-demo=argoproj/rollouts-demo:yellow

Argo creates a new ReplicaSet (rev 2) for the yellow image and waits, because we set autoPromotionEnabled: false. The active Service still points at blue. The preview Service is re-pointed at yellow:

kubectl argo rollouts get rollout rollouts-demo

Output:

Status:          ॥ Paused
Message:         BlueGreenPause
Strategy:        BlueGreen
Images:          argoproj/rollouts-demo:blue (stable, active)
                 argoproj/rollouts-demo:yellow (preview)
Replicas:
  Desired:       2
  Current:       4
  Updated:       2
  Ready:         2
  Available:     2

NAME                                       KIND        STATUS     AGE  INFO
⟳ rollouts-demo                            Rollout     ॥ Paused
├──# revision:2
│  └──⧉ rollouts-demo-7cf9dff6bb           ReplicaSet  ✔ Healthy  38s  preview
│     ├──□ rollouts-demo-7cf9dff6bb-cbp2c  Pod         ✔ Running  38s  ready:1/1
│     └──□ rollouts-demo-7cf9dff6bb-fn2gt  Pod         ✔ Running  38s  ready:1/1
└──# revision:1
   └──⧉ rollouts-demo-86c957c6d6           ReplicaSet  ✔ Healthy  5m   stable,active
      ├──□ rollouts-demo-86c957c6d6-72kjf  Pod         ✔ Running  5m   ready:1/1
      └──□ rollouts-demo-86c957c6d6-nv3zg  Pod         ✔ Running  5m   ready:1/1

This is the heart of blue/green. The cluster is now running both versions, but only blue is serving real traffic.

Step 6: Proving the split with curl

Forward both Services to your laptop on different local ports:

kubectl port-forward svc/rollouts-demo-active 8080:80 >/dev/null 2>&1 &
kubectl port-forward svc/rollouts-demo-preview 8081:80 >/dev/null 2>&1 &
sleep 3

Hit each one using:

echo "active : $(curl -s http://127.0.0.1:8080/color)"
echo "preview: $(curl -s http://127.0.0.1:8081/color)"

Output:

active : "blue"  
preview: "yellow"

This is exactly the window where you’d run smoke tests, point a staging frontend at the preview hostname, or have Argo run an AnalysisTemplate against Prometheus.

Nothing about production traffic has changed yet.

When you’re done with the port-forwards, run the following command:

kill %1 %2 2>/dev/null

Step 7: Promoting

When you’re happy, flip the active Service over with one command:

kubectl argo rollouts promote rollouts-demo

Output:

rollout 'rollouts-demo' promoted

Argo updates the active Service’s selector to the new ReplicaSet hash.

Subsequent requests should show production traffic is in yellow. The old blue pods stick around for scaleDownDelaySeconds (30 by default) before being torn down, which is what makes the next section possible.

Confirm the cutover by running:

kubectl argo rollouts status rollouts-demo --timeout 60s

You should see Healthy, and the Service selectors should now agree:

kubectl get svc rollouts-demo-active rollouts-demo-preview \
  -o jsonpath='{range .items[*]}{.metadata.name}{" -> hash="}{.spec.selector.rollouts-pod-template-hash}{"\n"}{end}'

Output:

rollouts-demo-active -> hash=7cf9dff6bb
rollouts-demo-preview -> hash=7cf9dff6bb

Step 8: Rolling back

If something goes wrong after the promotion (a metric tanks, you spot an error in the logs, a teammate flags a bug), undo it by running:

kubectl argo rollouts undo rollouts-demo

That brings the previous ReplicaSet back as the new “preview” and pauses, waiting for you to confirm with promote once more, which flips the active Service back to it. Because the old pods were kept warm by scaleDownDelaySeconds, this happens in seconds, not whatever your image pull time is.

Step 9: Cleaning up

Once you’re done, you can tear down the demo using:

kubectl delete -f rollout.yaml
helm uninstall argo-rollouts -n argo-rollouts
kubectl delete ns argo-rollouts

How does this fit with Octopus Deploy and Argo CD

Everything we’ve done so far works on its own. You’ve got a Rollout, two Services, and a one-command promote/undo loop.

Argo Rollouts is happy to do its job at the cluster level. What it doesn’t have is an opinion on how dev becomes staging and then becomes production. Who’s allowed to push the button, or what the deployment history looked like six weeks ago.

This is the layer Octopus Deploy is built for. A good mental model is:

  • Argo Rollouts owns the cluster-side mechanics: Which ReplicaSet is active, which is preview, when to flip, and when to scale down old pods.
  • Argo CD owns the GitOps sync: The Rollout (and its Services) live in a Git repo, and the cluster state is reconciled to match.
  • Octopus owns everything above that: Environments, approval gates, release lifecycles, audit trails, and the self-service UI that developers actually click on.

The promotion path between environments is described once in Octopus and reused across every service, instead of being re-encoded in each team’s CI script.

Ship green, sleep through the night

If you made it this far, you’ve got the cluster-side mechanics of progressive delivery sorted: a Rollout flipping between active and preview Services, a manual promotion gate, and instant rollback. That’s the hard, hands-on layer done.

What’s missing is the orchestration above it, including environments, approvals, audit trails, and the self-service flow your developers actually click. That’s where Octopus Deploy slots in, sitting on top of Argo CD and Argo Rollouts to give you a complete progressive delivery stack across every environment, not just one cluster.

Connect your Argo CD instance to Octopus and see how the whole pipeline comes together, or try Octopus free and wire it up against your own cluster.

Happy deployments!

Jubril Oyetunji

Jubril is a software engineer, primarily focused on building infrastructure using cloud native technologies. When he’s not coding or ranting, he’s sharing his learnings through technical writing.

Related posts