BLOG/TEAM CI/CD
THEME
TEAM CI/CD

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.

May 30, 2026 OrchStep Team 6 minROLE: SRESCALE: Any
RUNNABLE DEMO
Full source for this post: blog/canary-releases
VIEW SOURCE

A 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.

orchstep.yml
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:

  • watch captures an output. The step's stdout becomes steps.watch.status. In production you'd replace that echo with 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.
  • decide is the canary. The if: evaluates the captured status. Healthy traffic takes the then: path and promotes; anything else takes else: 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-run

You 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

ConcernHand-rolled canaryOrchStep
The promote/abort decisionburied in shell if [ ... ]one if/else block
Metric verdictscraped inline, untrackeda named step output
"Which way will it go?"find out in prod--dry-run prints the branch
Always notifyeasy to forgetfinally: runs regardless
Rehearsing the abort pathredeploy and prayflip 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

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.

#CANARY#DEPLOY#SRE#RELIABILITY
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — TEAM CI/CD