5. Progressive rollout with health gates#
In this tutorial
You turn an all-at-once deploy into a progressive one: a canary node first, then one availability zone at a time, with bake times and a health gate on HTTP probes and a Prometheus error rate. After the canary a person approves; if the gate turns red, every node goes back to the previous release.
Time: 30 minutes · You need: four servers in two AZs reachable over SSH (or the use case lab), Prometheus scraping your application
flowchart LR
K["canary<br/>web-1 · eu-1a"] --> G1{"gate<br/>10 min"}
G1 -->|green| A{{"approval"}}
A --> Z1["rest/eu-1a<br/>web-2"]
Z1 --> G2{"gate<br/>5 min"}
G2 -->|green| Z2["rest/eu-1b<br/>web-3, web-4"]
Z2 --> G3{"gate<br/>5 min"}
G3 -->|green| OK([SUCCESS])
G1 & G2 & G3 -->|red| RB(["rollback-all"])
See it run first
The end-to-end lab runs exactly this kind of rollout across two regions and five AZs, including a red gate and a rollback: make test-usecases CASES="uc4 uc5" (and uc15 for canary analysis on Prometheus). See Use cases.
1. Describe the topology with labels#
Regions and AZs are not separate environments: they are labels of the targets of one environment, so governance, inventory and promotion keep reasoning about prod. region and az are reserved; any other label (here canary) is yours.
plugin: remote-copy
targets:
- {host: web-1.prod.internal, user: deploy, labels: {region: eu-1, az: eu-1a, canary: "true"}}
- {host: web-2.prod.internal, user: deploy, labels: {region: eu-1, az: eu-1a}}
- {host: web-3.prod.internal, user: deploy, labels: {region: eu-1, az: eu-1b}}
- {host: web-4.prod.internal, user: deploy, labels: {region: eu-1, az: eu-1b}}
params:
dest_dir: /opt/hello
layout: releases
ssh_key_secret: deploy-key
prepare_command: 'tar -xzf "$MANENT_ARTIFACT" -C "$MANENT_RELEASE_DIR"'
post_command: "sudo systemctl restart hello"
secrets:
- {name: deploy-key, ref: "vault:secret/data/hello/prod#ssh_key"}
rollback:
enabled: true
2. Define the waves#
The strategy lives in the same environment configuration, which is authoritative: application teams cannot loosen it from the descriptor.
rollout:
waves:
- {name: canary, select: "canary=true", bake: 10m}
- {name: rest, select: "canary!=true", by: az, bake: 5m}
maxUnavailable: 1 # one node at a time within each AZ
onFailure: rollback-all # stop | rollback-wave | rollback-all
approvalsBetween: [canary] # a person decides after the canary
health:
interval: 30s
failuresToStop: 2 # two red readings in a row stop the rollout
probes:
- {url: "http://{host}:8080/healthz"}
metrics:
- name: error-rate
query: 'sum(rate(http_requests_total{app="{application}",code=~"5..",az="{az}"}[5m])) / sum(rate(http_requests_total{app="{application}",az="{az}"}[5m])) or vector(0)'
max: 0.01
selectpicks the targets of a wave with label conditions; every target must belong to exactly one wave.by: azsplits the wave into one stage per AZ. Within a stage, targets are updated in batches ofmaxUnavailable.- After every stage the worker waits
bake, reading the gate everyinterval. The last reading must be green. probescall every target of the stage; the URL must start withhttp(s)://{host}, so probes can only reach the targets.metricsrun a PromQL query per stage, with{application},{environment},{region}and{az}substituted. A query with no data is red: a gate that sees no traffic cannot say the release is healthy.or vector(0)accepts silence explicitly.
rollback-all uses the same compensations as automatic rollback, so it requires rollback.enabled and, for remote-copy, layout: releases.
3. Connect Prometheus#
rollout:
prometheus_url: http://prometheus.monitoring:9090
alertmanager_url: http://alertmanager.monitoring:9093 # optional, for health.alerts
check_timeout: 30s
Without prometheus_url, a rollout with Prometheus metrics is rejected at submission. To use Datadog, New Relic, Elastic, CloudWatch or Azure Monitor instead, see Metrics systems.
4. Check the plan#
{"name": "canary", "hosts": ["web-1.prod.internal"]}
{"name": "rest/eu-1a", "hosts": ["web-2.prod.internal"]}
{"name": "rest/eu-1b", "hosts": ["web-3.prod.internal", "web-4.prod.internal"]}
The plan is computed and checked at submission: a wave that selects nothing, a target in two waves or a by: az on targets without labels is a 422, before anything is touched.
5. Release and follow#
manent deploy -f hello-prod.yml --no-wait
manent status <id> | jq '{state, rollout_progress, rollout: {status: .rollout.status, stage: .rollout.stage}}'
secrets,artifactsandqarun once; the steps that touch targets (deploytoactivation) run for every batch, named after the pass and the batch:deploy@canary#0.- After the canary, the gate reads probes and the metric every 30 seconds for 10 minutes. Waits are durable: if the worker restarts, the elapsed bake time is not repeated.
-
The rollout pauses:
rollout.statusispaused-approvaland the deploy shows asAWAITING_APPROVAL. Someone withdeployment.approve(not the requester) continues it: -
The
restwave proceeds AZ by AZ, with its gates. At the end the deploy isSUCCESSandrollout_progressis3/3.
The web interface shows the same thing live: stages, passes, the gate of each stage and the approval button.
6. When the gate turns red#
Suppose 1.3.0 raises the error rate in eu-1a. At the second consecutive red reading the stage fails, and the status says why:
{
"state": "ERROR",
"rollout_progress": "1/3",
"rollout": {
"status": "failed",
"stage": "rest/eu-1a",
"on_failure": "rollback-all",
"stages": [
{"name": "canary", "state": "succeeded"},
{"name": "rest/eu-1a", "state": "failed", "gate": "metric error-rate (eu-1/eu-1a): value 0.034 out of threshold"},
{"name": "rest/eu-1b", "state": "pending"}
]
}
}
With rollback-all the failed stage and the canary go back to the previous release, in reverse order; eu-1b was never touched. With stop the nodes already updated would stay on 1.3.0, and the inventory would report drift.
Going further#
| Need | Use |
|---|---|
| Check every batch, not only the whole AZ | waves[].batchBake (batch gate) |
| Different batch sizes per wave | waves[].maxUnavailable |
| Take each batch out of the load balancer | traffic.pool (Load balancer draining) |
| Regions with separate networks | dbos.region and dbos.region_routing (running in the regions) |
| A freeze on one region only | regions in the freeze window: the rollout pauses before that region |
| Shift a percentage of traffic instead of nodes | strategy: traffic (Percentage rollouts), with statistical canary analysis |
What you learned#
- Topology is expressed with labels, and a rollout is waves → stages → passes → batches.
- A health gate combines probes, metrics and alerts, and treats no data as red.
- Approvals and freezes can stop a rollout between waves.
onFailuredecides what happens to the stages already released.
Next#
Tutorial 6 releases from a CI pipeline.