DevOps · 08

Deployment Strategies

Shipping a change is a risk decision. This page compares recreate, rolling, blue/green, canary, shadow and A/B releases, shows how feature flags separate deploy from release, and explains the part that causes real outages: changing databases, APIs and messages while two versions run at once. It ends with progressive-delivery tooling, a release checklist and the rollback playbook.

11
Chapters
7
Strategies
5
Diagrams
Expand/contract
DB pattern
01
Chapter 1 - The Problem

Why Deployment Strategy Matters, and Deploy vs Release

Replacing running software with a new version is the riskiest routine thing a team does. The strategy you choose decides how many users a bad version can hurt, how long they are hurt, how much extra capacity you pay for, and how quickly you can undo it. A good strategy turns "we shipped and hoped" into "we shipped to 1 percent, watched the numbers, and promoted".

The first idea to internalise is the split between two events that most teams treat as one:

DeployRelease
MeaningA new version of the code is running in productionUsers are exposed to the new behaviour
Controlled byPipeline, orchestrator (Kubernetes, ECS, CodeDeploy)Traffic routing, or a feature flag
Typical riskCrash on start, bad config, missing dependency, slow startWrong behaviour, performance regression, data issue
Undo byRoll back to the previous versionTurn the flag off or shift traffic back
Deploy is not release: code ships dark, then a flag exposes it
Without a flag: deploy = release. Every user gets version 2 the moment the pods change. deploy v2 100% of users see v2 With a flag: deploy and release are separate decisions, made at separate times. deploy v2 (flag OFF) smoke tests on prod flag ON for staff, then 1%, 10%, 100% problem? flag OFF (seconds, no deploy) Deploy risk: does the new binary start, stay healthy, talk to its dependencies? Release risk: does the new behaviour work for real users and real data? Splitting them lets you make each risk small and each rollback fast.

Every strategy in this page is a different answer to the same four questions. Keep them in mind as you read:

  • Exposure: what fraction of users or requests meets the new version first?
  • Capacity: do old and new versions run side by side, and does that need extra servers?
  • Compatibility: can old and new versions run at the same time against the same database and the same messages? (Chapters 6 and 7: this is where most real outages come from.)
  • Reversal: how long and how risky is it to go back?
Key insight: Changing a tyre on a moving car is not an option, but changing it one wheel at a time while the car stays on the road is what rolling, canary and blue/green all do in different ways. The database is the axle shared by every wheel: you cannot swap it out in the middle of a ride, only evolve it in compatible steps.

This page builds on page 05 CI/CD Pipelines (the pipeline that triggers deployments), page 03 Kubernetes Core and page 04 Kubernetes Operations (the primitives), and page 07 Observability (the metrics that tell you whether a rollout is healthy).

02
Chapter 2 - The Basics

Recreate and Rolling Update (and Kubernetes Deployments)

Recreate

Stop everything that runs the old version, then start the new version. It is the simplest strategy and the only honest one when two versions cannot coexist: a single-instance application, a schema change that breaks the old code, a licence that allows one instance. The price is downtime equal to shutdown plus startup plus warm-up. Kubernetes supports it as strategy.type: Recreate: all old pods are terminated before any new pod is created.

Rolling update

Replace instances gradually: start some new ones, wait until they are healthy, stop some old ones, repeat. No downtime and no extra environment, but for a while both versions serve traffic, so they must be compatible with each other and with the data. A rolling update has no built-in notion of "watch the error rate": its safety check is only the health probe.

Recreate vs rolling update: what the replica count looks like over time
Recreate: stop all old, then start all new. Downtime in the middle. v1 v1 v1 v1 stop all no pods: DOWNTIME start new v2 v2 v2 v2 Rolling update (maxSurge 1, maxUnavailable 0): replace one at a time, never fewer than 4 ready start v1 v1 v1 v1 surge +1 v1 v1 v1 v1 v2 extra capacity during the surge old -1 v1 v1 v1 v2 surge +1 v1 v1 v1 v2 v2 extra capacity during the surge old -1 v1 v1 v2 v2 done v2 v2 v2 v2 A new pod counts only when its readiness probe passes. A bad version stalls the rollout, not the service.
Kubernetes Deployment strategies

A Kubernetes Deployment supports exactly two strategy.type values: RollingUpdate (the default) and Recreate. For RollingUpdate, two fields set the pace. Per the Kubernetes documentation both default to 25 percent, and each can be an absolute number or a percentage:

FieldMeaningEffect
maxSurgeHow many pods above the desired count may exist during the updateHigher = faster rollout, more temporary capacity needed
maxUnavailableHow many pods below the desired count may be unavailable0 keeps full capacity; needs maxSurge above 0 or the rollout cannot start
minReadySecondsA new pod must be ready this long before it counts as availableCatches pods that pass the probe and then crash
progressDeadlineSecondsHow long to wait for progress before the Deployment is marked as failedDoes not roll back automatically; it sets a condition (ProgressDeadlineExceeded)
k8s-deployment.yml - a conservative rolling update (parsed as valid YAML; not applied to a cluster)
apiVersion: apps/v1 kind: Deployment metadata: name: shop spec: replicas: 4 minReadySeconds: 10 progressDeadlineSeconds: 300 strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 maxUnavailable: 0 selector: matchLabels: app: shop template: metadata: labels: app: shop spec: terminationGracePeriodSeconds: 30 containers: - name: shop image: registry.example.com/shop:1.4.0 ports: - containerPort: 8080 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 periodSeconds: 5 failureThreshold: 3 lifecycle: preStop: exec: command: ["sleep", "10"]

The /actuator/health/readiness path is the Spring Boot readiness probe group, available when probes are enabled (they are enabled automatically when Boot detects Kubernetes; management.endpoint.health.probes.enabled=true forces it). The preStop sleep gives the endpoints controller and load balancers time to stop sending new connections before the process gets SIGTERM, which is a standard fix for errors during rollouts.

Shell - drive and inspect a rollout
$ kubectl set image deployment/shop shop=registry.example.com/shop:1.5.0 $ kubectl rollout status deployment/shop # blocks until done or failed $ kubectl rollout history deployment/shop $ kubectl rollout undo deployment/shop # back to the previous ReplicaSet $ kubectl rollout pause deployment/shop # freeze mid-way (poor man's canary) $ kubectl rollout resume deployment/shop
⚠ Rolling update with no readiness probe
  • What you see: the rollout finishes in seconds and users get connection errors or 503s. Without a readiness probe a pod is "ready" as soon as its container starts, long before Spring has finished starting, so traffic goes to pods that cannot serve it, and the old pods are already gone. Fix: a readiness probe that checks the application (not just the port), plus minReadySeconds, plus a graceful shutdown (server.shutdown=graceful in Spring Boot).
A rolling update is not a canary
  • A rolling update shifts traffic in proportion to pod counts, as fast as probes pass, and nothing looks at error rates. With 4 replicas the first new pod receives about 25 percent of traffic almost immediately. If you want "1 percent, watch, then more", you need weighted routing (chapter 4), not a plain Deployment.
03
Chapter 3 - Strategies

Blue/Green Deployment

Run two complete production-like environments: blue (the current version, serving users) and green (the new version, idle for users). Deploy to green, test it with real infrastructure and synthetic or internal traffic, then switch the router so green becomes live. Blue stays up for a while as the instant rollback target, then is retired or becomes the next green.

Blue/green: two full environments, one router decides
Users Router LB / Service / DNS BLUE (v1, live) v1 pod v1 pod v1 pod GREEN (v2, idle / tested) v2 pod v2 pod v2 pod 100% 0% (test only) Cutover = flip the router. Rollback = flip it back (blue is still running). Cost: double capacity during the window.
AspectBlue/green
ExposureAll users at once at cutover (unless combined with a gradual weight shift)
DowntimeNone when the router flips cleanly and sessions/state are handled
Rollback speedFastest of all strategies: flip the router back
Capacity costUp to 2x for the cutover window (or permanently if blue is kept warm)
Pre-release testingExcellent: green runs on production infrastructure before any user sees it
Hard partThe shared database and any state: both colours usually use the same data store, so schema and messages must stay compatible (chapters 6 and 7)
How the switch is done
PlatformRouter flip
KubernetesTwo Deployments (shop-blue, shop-green) and a Service whose selector you change; or an Ingress/Gateway backend swap; Argo Rollouts automates this with activeService and previewService
AWS ALBTwo target groups; change the listener rule's forward action to the other target group (chapter 8)
AWS CodeDeployBlue/green deployment type for EC2/ASG and ECS: it provisions the replacement, shifts traffic, then terminates the original after a wait
DNSChange a weighted or simple record: slow and unreliable because of TTLs and client caches; avoid as the only mechanism
⚠ Blue/green with an incompatible schema
  • What you see: after cutover green works, but the moment you flip back to blue it fails because the migration that green needed already ran. The "instant rollback" is a lie. Fix: make every schema change backward compatible with the previous application version (expand/contract, chapter 6), so either colour can run against the current schema.
Warm the green environment
  • A cold green (empty caches, cold JIT, empty connection pools) can look slow or fail health checks under real load at cutover. Send it synthetic or mirrored traffic first, or shift traffic in steps. Argo Rollouts calls the pre-cutover phase the preview, and supports prePromotionAnalysis for automated checks before the switch.
04
Chapter 4 - Strategies

Canary Releases, and Shadow (Mirrored) Traffic

Canary

Named after the canary in the coal mine: send a small share of real traffic to the new version, compare its health with the old version, and only then increase the share. If metrics degrade, the share drops back to zero and only a few users noticed. The blast radius is chosen by you (1 percent, 5 percent) instead of by the size of the fleet.

Canary: shift a small, growing share of traffic and let metrics gate each step
step 1: 1% to v2 1% v2 / 99% v1 gate: error ratio, latency vs baseline step 2: 10% to v2 10% v2 / 90% v1 gate: error ratio, latency vs baseline step 3: 50% to v2 50% v2 / 50% v1 gate: error ratio, latency vs baseline step 4: 100% to v2 100% v2 / 0% v1 Gate fails at any step: weight back to 0%, v1 untouched, alert someone Gates pass at every step: promote, scale v1 down The key requirement is comparison: canary metrics are judged against the baseline (v1) at the same time, not against a number you made up, because traffic patterns change through the day. Needs weighted routing: service mesh, ingress controller, ALB weighted target groups, or Argo Rollouts / Flagger.
AspectCanary
ExposureSmall and growing; you pick the steps (for example 1, 10, 50, 100 percent)
DowntimeNone
Rollback speedFast: set the weight back to 0 (and the old version never stopped)
Capacity costSmall: roughly the canary share plus headroom
NeedsFine-grained traffic split and good metrics. Without observability a canary is just a slow rollout
Hard partEnough traffic for the comparison to mean anything; sticky sessions; both versions on the same data
What a canary gate should check
  • Errors: HTTP 5xx ratio of the canary against the baseline (page 07: RED metrics and the PromQL error ratio).
  • Latency: p95/p99 of the canary against the baseline, from a histogram.
  • Saturation: CPU, memory, GC pauses, connection pools of the canary pods.
  • Business metrics: orders per minute, payment success, login rate. A version can be technically healthy and commercially broken.
  • Enough samples: require a minimum request count per step, or a 1 percent canary at low traffic proves nothing.
Shadow (traffic mirroring)

In a shadow deployment real requests are copied to the new version, whose responses are discarded. Users only ever see the old version, so a bug in the new one hurts nobody, but the new version is exercised with genuine production traffic shapes. It is ideal for validating a rewrite, a new database engine or a performance change. Mirroring is supported by service meshes and gateways such as Istio, and Flagger documents blue/green mirroring for Istio and Gateway API.

⚠ Mirroring requests with side effects
  • A mirrored POST /payments charges the card twice, a mirrored POST /orders writes to the real database, an email gets sent twice. The shadow must be isolated: read-only operations only, or point it at a sandbox database and stubs for payment, email and other third parties. Always ask "what does this request do?" before mirroring it.
RollingCanaryShadow
Users see the new version?Yes, progressively by pod countYes, a chosen shareNo
Compares old vs new on live traffic?NoYesYes (response diffing, latency)
Risk to usersModerateSmallNone, if side effects are isolated
Extra complexityNoneWeighted routing + metrics gatesMirroring + isolation of side effects
05
Chapter 5 - Strategies

A/B Testing and Feature Flags

A/B testing is an experiment, not a deployment safety net

A canary asks "is the new version healthy?" and routes by percentage of traffic. An A/B test asks "which version produces a better outcome?" (conversion, retention) and routes by user attributes or cohorts (a header, a cookie, a user id hash, a country), keeping each user on the same variant for the whole experiment. It needs statistical analysis, runs for days or weeks, and is owned by product as much as by engineering. The routing machinery overlaps with canaries (Flagger documents A/B testing on HTTP headers and cookies), but the goal and the success criteria differ.

CanaryA/B test
QuestionIs it safe?Is it better?
Split byPercent of requestsUser cohort, header, cookie, hashed user id
User experienceMay vary per requestMust be sticky per user
DurationMinutes to hoursDays to weeks
Decision byHealth metrics, automatedStatistics on business metrics, human
Feature flags: separating deploy from release

A feature flag (feature toggle) is a runtime switch around a code path. The code ships to production disabled; a configuration change enables it for chosen users. That is the second half of the idea in chapter 1: deploy is a technical event, release is a decision that can be made later, per user group, and undone in seconds without a deployment.

Feature flag in Spring Boot - the simplest possible form (property-backed; real systems use a flag service)
@RestController class CheckoutController { private final boolean newPricingEnabled; private final PricingV1 v1; private final PricingV2 v2; CheckoutController(@Value("${features.new-pricing:false}") boolean newPricingEnabled, PricingV1 v1, PricingV2 v2) { this.newPricingEnabled = newPricingEnabled; this.v1 = v1; this.v2 = v2; } @GetMapping("/price") Price price(@RequestParam long productId) { return newPricingEnabled ? v2.price(productId) : v1.price(productId); } }

A property that needs a restart to change is a deploy-time flag, which defeats the purpose. Dedicated flag systems (commercial products such as LaunchDarkly, open-source ones such as Unleash, or a homegrown service) evaluate flags at request time with user context, so you can enable a feature for employees, then 1 percent of users, then everyone. OpenFeature is a CNCF project that defines a vendor-neutral flag evaluation API, so application code does not depend on one vendor.

Flag typeLifetimePurposeExample
Release flagDays to weeks, then deleteHide unfinished or risky work in main (trunk-based development)new checkout flow
Experiment flagDuration of the testA/B variantstwo button designs
Ops flag / kill switchLong-livedTurn off an expensive or risky feature under loaddisable recommendations during an incident
Permission flagLong-livedEntitlements per customer or planbeta features for one tenant
⚠ Flag debt
  • Release flags that stay forever turn the code into a maze of untested combinations (n flags means up to 2 to the n paths). Give every release flag an owner and a removal date, remove the flag and the dead branch once the rollout is complete, and keep a flag inventory. An outdated flag turned on by accident can cause an outage.
  • Test both sides of the flag in CI.
  • Make the default (when the flag service is unreachable) the safe one, and cache the last known values.
  • A flag does not remove the need for compatible schemas: the flag is evaluated per request, so the code for both branches must work with the current database.
  • Log or tag the flag state on traces and metrics so you can tell which variant caused an issue.
06
Chapter 6 - Data

Database Migrations: Expand and Contract

Application code is easy to replace and easy to roll back: it is stateless, and the old version is one kubectl rollout undo away. The database is not. It is shared by every running version, it is hard to undo, and during any rolling, canary or blue/green deploy two versions of the code run against one schema at the same time. So the schema must always be compatible with both the version being replaced and the version replacing it.

The technique is expand and contract, also called parallel change: make a breaking change in three compatible stages instead of one incompatible step.

PhaseWhat changesSafe because
ExpandAdd the new structure (column, table, index) alongside the old, nullable or with a defaultOld code ignores what it does not know about
MigrateDeploy code that writes both old and new; backfill existing rows; then switch reads to the new structureEach step works with the previous app version
ContractRemove the old structure once no running version uses itNothing depends on it; the only irreversible step, done last
Expand / contract (parallel change): rename a column without downtime
Goal: customer.full_name -> customer.first_name + customer.last_name 1 Expand ADD first_name, last_name (nullable) app v1 only 2 Dual write app v2 writes BOTH old and new columns v1 and v2 run (rolling deploy) 3 Backfill copy old rows into the new columns (in batches) v2 live 4 Switch reads app v3 reads the new columns only v2 -> v3 5 Contract DROP full_name (after v2 is gone) v3 only At every step, the schema works with the application version that is running AND the one before it. Each phase is a separate deployment, often separate days. Rollback of the app is safe at every step until the contract step, which is the only destructive (irreversible) one: do it last, and only when monitoring shows nothing reads the old column any more. Never combine "rename column" and "deploy code that needs the new name" in one step: a rolling update runs old and new code against the same database at the same time.
Flyway migrations for the rename, one file per phase (SQL verified by running it against SQLite 3.45)
-- V2__expand_add_name_columns.sql -- EXPAND: purely additive, nullable, safe for the old version of the app ALTER TABLE customer ADD COLUMN first_name TEXT; ALTER TABLE customer ADD COLUMN last_name TEXT; -- V3__backfill_names.sql -- MIGRATE: copy existing data (on a big table do this in batches, outside the deploy) UPDATE customer SET first_name = substr(full_name, 1, instr(full_name, ' ') - 1), last_name = substr(full_name, instr(full_name, ' ') + 1) WHERE first_name IS NULL AND instr(full_name, ' ') > 0; -- V4__contract_drop_full_name.sql -- CONTRACT: only after no running version reads or writes full_name ALTER TABLE customer DROP COLUMN full_name;

I ran this sequence against an in-memory SQLite database: after V2 the new columns were NULL, after V3 they held Ada/Lovelace and Alan/Turing, and after V4 the old column was gone and the rows were intact. SQLite stands in only to check the statements; substr and instr are SQLite functions, so on PostgreSQL you would use split_part and similar, and other engines have their own string functions.

Flyway and Liquibase

Both tools version the schema as code, apply pending changes in order at deploy time, and record what was applied in a table so each change runs exactly once.

FlywayLiquibase
Change formatPlain SQL files named V<version>__<description>.sql, plus repeatable R__ files; also Java migrationsChangelog files in XML, YAML, JSON or SQL, made of changeSets identified by id + author + file
History tableflyway_schema_historyDATABASECHANGELOG (and DATABASECHANGELOGLOCK)
Spring BootAuto-runs at startup when flyway-core (plus the database module for your DB in recent Flyway versions) is on the classpath; spring.flyway.* propertiesAuto-runs at startup when liquibase-core is on the classpath; spring.liquibase.* properties
RollbackUndo migrations are a paid-edition feature; the common practice is roll forward with a new migrationSupports rollback tags and rollback blocks per changeSet
StrengthsSimple, SQL-firstDatabase-agnostic abstractions, preconditions, contexts and labels
⚠ Running migrations at application startup on every replica
  • With 20 pods starting at once, 20 instances race to migrate. The tools take a lock so only one runs, but the others block, startup times balloon, and a long migration makes Kubernetes kill pods for failing probes. Safer: run migrations as a separate pipeline step or Kubernetes Job before rolling out the new version (the expand step is compatible with the old version, so it can go first), and keep long data backfills out of the deploy path entirely.
OperationRisk on a big, busy tableSafer approach
Add a nullable columnLowFine. On PostgreSQL 11 and later adding a column with a constant default is also cheap
Add NOT NULL to an existing columnFull scan and lock while checkingAdd a check constraint as not-validated, backfill, validate, then set NOT NULL (database-specific; read your DB docs)
Create an indexBlocks writes on some enginesUse the online option (CREATE INDEX CONCURRENTLY on PostgreSQL, which cannot run inside a transaction)
Rename or drop a columnBreaks every running old version immediatelyExpand/contract: add new, dual write, migrate, remove last
Change a column typeRewrites the table, long lockAdd a new column of the new type, copy in batches, switch, drop old
Backfill millions of rows in one statementLong transaction, replication lag, lock contentionBatches of a few thousand rows with pauses, as a background job
07
Chapter 7 - Compatibility

Backward-Compatible APIs and Messages; Rollback vs Roll-Forward

During any gradual deploy, clients and servers of different versions talk to each other. A caller on v1 hits a server on v2, a v2 service calls a v1 service, a message written by v2 is read by a consumer still on v1. The rules for staying safe are old and consistent.

HTTP APIs
ChangeBackward compatible?Note
Add an optional response fieldYesClients must ignore unknown fields (Spring Boot configures Jackson to not fail on unknown properties by default)
Add an optional request field/parameterYesServer must tolerate its absence
Add a required request fieldNoOld clients fail validation. Make it optional with a default first
Remove or rename a response fieldNoAdd the new one, keep both, deprecate, remove after all consumers moved
Change a field's type or meaningNoNew field name instead
Tighten validationNoCan reject requests that used to succeed
Add an enum valueRiskyClients with exhaustive switches break; document an "unknown" fallback

Follow the robustness principle in clients (be liberal in what you accept) and treat the contract as additive-only. Deploy order matters when a contract does change: for a new optional capability deploy the provider first, then the consumers; when removing, stop the consumers from using it first, then remove it from the provider.

Messages and events

Messages are harder than HTTP because they outlive the deployment: events written by v2 sit in a Kafka topic or queue and are read later, possibly by v1, possibly in a replay months later. Use a schema format with explicit evolution rules (Avro, Protobuf, JSON Schema with a registry) and check compatibility in the build. In schema-registry terms: backward compatible means new readers can read old data (add optional fields with defaults); forward compatible means old readers can read new data; full means both. When consumers are upgraded before producers, backward compatibility is enough; when producers may be upgraded first, forward compatibility is needed; when you cannot control the order, require full. See page 07 Event-Driven Patterns and page 08 Reliability & Operations in the Messaging section.

⚠ Consumers that fail hard on unknown fields or unknown event types
  • What you see: after a producer deploy, a consumer enters a retry loop or a dead-letter queue floods. Fix: tolerant readers (ignore unknown fields, log and skip unknown event types), and deploy consumers that understand the new format before any producer emits it.
Rollback or roll forward?
Roll backRoll forward
WhatRedeploy the previous known-good versionFix the problem and deploy a new version
SpeedFast if the previous version is still around (blue/green, canary weight to 0, rollout undo)Bounded by how fast you can write, review and ship a fix
Works whenThe change is compatible with the old version: no incompatible schema or message changes happenedData has already moved on, an irreversible migration ran, or the bug is tiny and a fix is quicker
RiskOld version meets data written by the new versionShipping an untested fix under pressure
  • Default to rollback for application changes: it is the fastest way to stop the bleeding, and with expand/contract it is always available until the contract step.
  • Plan for roll-forward when data is involved: destructive migrations and one-way data conversions cannot be undone by redeploying code.
  • Whichever you choose, decide before the rollout (the release checklist in chapter 9 asks for it) and rehearse it. Deciding under incident pressure is how mistakes happen.
  • Keep the previous artefact and its configuration available, and keep build artefacts immutable: roll back to the same image digest you ran before, not "rebuild v1".
08
Chapter 8 - Tooling

Progressive Delivery: Argo Rollouts, Flagger, and AWS Options

Progressive delivery automates canary and blue/green: a controller shifts traffic in steps, queries your metrics at each step, and promotes or aborts without a human watching. Two widely used Kubernetes controllers do this.

Argo RolloutsFlagger
Part ofArgo projectFlux project (CNCF)
How you use itReplace Deployment with a Rollout resource (argoproj.io/v1alpha1), or reference an existing DeploymentKeep your Deployment; add a Canary resource (flagger.app/v1beta1) that targets it
StrategiesCanary (with setWeight, pause, analysis steps) and BlueGreen (activeService, previewService, autoPromotionEnabled)Canary releases, A/B testing (headers and cookies), blue/green (traffic switching), blue/green mirroring
Traffic shapingPod-ratio weights on its own; precise weights through ingress or mesh integrationsNeeds a supported mesh, ingress or Gateway API implementation for weighted canaries; its Kubernetes CNI option supports blue/green only (see the Flagger deployment-strategies table for the current matrix)
Metric gatesAnalysisTemplate / AnalysisRun with providers such as Prometheusmetrics in the Canary spec: built-in request-success-rate and request-duration, plus custom Prometheus queries and webhooks
CLI / UIkubectl argo rollouts plugin and dashboardWorks with Flux and Grafana dashboards
rollout-canary.yml - Argo Rollouts canary with a metric gate (YAML parsed; not applied to a cluster)
apiVersion: argoproj.io/v1alpha1 kind: Rollout metadata: name: shop spec: replicas: 5 selector: matchLabels: app: shop template: metadata: labels: app: shop spec: containers: - name: shop image: registry.example.com/shop:1.4.0 ports: - containerPort: 8080 strategy: canary: steps: - setWeight: 10 - pause: {duration: 5m} - analysis: templates: - templateName: success-rate - setWeight: 50 - pause: {duration: 10m} - setWeight: 100
analysis-template.yml - the gate: success ratio from Prometheus must stay at 99 percent or above
apiVersion: argoproj.io/v1alpha1 kind: AnalysisTemplate metadata: name: success-rate spec: metrics: - name: success-rate interval: 1m count: 5 successCondition: result[0] >= 0.99 failureLimit: 1 provider: prometheus: address: http://prometheus.monitoring:9090 query: | sum(rate(http_server_requests_seconds_count{app="shop",status!~"5.."}[2m])) / sum(rate(http_server_requests_seconds_count{app="shop"}[2m]))

Read the steps top to bottom: send 10 percent of traffic to the new version, wait five minutes, run the analysis (five measurements one minute apart, tolerating at most one failure), then go to 50 percent, wait ten minutes, then 100 percent. If the analysis fails, the controller aborts and returns traffic to the stable version. The result[0] syntax is how Argo Rollouts reads the first element of a Prometheus vector result. The query is the same error-ratio pattern from page 07, parameterised to the application label. Without a traffic-routing integration Argo Rollouts approximates the weights using the ratio of canary to stable pods.

flagger-canary.yml - the equivalent in Flagger (YAML parsed; not applied)
apiVersion: flagger.app/v1beta1 kind: Canary metadata: name: shop spec: targetRef: apiVersion: apps/v1 kind: Deployment name: shop service: port: 8080 analysis: interval: 1m threshold: 5 maxWeight: 50 stepWeight: 10 metrics: - name: request-success-rate thresholdRange: min: 99 interval: 1m - name: request-duration thresholdRange: max: 500 interval: 1m

Flagger raises the canary weight by stepWeight every interval until maxWeight, checking the metrics each time; after threshold failed checks it rolls back. The thresholds above mean "success rate at least 99 percent, request duration at most 500" (Flagger documents the duration threshold in milliseconds for its built-in metric).

⚠ Gates that cannot fail
  • A metric query that returns no data because the label is wrong, or a gate checked over a window so wide that a bad canary is diluted, passes silently. What you see: a broken version is promoted "successfully". Fix: test the gate by deliberately deploying a bad version in staging, make "no data" a failure, require a minimum request count, and compare the canary with the baseline over the same period.
On AWS
OptionHow it shifts trafficNotes (AWS documentation)
ALB weighted target groupsA listener rule with a forward action listing several target groups, each with a weight from 0 to 999; requests are distributed in proportion to the weightsPrimitive for blue/green and canary on EC2, ECS and EKS: change the weights step by step, from your pipeline or a script
AWS CodeDeployDeployment types for EC2/on-premises (in-place and blue/green), Lambda and ECS (traffic shifting)Predefined ECS configurations include CodeDeployDefault.ECSCanary10Percent5Minutes (10 percent first, the rest five minutes later), CodeDeployDefault.ECSLinear10PercentEvery1Minutes and CodeDeployDefault.ECSAllAtOnce; Lambda has analogous canary, linear and all-at-once configurations. Attach CloudWatch alarms to trigger automatic rollback
ECS / EKS rollingECS service rolling updates (minimum healthy percent and maximum percent) and Kubernetes RollingUpdateSame trade-offs as chapter 2
Lambda aliasesAlias with weighted routing between two versionsCodeDeploy automates the weight shifts
Shell - shift 10 percent to the new target group with the AWS CLI (command shape per the ELB documentation; placeholders, not run here)
$ aws elbv2 modify-listener --listener-arn <listener-arn> \ --default-actions '[{"Type":"forward","ForwardConfig":{"TargetGroups":[ {"TargetGroupArn":"<blue-tg-arn>","Weight":90}, {"TargetGroupArn":"<green-tg-arn>","Weight":10}]}}]'
Health checks gate everything
  • Whatever the tool, a traffic shift is only as safe as the health signal behind it. Use a readiness check that proves the app can serve (dependencies reachable, caches warm if needed), not a liveness-style "process is up" check; on the ALB set the health check path and thresholds, and the deregistration delay so that connections drain before an instance is removed.
09
Chapter 9 - Decisions

Choosing a Strategy, Release Checklist, Verification and Rollback

StrategyExposure of a bad versionDowntimeRollback speedExtra capacityComplexityBest for
RecreateAll users, with an outageYesSlow (redeploy)NoneLowestDev/test, single-instance, incompatible versions, maintenance windows
RollingGrows as pods roll; no metric gateNoMedium (roll back the same way)Small (maxSurge)LowDefault for stateless services with compatible versions
Blue/greenAll users at cutoverNoFastest (flip back)Up to 2x for the windowMediumBig-bang switches, risky releases needing pre-release testing, strict rollback needs
CanarySmall, chosen shareNoFast (weight to 0)SmallMedium-high (weighted routing, metric gates)High-traffic services with good metrics
ShadowNone (responses discarded)Non/a (nothing released)Roughly 2x for mirrored loadHigh (side-effect isolation)Validating rewrites and migrations before real exposure
A/B testA cohort, for an experimentNoFast (flag/route)SmallHigh (experiment platform, statistics)Product experiments
Feature flagWhoever the flag targetsNoSeconds (flag off)NoneMedium (flag platform, hygiene)Decoupling release from deploy; combine with any of the above

This table compares qualitative trade-offs and is not a benchmark: real cost and speed depend on your platform and tooling. Strategies combine well: a common mature setup is a rolling or canary deploy with the new code behind a feature flag, over an expand/contract schema, gated by SLO metrics.

If you need...Choose
Simplicity, stateless service, backwards-compatible changeRolling update with probes
Instant, tested rollback, and you can afford the capacityBlue/green
Minimal blast radius with data to prove healthCanary with automated analysis
To test a rewrite on real traffic without riskShadow, then canary
To start a risky feature for a few users and stop it instantlyFeature flag
To change the schemaExpand/contract, whatever the strategy
Release checklist
BeforeCheck
ChangeBackward compatible with the previous version (API, messages, database)? Migrations are expand-only?
ArtefactBuilt once, immutable, tagged with the commit; the same image runs in staging and prod
TestsCI green, including integration tests against the migrated schema
ConfigurationNew settings and secrets exist in the target environment; defaults are safe
ObservabilityDashboard and alerts for the service are working; the new code emits the metrics you will check
CapacityRoom for surge or the second environment; autoscaling limits checked
Rollback planWritten down: which command, who decides, what the data impact is. Previous version still deployable
TimingNot Friday evening; people available; no conflicting release or maintenance
CommunicationSupport and dependent teams informed; status page ready if risk is high
Post-deploy verification
  • Automated smoke tests against the new version immediately: login, one read, one write, one dependency call.
  • Watch the golden signals (page 07) for the new version for a defined bake time: error ratio, p95/p99 latency, saturation, and business KPIs, compared with the baseline.
  • Check logs for new error signatures and exceptions that did not exist before; check traces for new slow spans.
  • Confirm the deploy is real: an exposed build-info endpoint (/actuator/info with git commit) or a version metric proves which version answers.
  • Verify dependencies: queue depth, consumer lag, database connection and slow-query metrics.
  • Close the loop: only then promote, scale down the old version, and remove the flag or old environment.
Incident-driven rollbacks
Alert fires shortly after a deploy
▶
Stop and look at the change timeline first. "What changed in the last hour?" explains a large share of incidents: deploys, flag flips, config and infrastructure changes
A deploy is the likely cause
▶
Roll back (or flip the flag) first, diagnose second. Restoring service is the priority; the broken version can be analysed afterwards from logs and in staging
Rollback is not possible (data changed)
▶
Roll forward with the smallest possible fix, or disable the feature with a flag; communicate that data may need repair
Service is stable again
▶
Write down the timeline, verify the recovery with the same metrics, then run a blameless review: why did the gates not catch it?
⚠ Common deployment mistakes
  • Changing code and schema incompatibly in one release; no readiness probe; rolling out without a rollback plan; a gate that never fails; mirroring requests with side effects; leaving release flags in forever; deploying on Friday evening; running long migrations at app start; different artefacts in staging and production; "fixing forward" blindly during an incident when a rollback would be faster.
10
Chapter 10 - Interview

Questions You Should Be Able to Answer

Crisp answers for deployment-strategy questions.

  • Q: Compare rolling, blue/green and canary deployments.
    Rolling replaces instances gradually using the same capacity but with no metric-based gate and mixed versions for a while. Blue/green runs a full second environment and flips traffic at once: fastest rollback, higher cost. Canary sends a small, growing share of traffic to the new version and promotes based on metrics: smallest blast radius, needs weighted routing and good observability.
  • Q: What is the difference between deployment and release?
    Deploy puts new code into production; release exposes it to users. Feature flags and traffic routing let you separate them, so a risky feature can ship dark, be enabled gradually and be switched off in seconds.
  • Q: How does a Kubernetes rolling update work, and which settings control it?
    The Deployment scales a new ReplicaSet up and the old one down, governed by maxSurge and maxUnavailable (both default to 25 percent), and new pods only count once their readiness probe passes (plus minReadySeconds). kubectl rollout undo returns to the previous ReplicaSet.
  • Q: A rolling update caused errors for a few seconds even though the pods were healthy. Why?
    Typical causes: no or weak readiness probe, no graceful shutdown (in-flight requests dropped when SIGTERM arrives), load balancers still sending traffic to a terminating pod (fix with a preStop delay and server.shutdown=graceful), or too aggressive maxUnavailable.
  • Q: What is expand and contract?
    A pattern for making breaking schema or API changes safely: expand by adding the new structure alongside the old, migrate by dual-writing, backfilling and switching reads, then contract by removing the old structure. Every step is compatible with the running version and the previous one, so rollbacks stay safe until the final destructive step.
  • Q: How would you rename a database column with zero downtime?
    Add the new column (nullable); deploy code that writes both; backfill existing rows in batches; deploy code that reads the new column; once no version uses the old one, drop it in a later release. Never rename in place while two app versions run.
  • Q: Why can blue/green rollback be unsafe?
    Both environments share the database. If green ran an incompatible migration, flipping back to blue runs old code against a new schema. Rollback is only instant if the schema stays compatible with both versions.
  • Q: Flyway vs Liquibase?
    Both version schema changes and track applied ones in a history table. Flyway is SQL-first with versioned files; Liquibase uses changesets in XML/YAML/JSON/SQL, with preconditions and rollback blocks. In practice teams roll forward rather than roll back.
  • Q: Rollback or roll forward?
    Roll back application changes first when it is safe, because it is fastest. Roll forward when data or schema has moved on irreversibly or when a tiny fix is quicker. Decide the plan before the release.
  • Q: How do you make API and message changes backward compatible?
    Additive only: new optional fields, tolerant readers that ignore unknown fields, deprecate before removing, version when you must break, and schema-registry compatibility checks for events. Deploy providers before consumers for additions.
  • Q: What do Argo Rollouts and Flagger do?
    They automate progressive delivery on Kubernetes: shift traffic in steps, evaluate metrics (typically from Prometheus) at each step, and promote or abort. Argo Rollouts replaces Deployment with a Rollout resource; Flagger watches an existing Deployment through a Canary resource.
  • Q: How do you do canary on AWS?
    ALB listener rules with weighted target groups that your pipeline adjusts step by step, or CodeDeploy traffic-shifting configurations such as CodeDeployDefault.ECSCanary10Percent5Minutes for ECS and canary configurations for Lambda, with CloudWatch alarms triggering automatic rollback.
  • Q: Canary vs A/B test?
    A canary checks the health of a new version on a share of requests. An A/B test compares business outcomes between sticky user cohorts. Similar routing, different goal and duration.
  • Q: What is shadow traffic and what is the danger?
    Copying production requests to a new version while discarding its responses, to test it on real traffic with no user impact. The danger is side effects (payments, emails, writes), so the shadow must be isolated.
  • Q: What would you check right after a deploy?
    Smoke tests, version endpoint, golden signals versus baseline for a bake time, new error signatures in logs, dependency and queue health, and business KPIs, before promoting and cleaning up.
11
Chapter 11 - Summary

Cheat Sheet and Recap

Cheat sheet
Strategy / conceptOne lineWatch out for
RecreateStop old, start newDowntime
Rolling updateReplace gradually; maxSurge/maxUnavailable (default 25%)Mixed versions; needs readiness probe
Blue/greenTwo environments, flip the routerShared database; 2x capacity
CanarySmall share first, metrics gate each stepNeeds weighted routing and enough traffic
ShadowMirror requests, drop responsesSide effects
A/B testSticky cohorts, business metricStatistics, duration
Feature flagDeploy dark, release by configFlag debt; safe defaults
Expand/contractAdd, migrate, remove: three compatible stepsNever rename or drop in one step
Flyway / LiquibaseVersioned schema as codeDo not migrate on 20 pods at once
Rollback vs roll-forwardRollback app, roll forward dataDecide before the release
Argo Rollouts / FlaggerAutomated canary with metric analysisGates that cannot fail
AWSALB weighted target groups, CodeDeploy traffic shiftingHealth check path and deregistration delay
∑ Deployment strategies - what to remember
  • Deploy and release are different events. Separating them with flags and routing is the biggest safety lever you have.
  • Every gradual strategy runs two versions at once, so code, APIs, messages and the database must be compatible with both.
  • Use expand/contract for any breaking schema change, one deployable step at a time; the contract step is the only irreversible one.
  • Pick the simplest strategy that fits the risk: rolling by default, canary for high-traffic or risky changes, blue/green for fast rollback.
  • Automate the verdict: gate each step on error ratio, latency and business metrics compared with the baseline, and make gates able to fail.
  • Rollback first, diagnose second during an incident, while it is still possible; plan roll-forward for data changes.
Version note

Checked on 2026-10-31: Kubernetes Deployment documentation (strategy types, maxSurge and maxUnavailable defaults of 25 percent, Recreate behaviour); Argo Rollouts documentation (canary steps, BlueGreen fields, AnalysisTemplate with Prometheus provider); Flagger documentation (Canary resource, supported strategies, metric thresholds); AWS documentation (ALB weighted target groups with weights 0 to 999, CodeDeploy predefined configurations). The expand/contract statements were run against SQLite 3.45 only. The Kubernetes, Argo Rollouts and Flagger manifests were parsed as YAML but not applied: no cluster was available. Controller versions move quickly; check the current CRD versions before use.

Next in this section