Build the mental model
Rolling, blue-green and canary are three answers to one question: while the new version is going out, what does live traffic see?
A rolling deploy replaces instances a few at a time. Old and new run side by side for the length of the rollout, so both must tolerate each other — and, critically, both must tolerate the same database schema. It needs no extra capacity beyond a little headroom, which is why it is the default almost everywhere, but a bad version reaches users gradually rather than not at all, and rolling back means another full rollout in reverse.
Blue-green runs two complete environments. Green sits idle, you deploy to it, you exercise it against real infrastructure with no real users, then you flip the router. The cutover is near-instant and so is the undo — flip back. You pay for that with roughly double the production footprint during the release, and with the awkwardness of in-flight requests and cold caches on the side you just switched to.
Canary sends a small slice of real traffic — one per cent, then ten, then fifty — to the new version and watches error rate and latency between steps. It catches the problems only real users produce, at the cost of a traffic-splitting layer and, more demanding, agreed metrics and thresholds that decide promote-or-abort. Without that analysis a canary is just a slow rolling deploy with extra steps.
Zero downtime is not a property of any of these. It requires backward-compatible requests, drained connections, and health checks that genuinely reflect readiness rather than process liveness. And the truly hard part is rarely the application. Two versions share one database, so the schema must satisfy both at once — which is why migrations, not deploy mechanics, are what sink most releases.
TRAFFIC DISTRIBUTION OVER TIME
------------------------------
TIME ------------------------------------------------------->
ROLLING
old ######### ###### ### #
new ## #### ###### #########
both versions live at once; one schema must serve both
BLUE-GREEN
blue ###########################|
green |###################
switch --+ (instant, reversible)
cost: two full environments during the release
CANARY
old ########## ####### ##### ###
new # ## ##### #########
1% 10% 50% 100%
| | |
+-- check error rate / latency, then promote or abort
HARD PART (all three): the database both versions shareConnect it to a real scenario
This workflow expresses canary as explicit steps. The canary job shifts ten per cent of traffic to the new image and then evaluates error rate over a window. The load-bearing detail is that `check-error-rate.sh` must exit non-zero when the threshold is breached — that failure is what drives every subsequent decision. A script that merely prints a metric for a human to read turns your canary into a dashboard.
The promote job carries `needs: canary`, so it only runs when the canary held. The rollback job carries `if: failure()`, so a breached threshold sends traffic straight back to the stable version without anybody typing a command. That is what makes the pattern useful under pressure: the undo path is already written down and already tested by every release, rather than being recalled from memory during an incident.
To model blue-green instead, set the weight to 100 in one step and drop the observation window — which shows that blue-green is really a canary with a single step and no analysis phase. Before you rely on either, verify the assumption underneath all of it: run two versions against your real database at the same time and confirm the older one still works after the newer one has written a row.
Try the working example
name: Canary Release
on:
workflow_dispatch:
inputs:
image_tag:
description: Immutable image tag to release
required: true
type: string
permissions:
contents: read
id-token: write
jobs:
canary:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE }}
aws-region: ap-southeast-1
- name: Send 10 percent of traffic to the new version
run: ./scripts/set-traffic-weight.sh ${{ inputs.image_tag }} 10
- name: Fail if error rate breaches the threshold
run: ./scripts/check-error-rate.sh --window 10m --max-error-rate 0.01
promote:
needs: canary
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE }}
aws-region: ap-southeast-1
- name: Send all traffic to the new version
run: ./scripts/set-traffic-weight.sh ${{ inputs.image_tag }} 100
rollback:
needs: canary
if: failure()
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE }}
aws-region: ap-southeast-1
- name: Send all traffic back to the stable version
run: ./scripts/set-traffic-weight.sh ${{ inputs.image_tag }} 0
Starting the workflow manually with an image tag runs the canary job: it shifts ten per cent of traffic to the new version and then evaluates the error rate. If that check passes, promote runs and moves all traffic across, and the rollback job is skipped. If the check exits non-zero, promote is skipped because its `needs` dependency failed, while the rollback job — guarded by `if: failure()` — runs and returns the traffic weight to zero, leaving the stable version serving everything. All three jobs declare the production environment, so any protection rule you configured there gates the canary step first, before any traffic moves.5-minute try-it
Extend this into a multi-step canary: three sequential jobs at one, ten and fifty per cent, each wired with `needs:` and each followed by its own error-rate check. Confirm the rollback job still triggers no matter which step fails — you may need to widen its `needs:` list. Then, on paper, take the most recent migration your application shipped and answer honestly whether the previous version could keep serving traffic against that schema. If the answer is no, you cannot safely use any of these three strategies yet.
One important caution
Allowing two application versions to run side by side while migrating the schema for only the new one — both rolling and canary break in the middle
Judging a canary by elapsed time rather than by traffic volume: at low request rates, ten minutes may carry too few requests for a real regression to show up at all
Google Cloud Architecture: Application deployment and testing strategies — CI/CD with GitHub Actions