Back to resources
// devops · deployment

Make a deployment boring

A good deployment shouldn't trigger any emotion. No “fingers crossed,” no midnight maintenance window. Here's how I turn a stressful deploy into a reproducible non-event.

GuideJul 14, 2026~8 min · intermediate
CI/CDTerraformGitHub Actionshealth checksrollback
01

Why "boring" is the goal

An exciting deployment is a risky one. “Works on my machine” means the environment isn't reproducible; a stressful release means there's no safety net. The goal: to deploy on a Friday at 5pm without thinking about it.

01
Reproducible

The same command produces the same result, everywhere. Zero manual step, zero “don’t forget to also…”.

02
Automated

A merge to main triggers build, tests, deploy. Humans no longer copy files by hand.

03
Observable

You know in real time whether the deploy went well — health checks, logs, metrics — without waiting for a customer ticket.

04
Reversible

A one-command rollback. The question isn’t “will it work” but “how fast can I roll back.”

02

Infrastructure as code

Infra clicked by hand in the console isn’t reproducible and nobody knows what changed. In Terraform, infra becomes code: versioned, reviewed in a pull request, recreatable identically.

infrastructure.tfcopy
# infrastructure.tf — l'infra est du code, versionne et relisible
resource "aws_instance" "web" {
ami = "ami-0abc"
instance_type = "t3.large"
tags = { Project = "shop", Env = "prod" }
}
# "terraform apply" recree la meme infra a l'identique, partout
03

The pipeline

The key principle: the same deploy script runs in CI and locally. No secret logic hidden in the GitHub UI — if CI is down, you run the script by hand and get the same result.

.github/workflows/deploy.ymlcopy
# .github/workflows/deploy.yml — chaque merge sur main deploie
name: deploy
on:
push: { branches: [main] }
jobs:
ship:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm run build && npm test
- name: deploy
run: ./scripts/deploy.sh # meme script que tu peux lancer en local
04

The net: health checks + rollback

The new version only takes traffic after proving it’s alive. The load balancer polls a /health endpoint; until it returns 200, the old version stays in place.

health.tscopy
// l'app expose un endpoint que le load balancer interroge
app.get("/health", (_, res) => res.json({ ok: true }));
// le deploiement attend que /health reponde 200 avant de basculer
// sinon : rollback automatique, l'ancienne version reste en place

With a blue-green or rolling deploy, the switch is gradual: if /health fails, nothing switches and the user sees nothing. Rollback becomes a non-issue.

i

On the application side, the same observability logic applies to AI: I cover tracing in trace and score an agent.

05

Blue-green or canary: picking your net

I mentioned blue-green above; in practice I pick between two strategies depending on risk. With blue-green, two identical environments run in parallel: I switch 100% of traffic from "blue" to "green" at once, and rollback is just a traffic switch back to the still-running "blue." With canary, I shift traffic in steps — 5%, then 25%, then 100% — and only advance if error rates and latency stay healthy.

Blue-green

Instant cutover, instant rollback.

Rollback = re-route to the original, already-warm environment. "How fast can I roll back" is measured in seconds.
Cost: two full environments in parallel during the cutover.
Blind spot: at cutover, 100% of users see the new version at once. A bug the /health check doesn't catch hits everyone.
Canary

Gradual exposure, capped blast radius.

The first step caps the damage: at 5%, a bad version only hits one user in twenty before it's halted.
Requires solid observability: without reliable per-version metrics, you don't know when to advance or roll back.
Slower: the traffic ramp takes minutes to hours, and you manage two coexisting versions.
i

My rule of thumb: blue-green when instant rollback matters most and traffic is predictable; canary when I want to validate a risky version on a real sample before exposing everyone. Both are handled on AWS with CodeDeploy and ELB traffic shifting.

06

Readiness vs liveness, and measuring the boredom

One /health isn't enough: separate "alive" from "ready"

The /health I showed above actually hides two different questions, and Kubernetes splits them explicitly with its liveness, readiness and startup probes. Readiness says "I can take traffic now" (DB connected, cache warm): if it fails, I'm pulled from the load balancer without being killed. Liveness says "I'm alive, don't restart me": if it fails, the container is restarted. Conflating the two leads to restart loops the moment a dependency slows down.

probes.tscopy
// deux endpoints distincts, deux responsabilites
// /livez : le process tourne-t-il ? (ne teste PAS les dependances)
app.get("/livez", (_req, res) => res.status(200).send("ok"));
// /readyz : puis-je servir du trafic ? (teste les dependances)
app.get("/readyz", async (_req, res) => {
try {
await db.query("SELECT 1"); // la base repond ?
await redis.ping(); // le cache repond ?
res.status(200).send("ready");
} catch {
res.status(503).send("not ready"); // retire du LB, pas de restart
}
});
!

Classic trap: putting dependencies in the liveness check. If the database hiccups for 30s, all your instances fail liveness at once, get restarted at once, and you turn a transient slowdown into a full outage. Liveness must stay local to the process.

Measuring that it's actually boring: DORA metrics

"Boring" isn't just a feeling — it's measurable. The DORA metrics, from six years of Google research, capture both the velocity and the stability of a delivery pipeline. The report's key counterintuition: speed and stability aren't a trade-off — the best teams deploy more often AND recover faster.

Deployment frequencyHow often you ship to production. A boring deployment happens often, in small batches — not one big, stressful quarterly release.
Lead time for changesTime from a commit to production. The shorter it is, the smaller the change and the easier to diagnose if it breaks.
Change failure rateShare of deployments that cause an incident. It's the ultimate judge of your health checks and cutover strategy.
Failed deployment recovery timeTime to restore service after a failure. This is exactly the "reversible" property: my one-command rollback drives it toward zero.

Note: DORA has evolved its model to five metrics (adding a "rework rate"), but these four remain the foundation. I instrument them like everything else — metrics, logs and traces — in line with the three signals of OpenTelemetry.

07

Sources & further reading

01DORA — Software delivery performance metricsThe official reference for DORA metrics and their up-to-date definitions.02Google Cloud — Use the Four Keys to measure DevOps performanceThe four metrics explained, with the open-source Four Keys project to collect them.03Kubernetes — Liveness, Readiness and Startup ProbesThe canonical distinction between "alive" and "ready," with configuration examples.04AWS — Canary deploymentsStep-by-step traffic shifting to cap the blast radius.05AWS — Blue/Green Deployments whitepaperTwo identical environments and rollback via traffic switch-back.06HashiCorp — Manage resource drift with TerraformDetect drift between state and real infrastructure with terraform plan -refresh-only.07OpenTelemetry — Signals (traces, metrics, logs)The three observability signals to know in real time whether a deploy went well.

A boring deployment is the highest compliment you can pay an infra team. Reproducible, automated, observable, reversible: four properties, and the stress vanishes. The freed-up time goes to the product.