TL;DR: Every deployment strategy answers the same question: how do I put a new version in front of users without downtime, and how fast can I go back? Rolling replaces servers one by one. Blue-green switches all traffic at once between two environments. Canary sends a small part of traffic first. Strangler fig replaces an old system piece by piece. And whatever you pick, the database is what makes rollback hard.
Why this post
“How do you roll out a new version?” sounds like a question about tools: nginx, Kubernetes, Azure. But tools come second. First you choose a strategy, and the strategy decides how much risk you take and how you go back.
So here is a short refresh, starting from the simplest possible deploy.
Before: stop, copy, start
1. Stop the app
2. Copy new files
3. Run DB migrations
4. Start the app
5. Hope
Problems:
- Downtime on every deploy, so people deploy rarely, so each deploy is big and scary.
- If the new version is broken, all users see it.
- Rollback = do the same thing again with old files. If the migration already changed the schema, maybe you can’t.
The base you need first: health checks
Every strategy below needs to know “is the new instance ready?”. In ASP.NET Core:
builder.Services.AddHealthChecks()
.AddDbContextCheck<AppDbContext>();
app.MapHealthChecks("/health/ready");
The load balancer or Kubernetes sends traffic to an instance only when /health/ready returns 200. Without this, “zero downtime” is just luck.
1. Rolling update
Replace instances one by one. With 4 servers: take one out, update it, put it back when healthy, next one.
In Kubernetes this is the default:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # one extra pod during the update
maxUnavailable: 0 # never go below full capacity
- Good: no extra infrastructure, no downtime.
- Bad: for a few minutes, old and new versions run at the same time. Both must work with the same database and the same messages. Rollback is another rolling update, so it is not instant.
kubectl rollout undo deployment/orders-api
2. Blue-green
You have two identical environments. Blue is live. You deploy to green, test it, then switch all traffic to green at once. Blue stays there, untouched.
In Azure App Service this is a deployment slot swap:
az webapp deployment slot swap \
--resource-group my-rg --name orders-api \
--slot staging --target-slot production
- Good: you test the real new environment before users see it. Rollback is instant: switch back to blue.
- Bad: double the infrastructure, at least during the deploy. And the switch is all-or-nothing: if there is a bug that only appears under real load, 100% of users see it.
3. Canary
Like blue-green, but you move traffic gradually: 1% of requests go to the new version, then 5%, 25%, 100%. At each step you compare error rate and p99 latency between old and new. If the canary looks worse, you send its traffic back to the old version.
The name comes from the canary in a coal mine: a small bird that shows danger before it hurts everyone.
- Good: real traffic tests the new version, but a bug only hits a small part of users.
- Bad: you need good metrics and a router that can split traffic by weight (a service mesh, a cloud load balancer, or an ingress with canary support). Like rolling, two versions run at the same time.
Canary vs feature flags: canary splits traffic between two versions of the app. A feature flag splits users between two code paths inside one version. Canary protects you from a bad build, flags protect you from a bad feature. You can use both.
4. Strangler fig
This one is not about one release. It is about replacing a big old system without a “big rewrite”.
A strangler fig is a plant that grows around a tree until the old tree is gone. Same idea: put a proxy in front of the legacy app, move one route at a time to the new service, and the legacy part slowly gets smaller.
With YARP, the reverse proxy from Microsoft:
"ReverseProxy": {
"Routes": {
"orders-new": {
"ClusterId": "orders-service",
"Match": { "Path": "/api/orders/{**rest}" }
},
"everything-else": {
"ClusterId": "legacy",
"Order": 100,
"Match": { "Path": "{**rest}" }
}
},
"Clusters": {
"orders-service": { "Destinations": { "d1": { "Address": "https://orders.internal/" } } },
"legacy": { "Destinations": { "d1": { "Address": "https://legacy.internal/" } } }
}
}
Today only /api/orders goes to the new service. Next month, /api/invoices. Each step is small, and you can go back by changing one route.
- Good: the old system keeps working the whole time. No “big bang” migration weekend.
- Bad: for a long time you run two systems. And shared data between them is the hardest part.
Rollback: the database is the real problem
Rolling back code is easy. Rolling back data is not.
If version 2 renames a column, version 1 can’t read it anymore. So rollback breaks, and during rolling or canary deploys, version 1 instances break too.
The rule: every schema change must be compatible with the previous version of the code. Use expand and contract:
- Expand: add the new column. Old code ignores it.
- Migrate: new code writes both columns, a job copies old data.
- Switch: new code reads only the new column.
- Contract: in a later release, when you will never roll back to the old version, drop the old column.
Slower, yes. But at every step you can roll back the code safely.
Also: practice the rollback. A rollback plan that nobody ever ran is not a plan.
Quick comparison
| Strategy | Two versions live at once? | Rollback speed | Extra cost |
|---|---|---|---|
| Rolling | yes, for minutes | slow (another rollout) | low |
| Blue-green | no | instant | double infra |
| Canary | yes, on purpose | fast (move traffic back) | traffic routing + good metrics |
| Strangler fig | yes, for months | per route | proxy + two systems |
The 30-second summary
Rolling update replaces instances one by one, cheap but rollback is slow and two versions overlap. Blue-green deploys to a second environment and switches all traffic at once, so rollback is instant but you pay for double infrastructure. Canary moves a small percentage of traffic to the new version and grows it while you compare metrics. Strangler fig is for replacing a legacy system route by route behind a proxy. All of them need health checks, and all of them need database changes to be backward compatible, using expand and contract, otherwise you can’t roll back.
That’s it. If your team deploys in a different way, tell me in comments. 🙂
Leave a Reply