Blue/Green Rollout
Deploy to the idle slot, verify before any traffic moves, flip atomically, keep instant rollback
The blue/green discipline: the new version never sees traffic until it has proven itself on the idle slot, the cutover is a single atomic pointer write, and the previous slot stays warm as an instant rollback target.
Executed for real against OrchStep v0.7.1 - including a deliberately broken release.
What it demonstrates
| Capability | Where |
|---|---|
| State discovery | read_state derives active/idle from the pointer |
| Pre-traffic verification | verify_idle_slot with retry before the flip |
| Atomic cutover | flip_traffic writes one pointer file |
| Defense in depth | post_flip_check with catch-based auto-rollback |
| Failure injection | --var force_bad_release=true |
The workflow
name: bluegreen-rollout
desc: "Deploy to the idle slot, verify it, flip traffic atomically, keep the old slot for instant rollback"
defaults:
slots_dir: "./slots"
version: "2.4.0"
force_bad_release: "false"
tasks:
bootstrap:
desc: "Create the initial blue slot serving v1.0.0 (demo helper)"
steps:
- name: init
func: shell
do: |
rm -rf {{ vars.slots_dir }}
mkdir -p {{ vars.slots_dir }}/blue
echo "version=1.0.0" > {{ vars.slots_dir }}/blue/release.txt
echo "healthy" > {{ vars.slots_dir }}/blue/health
echo "blue" > {{ vars.slots_dir }}/ACTIVE
echo "bootstrapped: blue serves v1.0.0"
rollout:
desc: "The full blue/green cycle for vars.version"
steps:
- name: read_state
func: shell
do: |
ACTIVE=$(cat {{ vars.slots_dir }}/ACTIVE)
if [ "$ACTIVE" = "blue" ]; then IDLE=green; else IDLE=blue; fi
echo "active=$ACTIVE idle=$IDLE"
outputs:
active: '{{ result.output | regexFind "active=([a-z]+)" }}'
idle: '{{ result.output | regexFind "idle=([a-z]+)" }}'
- name: deploy_to_idle
desc: "New version lands on the slot taking no traffic"
func: shell
do: |
SLOT={{ vars.slots_dir }}/{{ steps.read_state.idle }}
rm -rf $SLOT && mkdir -p $SLOT
echo "version={{ vars.version }}" > $SLOT/release.txt
if [ "{{ vars.force_bad_release }}" = "true" ]; then
echo "crashed" > $SLOT/health
else
echo "healthy" > $SLOT/health
fi
echo "deployed {{ vars.version }} -> {{ steps.read_state.idle }}"
- name: verify_idle_slot
desc: "Health + smoke on the idle slot BEFORE any traffic moves"
func: shell
do: |
SLOT={{ vars.slots_dir }}/{{ steps.read_state.idle }}
grep -q "healthy" $SLOT/health && grep -q "version={{ vars.version }}" $SLOT/release.txt && echo "checks=pass"
retry:
max_attempts: 3
interval: "200ms"
outputs:
checks: '{{ result.output | regexFind "checks=([a-z]+)" }}'
- name: flip_traffic
desc: "The cutover is one atomic pointer write"
func: shell
do: |
echo "{{ steps.read_state.idle }}" > {{ vars.slots_dir }}/ACTIVE
echo "traffic -> {{ steps.read_state.idle }}"
- name: post_flip_check
desc: "Verify the live slot; auto-rollback if it fails"
func: shell
do: |
LIVE=$(cat {{ vars.slots_dir }}/ACTIVE)
grep -q "healthy" {{ vars.slots_dir }}/$LIVE/health && echo "live=$LIVE status=ok"
catch:
- name: rollback
desc: "Instant rollback: point back at the previous slot"
func: shell
do: |
echo "{{ steps.read_state.active }}" > {{ vars.slots_dir }}/ACTIVE
echo "ROLLED_BACK to {{ steps.read_state.active }}"
outputs:
status: '{{ result.output | regexFind "status=([a-z]+)" }}'
- name: summary
func: shell
do: |
ACTIVE=$(cat {{ vars.slots_dir }}/ACTIVE)
echo "ROLLOUT_DONE live_slot=$ACTIVE version=$(cat {{ vars.slots_dir }}/$ACTIVE/release.txt)"Run it
orchstep run bootstrap # blue serves v1.0.0
orchstep run rollout # v2.4.0 -> green, traffic flips
orchstep run rollout --var version=2.5.0-broken --var force_bad_release=trueVerified results:
ROLLOUT_DONE live_slot=green version=2.4.0
# broken release: idle verification fails after 3 attempts -> exit 1
# ACTIVE still points at green/v2.4.0 - the bad build never took trafficDesign notes
The gate sits before the flip. That is the whole point of blue/green:
the broken release failed verify_idle_slot and the workflow stopped with
traffic untouched. post_flip_check + rollback exists for regressions
that only show under real traffic.
Slots are directories here, infrastructure in production. Swap the file operations for your load balancer's target-group API or a k8s service selector patch - the orchestration shape (discover, deploy idle, verify, flip, re-verify, rollback) is identical.