Canary releases, step by step
Ship to a slice of traffic, watch the metrics, then promote or abort — with the decision written as one if/else block you can read and dry-run before it ever touches production.
blog/canary-releasesA canary release is a simple idea wearing a complicated costume. Ship the new version to a small slice of traffic, watch it for a minute, and if the error rate stays flat, roll it out to everyone. If it doesn't, pull it back before most of your users ever see it.
The idea is three steps. The implementations are usually 300 lines of someone's deploy.sh, a Jenkins stage that nobody can run locally, or a service mesh config that takes a PhD to read. The hard part was never the deploy — it was the decision. Where, exactly, does "promote" branch away from "abort", and can you see that branch before you trust it at 2am?
This post writes the canary as an OrchStep workflow where the promote/abort decision is one if/else block you can preview with --dry-run.
The shape
Deploy the canary. Watch it. Branch on what you saw.
name: canary
# Override any of these at the CLI with --var, e.g. --var version=2.5.0
defaults:
version: "2.4.0"
target: production
canary_weight: "10"
tasks:
# orchstep run release --var version=2.5.0
release:
steps:
- name: deploy_canary
func: shell
do: echo "deploying canary {{ vars.version }} to {{ vars.canary_weight }}% of {{ vars.target }}"
# Watch the canary. In a real run this queries your metrics backend;
# here it emits a status string we branch on below.
- name: watch
func: shell
do: echo "healthy"
outputs:
status: '{{ result.output | trim }}'
- name: decide
if: '{{ eq steps.watch.status "healthy" }}'
then:
- name: promote
func: shell
do: echo "canary healthy — promoting {{ vars.version }} to 100% of {{ vars.target }}"
else:
- name: abort
func: shell
do: echo "canary unhealthy — aborting and rolling {{ vars.target }} back"
finally:
- name: record
func: shell
do: echo "canary decision recorded for {{ vars.version }}"Three things are doing the work here, and none of them are clever:
watchcaptures an output. The step's stdout becomessteps.watch.status. In production you'd replace thatechowith a query against Prometheus, Datadog, or your error-budget dashboard — anything that prints a verdict. The workflow doesn't care where the number comes from, only what it says.decideis the canary. Theif:evaluates the captured status. Healthy traffic takes thethen:path and promotes; anything else takeselse:and rolls back. That is the entire control plane, in eight lines.finally:always records the outcome. Promote or abort, the decision gets logged. No leaking the "we aborted but forgot to tell anyone" failure mode.
See the branch before you trust it
The reason teams fear canaries isn't the deploy — it's not knowing which way the fork will go. A dry-run resolves the variables, evaluates the if:, and prints the exact plan without running anything:
orchstep run release --var version=2.5.0 --dry-runYou can read that the healthy path promotes and the unhealthy path rolls back, on this version, with these weights, before a single byte of traffic shifts. That is the difference between "I'm pretty sure the abort path works" and watching it resolve in front of you. Full tour: Previewing with Dry Run.
To rehearse the bad day, flip the watch step's output to anything other than healthy and run it — the else: branch fires and you can confirm the rollback wording and notifications are right while nothing is on fire.
What you actually gained
| Concern | Hand-rolled canary | OrchStep |
|---|---|---|
| The promote/abort decision | buried in shell if [ ... ] | one if/else block |
| Metric verdict | scraped inline, untracked | a named step output |
| "Which way will it go?" | find out in prod | --dry-run prints the branch |
| Always notify | easy to forget | finally: runs regardless |
| Rehearsing the abort path | redeploy and pray | flip one output, run it |
Where OrchStep stops
OrchStep does not move traffic for you. The echos above stand in for your real kubectl, argo, or load-balancer commands — swap them in and the structure is unchanged. If your platform already gives you progressive delivery with automated analysis (Argo Rollouts, Flagger) and you trust it, use that. OrchStep earns its place when the decision logic lives in scripts you can't read, run locally, or preview — and you want it to be eight legible lines instead.
Where to go next
- Variables & Outputs — capturing a step's stdout and branching on it
- Error Handling —
catch,finally, and timeouts - Previewing with Dry Run — resolve the plan, execute nothing
The demo above is runnable as-is — every step only echos. orchstep run release --dry-run shows you the canary's fork before you wire in real metrics.
curl -fsSL https://orchstep.dev/install.sh | sh