Leer en español · Lire en français · Auf Deutsch lesen
Section 10.5 of 10.8 · Intermediate · 22 min read
CI/CD pipelines and deployment strategies
Video lesson
5 min · narrated, with captionsListen only (audio)
Read the transcript
How quickly and safely can your team change anything? The deployment pipeline decides. In this lesson you'll follow a change from commit to production, and learn the strategies that limit the harm a bad version can do.
Think of a bakery chain launching a new loaf. It bakes one test batch, and checks it in its own kitchen. Then it sells it in one shop for a morning, before sending it to every shop. If customers complain, only one shop was affected, and the old loaf is still on the shelves. A pipeline is that routine, automated: each change is built once, checked in stages, tried on a few users, and spread only when the evidence is good. And a separate switch decides when customers see a new feature.
The deployment pipeline, described by Jez Humble and David Farley in 2010, runs every commit through automated stages, fastest first. The commit stage compiles, runs unit tests and checks, and should take under ten minutes. The package stage builds one container image, with a software bill of materials, signs it and pushes it to the artefact repository, once. Acceptance tests, staging and production then all deploy exactly that same image. A change moves right only if the previous stage passed. This is build once, deploy many. Only configuration differs between environments. If you rebuild for production, the thing you tested is not the thing you ship. And any failure stops the line and goes straight back to the author, while the change is still fresh in their mind.
A deployment strategy decides how many users a bad version can hurt before it's stopped. Recreate stops all old instances, then starts the new ones: simple, but with downtime, and everyone meets a bad version at once. Rolling replaces instances a few at a time, and is Kubernetes' default. Rolling back takes as long as the rollout. Blue-green runs a full new environment beside the live one, tests it, then switches all traffic at the router. Rolling back is fast, but you need up to double the capacity during the switch. A canary sends a small share of traffic, say one to five per cent, to the new version, compares its metrics, then widens in steps. A shadow copies real requests to the new version but never returns its answers, so side effects such as payments must be suppressed. Whenever two versions run at once, they must be compatible with each other's data and messages.
Here's a canary with automated analysis. Users reach a traffic router: a load balancer or a service mesh. Ninety per cent of traffic goes to the production fleet, running version one. Five per cent goes to a fresh baseline of version one, and five per cent to the canary, version two. Comparing with a fresh baseline removes differences caused only by warm caches and long uptime. An analysis step compares errors, latency and saturation, then widens the canary to twenty five per cent, or rolls it back. The system decides, not a person watching dashboards. That's progressive delivery.
Deployment installs a new version in an environment. Release makes a behaviour available to users. Feature flags separate them: the code is deployed switched off, and released later, gradually and reversibly. Every flag has a carrying cost, so give flags expiry dates and delete them once settled. And because data can't be rolled back like code, schema changes must be backward compatible: expand first, and contract only in a later release.
Let's put it together. The order service is changing how delivery fees are calculated. The new calculator sits behind a release flag, switched off, and an expand-only migration adds a new column. The change is merged at two minutes past ten. Lint, unit tests and scans take six minutes, and one signed image is pushed at nine minutes past. The additive migration runs as its own step at a quarter past ten. The running version ignores the new column, so nothing breaks. Then the same image passes the staging tests. At ten thirty five, five per cent of production traffic goes to the new version, with the flag still off. Twenty minutes of analysis against a fresh baseline: pass. Traffic moves to twenty five, fifty and a hundred per cent in fifteen minute steps, finishing at eleven forty. The deployment is complete, yet no customer has seen the new fee. The next day, the product manager turns the flag on for ten per cent of customers in one city, then widens it to all cities over a week. Deployment risk and release risk were handled separately, each with a small blast radius.
The full section covers trunk-based development, the four kinds of feature flag, and when to roll back or roll forward. Next, in Section ten point six, we look at running across regions. See you there.
What you need to know
- A deployment pipeline (Humble and Farley, 2010) runs every commit through automated stages, fastest first, and a failure at any stage stops that change and notifies its author.
- Build once, deploy many: package each change into one immutable artefact, store it in an artefact repository and promote that same artefact through every environment, changing only configuration.
- Trunk-based development keeps branches short-lived and changes small, which makes continuous integration real and every change easier to review, test and undo.
- Recreate, rolling, blue-green, canary and shadow deployments trade extra infrastructure and complexity for less user exposure and faster rollback; all but recreate require old and new versions to be compatible.
- Feature flags separate deployment from release; Hodgson's categories are release, experiment, ops and permissioning toggles, and every flag carries a cost until it is removed.
- Database changes must be backward-compatible (expand and contract) and run as separate pipeline steps, because data cannot be rolled back like code.
- Progressive delivery automates canary decisions by comparing the canary statistically with a fresh baseline; the default response to a bad release is to roll back first and diagnose later.
From commit to production: what a pipeline is for
Section 0.8 (how software gets built) introduced continuous integration (every change is merged and verified automatically), continuous delivery (the main branch is always releasable) and continuous deployment (every change that passes the checks goes to production without a human decision). This section treats them as architecture: the design of the path a change takes, and the strategies that move it into production without hurting users.
The central idea is the deployment pipeline, described by Jez Humble and David Farley in Continuous Delivery (2010). Every commit triggers an automated sequence of stages. Early stages are fast and cheap and catch most problems; later stages are slower, more realistic and more expensive. A change moves to the next stage only if the previous stage passed, so the further a build gets, the more confidence it has earned. A failure anywhere stops that build and goes straight back to the person who made the change, while it is still fresh in their mind.
The pipeline matters to architects for three reasons. First, it is where deployability, a quality attribute from section 2.6, is actually achieved or lost. Second, its speed bounds how quickly the organisation can respond to a market change or a security vulnerability: if a one-line fix takes two days to reach production, the architecture is two days slow, whatever the code looks like. Third, the shape of the system constrains the shape of the pipeline: independently deployable services can have independent pipelines, but a distributed monolith forces coordinated releases however clever the tooling.
The stages of a typical pipeline
What each stage does and how long it should take
- Commit stage (target: under 10 minutes)Compile, run unit tests, linters, static analysis and dependency vulnerability scans. This is the stage developers wait for, so it must be fast; slow tests belong later.
- PackageBuild the deployable artefact (usually a container image), generate a software bill of materials (SBOM), sign it and push it to the artefact repository. Supply-chain controls are covered in section 8.6.
- Acceptance and integrationDeploy the artefact to a temporary or shared test environment and run API tests, consumer-driven contract tests and a few end-to-end journeys. Performance and security tests may run here in parallel or on a schedule.
- StagingDeploy to an environment configured like production, apply database migrations, run smoke tests. Its value depends on how closely it matches production in configuration, data shape and dependencies.
- ProductionDeploy with a strategy that limits exposure (rolling, blue-green or canary), verify with real metrics against service level objectives, then complete or roll back automatically.
Build once, deploy many, and the artefact repository
A rule from Continuous Delivery that is still broken surprisingly often is build once, deploy many: compile and package each change exactly once, then promote that same artefact through every environment. If a team rebuilds for staging and again for production, the thing it tested is not the thing it shipped. A different compiler patch, a dependency resolved to a newer version overnight or a changed base image can make the two builds differ, and the tests prove nothing about the second one.
What differs between environments is configuration, supplied at deployment or start-up time (database addresses, credentials, feature-flag settings), never baked into the artefact. This is the configuration factor of the twelve-factor app. A container image tagged orders:4f1c2ab (the commit hash) is deployed to test, staging and production; only its environment variables and mounted secrets change.
The artefact repository is the store between build and deploy: a container registry (Amazon ECR, Azure Container Registry, Google Artifact Registry, GitHub Container Registry, Harbor) or a universal repository such as JFrog Artifactory or Sonatype Nexus for packages and binaries. Treat it as critical infrastructure. Deploy by immutable digest (sha256:...) rather than by a mutable tag such as latest, which can be moved to point at a different image; keep artefacts that are or were in production for as long as you might need to roll back to them or investigate them; and record provenance, meaning which commit, pipeline run and dependencies produced each artefact.
Trunk-based development and small batches
How code is branched decides how often it is really integrated. In trunk-based development everyone integrates small changes into one main branch (the trunk) at least daily, either by committing directly or through short-lived branches and pull requests that live for hours, not weeks. The DORA research programme has repeatedly associated short-lived branches and frequent merging with higher delivery performance, and it is a precondition for continuous integration in the literal sense: code on a branch for three weeks is not being integrated continuously.
Long-lived feature branches feel safer because unfinished work is kept apart, but the risk is merely postponed: the longer two branches diverge, the larger and riskier the eventual merge, and the longer the pipeline sees code that is not the code that will ship. GitFlow, a branching model with long-lived develop and release branches published by Vincent Driessen in 2010, was widely adopted; in 2020 Driessen added a note that teams delivering continuously, such as web applications, should consider a simpler workflow. Teams that do need to work on large changes in the trunk use two techniques: feature flags, which hide unfinished behaviour, and branch by abstraction, which introduces an interface in front of the old implementation, builds the new one behind it and switches over when ready.
Practitioners genuinely disagree at the edges. Open-source projects with many untrusted contributors rely on longer-lived pull requests and review, and regulated teams sometimes need explicit release branches for audit. The principle that survives the disagreement is the batch size: the smaller each change, the easier it is to review, test, deploy, diagnose and undo.
name: delivery
on:
push:
branches: [main]
env:
IMAGE: registry.example.com/food/orders:${{ github.sha }}
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: make lint test # fast feedback first
- run: docker build -t "$IMAGE" . && docker push "$IMAGE"
staging:
needs: build
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh staging "$IMAGE" && ./smoke-test.sh staging
production:
needs: staging
runs-on: ubuntu-latest
environment: production # can require an approval
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh production "$IMAGE" --canary 5Deployment strategies
A deployment strategy decides how the new version replaces the old one in production, and therefore how many users a bad version can hurt before it is stopped. The strategies differ in the extra infrastructure they need, how fast they roll back and whether two versions run at once. Whenever two versions run at once, they must be compatible with each other's data and messages, which is the price of every strategy except recreate.
Deployment strategies compared
| Strategy | How it works | Risk to users | Rollback | Extra cost and complexity |
|---|---|---|---|---|
| Recreate | Stop all old instances, then start the new ones. | Downtime during the switch; every user meets a bad version at once. | Redeploy the old version, with more downtime. | None. Acceptable for internal tools, batch jobs and maintenance windows. |
| Rolling | Replace instances a few at a time (Kubernetes' default for Deployments, with maxSurge and maxUnavailable both defaulting to 25%). | Exposure grows as the rollout proceeds; health checks catch crashes but not subtle errors. | Roll back by rolling the old version out again, which takes as long as the rollout. | Low. Old and new versions run side by side, so they must be compatible. |
| Blue-green | Run a full new environment (green) beside the live one (blue), test it, then switch all traffic at the router. | All users move at once, but the new environment was verified before the switch. | Fast: switch the router back, while blue is still running. | Up to double capacity during the switch; shared databases must work with both versions. |
| Canary | Send a small share of traffic (say 1-5%) to the new version, compare its metrics, then widen in steps. | Only the canary's share of users meets a bad version, and only until analysis stops it. | Fast: set the canary's weight to zero. | Needs weighted routing, good metrics and enough traffic for a meaningful comparison. |
| Shadow (dark launch) | Copy real requests to the new version; its responses are compared or discarded, never returned. | None directly, provided side effects (payments, emails, writes) are suppressed or faked. | Nothing to roll back; users never depended on it. | Double load on the shadowed path and careful handling of side effects; best for read paths and rewrites. |
Blue-green deployment was described by Martin Fowler in 2010, crediting Dan North and Jez Humble; the canary release, named after the canaries miners carried to detect gas, was written up by Danilo Sato on the same site in 2014. In Kubernetes, rolling updates are built in; blue-green and canary releases usually come from a progressive delivery controller (Argo Rollouts, Flagger) or a service mesh and the Gateway API's weighted routing. On managed platforms, the equivalent features are called traffic splitting or deployment slots.
Canary release with automated analysis
Separating deployment from release with feature flags
Deployment installs a new version of the software in an environment; release makes a behaviour available to users. Treating them as one event means every deployment is a business event, with all the nervousness that brings. Feature flags (feature toggles) separate them: the new code is deployed switched off, and the behaviour is released later, gradually and reversibly, by changing configuration. Pete Hodgson's article Feature Toggles (aka Feature Flags) on martinfowler.com (2017) distinguishes four categories by how long they live and how dynamically they decide.
Hodgson's four categories of feature toggle
| Category | Purpose | Typical lifetime | How dynamic |
|---|---|---|---|
| Release toggles | Ship incomplete or unreleased features in the trunk, switched off. | Days to a few weeks; remove after release. | Mostly static per release. |
| Experiment toggles | A/B or multivariate tests that route each user to a cohort. | Long enough for statistically sound results, then removed. | Per request, by user cohort. |
| Ops toggles | Operational controls, such as switching off an expensive recommendation panel under load (kill switches). | Mostly short; a few kill switches are kept long-term. | Changed at run time, often during an incident. |
| Permissioning toggles | Features for particular users: premium tiers, beta testers, internal staff. | Can live for years. | Per request, by user. |
Flags have a carrying cost. Every flag doubles the paths through the code it guards, adds test combinations and leaves dead code once its decision is settled. Hodgson recommends treating flags as inventory: add a removal task when a release toggle is created, give flags expiry dates, add tests that fail once a flag is past its date, and limit how many may exist at once. The Knight Capital loss of 2012, described in section 1.3, involved a repurposed flag that reactivated old code on a server missed by a manual deployment: a reminder never to reuse flag names and to delete dead code behind settled flags. At scale, flags are served by a dedicated service (open-source options such as Unleash and Flagsmith, commercial ones such as LaunchDarkly, or the vendor-neutral OpenFeature API) so that a change propagates in seconds and is audited.
Database changes in the pipeline
Code can be rolled back in seconds; data cannot. A deployment that renames a column and is then rolled back leaves the old code facing a schema it does not understand. The answer, covered in depth in schema evolution, is to make every schema change backward-compatible with the version of the code currently running, using expand and contract: add the new structure first, deploy code that writes both and reads either, backfill, switch readers, and remove the old structure only in a later release once nothing uses it.
In the pipeline this means migrations are versioned artefacts that run as their own step, before the code that needs them is deployed, and are tested on a production-sized copy of the data so that a lock on a large table is discovered in staging rather than at lunchtime. Destructive steps (dropping columns or tables) are delayed and run separately, because they are the one kind of change a rollback cannot undo. A useful pipeline check fails the build when a migration is not additive unless it is explicitly marked as a contract step.
Progressive delivery and automated canary analysis
Progressive delivery, a term popularised by James Governor of the analyst firm RedMonk in 2018, combines canaries, feature flags and automated verification: a change reaches progressively larger audiences, and at each step the system, not a person watching dashboards, decides whether to continue. The decision uses automated canary analysis: the canary's metrics (error rate, latency percentiles, saturation, business signals such as orders placed) are compared statistically with a baseline. Netflix and Google open-sourced their analysis engine, Kayenta, in 2018; Argo Rollouts and Flagger provide similar analysis steps in Kubernetes.
Three details decide whether the analysis can be trusted. Compare like with like: a fresh baseline of the old version receiving the same share of traffic avoids blaming the new version for differences caused by cold caches or recent restarts. Have enough traffic: at 5% of a service handling ten requests a minute, an hour of canary sees thirty requests, too few to detect a 1% error rate, so low-traffic services need longer steps, synthetic load or a different strategy. Choose metrics tied to the service level objectives and to business outcomes, because a version can be fast and error-free while silently charging the wrong delivery fee.
Rollback or roll forward
When a release goes wrong, there are two ways out. Rollback returns to the previous artefact, which is fast and needs no new code, provided the previous version still works with the current data and with the versions of other services. Roll forward ships a fix through the pipeline, which is slower but sometimes the only option, for example after a data migration that cannot be reversed or a change that other services already depend on.
A sound default is roll back first, diagnose later
: restore service with the fastest safe action, then investigate without pressure. Rolling forward under pressure tends to bypass the pipeline's checks, and a rushed fix to a production incident is a classic source of the next incident. Rollback is only a real option if it is designed in: previous artefacts kept, schema changes compatible in both directions for at least one version, messages readable by old and new consumers, and the rollback path itself exercised regularly. A team that has never rolled back should assume it cannot.
Worked example
Worked example: releasing a new delivery-fee rule
The food-delivery platform's order service is changing how delivery fees are calculated, using distance and demand instead of a flat fee. The team uses trunk-based development, a canary deployment and a release flag.
| Change | New fee calculator behind a release flag dynamic-delivery-fee, off by default; an expand-only migration adds a nullable column fee_rule_version. |
|---|---|
| Commit to artefact | Merged to main at 10:02. Lint, unit tests and scans take 6 minutes; image orders:4f1c2ab with its SBOM and signature is pushed at 10:09. |
| Migration | The additive migration runs at 10:15 as its own pipeline step. The running version ignores the new column, so nothing breaks. |
| Staging | The same image is deployed at 10:16; contract tests with the payment and restaurant services and smoke tests pass by 10:31. |
| Canary | At 10:35, 5% of production traffic goes to the new version, still with the flag off. Analysis over 20 minutes compares p99 latency, 5xx rate and order success with a fresh baseline: pass. |
| Rollout | Weights move to 25%, 50% and 100% at 15-minute steps; complete at 11:40. The deployment is finished, yet no customer has seen the new fee. |
| Release | Next day the product manager enables the flag for 10% of customers in one city and watches order conversion and complaints, then widens to all cities over a week. |
| Clean-up | The flag and the flat-fee code are deleted the following sprint; a later contract migration makes the column mandatory. |
Deployment risk (does the code work?) and release risk (do customers like the new fees?) were handled separately, each with its own small blast radius.
At every step until the contract migration, recovery was a configuration change or a router weight, not an emergency fix. Commit to full production deployment took about 1 hour 40 minutes.
Measuring the pipeline
The DORA software delivery metrics introduced in section 2.6 (change lead time, deployment frequency, change fail rate, failed deployment recovery time and deployment rework rate) are largely properties of the pipeline and the architecture together. Small batches and trunk-based development shorten lead time and raise frequency; automated testing and canary analysis lower the change fail rate; fast rollback and flags shorten recovery time. Measure them per service, watch trends rather than single values, and never turn them into targets for individuals, which simply invites gaming.
Check your understanding
Why is rebuilding the artefact separately for production a problem even if the source code is identical?
Dependencies, base images or build tools may have changed between builds, so the production artefact is not the one that was tested; build once and promote the same artefact.
What is the difference between deploying a feature and releasing it?
Deployment installs the code in an environment; release makes the behaviour available to users. Feature flags let the code be deployed switched off and released later, gradually and reversibly.
Why does a canary on a low-traffic service often give unreliable results?
A small share of little traffic yields too few requests to detect differences in error rate or latency with statistical confidence, so steps must be longer, traffic synthetic or another strategy chosen.
When might rolling forward be the only option?
When the release included an irreversible change, such as a destructive migration, or other services already depend on the new behaviour, so the previous version can no longer run correctly.
Sources and further reading
Have a question about this section? ArchitectBot, the course’s AI study assistant, answers from the course first.
Ask: “Our team deploys every two weeks with a manual checklist. Which pipeline and deployment changes should we make first to deploy safely several times a day?”