Blue/green deploys with instant rollback
Rollback shouldn't mean a frantic re-deploy of the last good build while the pager screams. Deploy to the idle slot, verify it before any traffic moves, flip atomically, and let a catch block undo it.
blog/blue-green-deployThe worst time to discover your rollback plan is during the incident it was supposed to handle. The deploy went out, error rates spiked, and now rollback means re-running the pipeline with the previous tag — a fresh build, a fresh deploy, three to five minutes of bad traffic while you wait. "Rollback" that takes as long as a deploy isn't rollback. It's a second deploy you're doing under pressure.
Blue/green fixes this by never overwriting the running version. You keep two slots. The live one serves traffic; the idle one receives the new build. You verify the idle slot in isolation, then flip the router. Rollback is flipping it back — instant, because the previous version was never touched.
The discipline that makes it safe is sequencing: deploy to idle, verify before any traffic moves, then flip atomically — and have an automatic undo wired to the flip. That's exactly what a workflow with catch expresses.
Deploy idle, verify, flip, rollback on failure
Here's the deploy. Each step is a phase of the blue/green dance, and the flip step carries a catch that rolls back if the flip or its verification fails:
name: app
# Blue/green: deploy to the idle slot, verify it before any traffic moves,
# then flip atomically. If the flip or verify fails, catch rolls back —
# the live slot never saw a bad build.
defaults:
version: "2.4.0"
active: blue
idle: green
tasks:
# `orchstep run deploy --var version=2.5.0`
deploy:
steps:
- name: release_idle
func: shell
do: echo "deploying {{ vars.version }} to idle slot {{ vars.idle }}"
- name: verify_idle
func: shell
do: echo "verifying {{ vars.idle }} responds before any traffic"
- name: flip
func: shell
do: echo "flipping traffic {{ vars.active }} -> {{ vars.idle }}"
catch:
- name: rollback
func: shell
do: echo "flip failed — traffic stays on {{ vars.active }}, {{ vars.idle }} drained"
finally:
- name: record
func: shell
do: echo "deploy of {{ vars.version }} finished"
- name: confirm
func: shell
do: echo "{{ vars.idle }} is now live serving {{ vars.version }}"The sequence is the safety. release_idle never touches the live slot. verify_idle runs against the new version while it's still dark — if it fails, no user ever saw it. Only after verification does flip move the router, and its catch block is the instant rollback: a failed flip leaves traffic exactly where it was. The finally records the attempt either way, so your audit log has the outcome regardless of branch.
orchstep run deploy --var version=2.5.0
# deploying 2.5.0 to idle slot green
# verifying green responds before any traffic
# flipping traffic blue -> green
# green is now live serving 2.5.0Verify against the real health checks
The verify_idle step is where the demo's echo becomes your actual health probe — hit the idle slot's /healthz, check the version it reports, run a smoke test. Because verification happens before the flip, a bad build fails here, with zero blast radius:
curl -fsS https://green.internal/healthz | grep '"version":"2.5.0"'If that check exits non-zero, the step fails before the flip ever runs. The live slot keeps serving the known-good version while you look at the logs. This is the difference between catching a bad deploy in a dark slot and catching it from the error-rate graph.
Rollback is a flip, not a deploy
The thing worth internalizing: in this model the previous version is still deployed and warm on the active slot the entire time. Rollback doesn't rebuild or redeploy anything — it points the router back. The catch block does it automatically on a failed flip; a manual rollback after the fact is the same one operation, run on purpose. Either way it's seconds, not a pipeline run. More on the catch/finally model: Error Handling.
What you actually gained
| Concern | Re-deploy rollback | Blue/green + catch |
|---|---|---|
| Bad build reaches users | until you notice + redeploy | caught in the dark idle slot |
| Rollback time | a full deploy (minutes) | a router flip (seconds) |
| Undo on a failed flip | manual, under pressure | automatic catch block |
| Verify before traffic | hope and monitor | verify_idle gates the flip |
| Audit of the attempt | scrape logs | finally records it |
The honest caveat: blue/green wants two slots and a router you can flip atomically — load balancer target groups, a service mesh, DNS with a low TTL, whatever your platform gives you. If you're on a single box with no way to run two versions side by side, this pattern doesn't apply, and a fast forward-fix may serve you better. Where you do have two slots, wiring the flip with a catch turns rollback from a runbook into a code path.
Where to go next
- Error Handling —
catch,finally, and failure semantics - Variables & Outputs — track active/idle slots as variables
- Previewing with Dry Run — rehearse the flip before you run it
Tired of rollback meaning "redeploy the old tag and pray"? Make it a flip with an automatic undo — curl -fsSL https://orchstep.dev/install.sh | sh.
curl -fsSL https://orchstep.dev/install.sh | sh