Skip to main content
ANVISoftware Solutions
Lesson 17 of 17Advanced17 min

Rollback

By the end of this lesson

Have a rehearsed way back before you need it.

Rollback means putting the previous version back. It is the cheapest thing you can do during an incident, because it does not require understanding what went wrong — only knowing that things were better twenty minutes ago.

It is also the procedure most likely to be attempted for the first time while a service is down. That is when teams discover the previous image was pruned from the registry last week, that nobody wrote down which version was running, that the deployment script only works from one person's laptop, or that the person on call does not have the permission the command needs. None of those is difficult to fix. All of them are expensive to find out about at the wrong moment.

So the work in this lesson is mostly done in advance, on an ordinary day. Rehearse the rollback, time it, and write down what you needed. The number you get becomes the basis for a decision you will have to make under pressure: is it faster to go back, or to go forward?

The terms, used consistently through the rest of the lesson:

Rollback
Deploying the previous artifact, unchanged, using the normal deployment mechanism. It is a deployment like any other, which is why the checks that make deployment safe apply to it too.
Fix forward
Leaving the new version in place and shipping a further change that corrects the problem. Often called rolling forward. It is a new release, written under pressure, with the usual review compressed.
Known-good version
The version that was serving traffic before the change, which you have evidence about rather than hope. Knowing which one that was, and that its artifact still exists, is the whole of rollback readiness.
Backward-compatible change
A change the previous version can live alongside. The previous code runs against the new database schema, reads the new message format, and ignores what it does not recognise. Compatibility is what makes rollback possible at all.
Expand and contract
Splitting a schema change into an additive release, a transition, and a destructive release much later. The name describes the shape: the schema grows to hold both versions, then shrinks once nothing needs the old one.
Feature flag
A configuration value your code checks to decide whether a change is active. It separates deploying from releasing, so new code can sit in production doing nothing until you switch it on, and stop doing it again without a deployment.
Kill switch
A feature flag whose only job is to turn something off in a hurry — an expensive report, a third-party integration, a background job. Worth having on anything you can imagine wanting to disable at short notice.
Recovery time
How long it takes from deciding to act to the service being healthy again. Measure it during a rehearsal. This is the number that decides between rollback and fixing forward, and guessing it produces the wrong decision.

Neither of these is the correct answer in general. The useful thing is knowing which conditions favour which, before you are standing in the middle of an incident deciding.

 Roll backFix forward
What you are betting onThat the previous version was healthy, which you have evidence forThat you have correctly diagnosed the fault and your fix is right
Typical time to recoveryMinutes, and predictable, because the artifact exists and the path is rehearsedAs long as diagnosis plus writing plus reviewing plus deploying takes. Rarely predictable
Risk it introducesLow. You are returning to a state that was workingMeaningful. A change written quickly during an incident is the most likely change to be wrong
What it needs in placeThe previous artifact retained, the version recorded, permissions granted, the script rehearsedDiagnosis, and a pipeline fast enough to be useful under pressure
The only option whenThe fault is in the new version and the change is reversibleThe change cannot be reversed: data has been written in a new shape, a destructive migration ran, a third party was told something, or the fault is in a dependency rather than your release
Effect on the fixBuys time. You investigate with the service healthy and ship the fix properlyCouples the fix to the outage. Every minute of review is a minute of impact
Sensible defaultYes. Stop the impact first, understand it secondWhen rollback is unavailable, or when the fix is genuinely one line and you can see why

The deployment lesson walked through expand and contract to keep a rolling deployment working. Here is the same sequence with one question asked at every step: if this release turns out to be bad, can you go back? Renaming employees.phone to employees.mobile_number is the example.

  1. Release 1 — add the new column, nullable

    Nothing reads or writes it yet. The running version does not know it exists and carries on. Rollback: fully available, because the previous artifact never referenced the column.

  2. Release 2 — write both columns, keep reading the old one

    New rows now populate both. Reads still come from phone, because mobile_number is empty for every existing row. Rollback: fully available. The previous version writes only phone, which is still the column being read.

  3. Release 3 — backfill the existing rows in batches

    Copy phone into mobile_number for rows written before release 2, a few thousand at a time. This is additive and repeatable, so running it twice changes nothing. Rollback: fully available, and the backfill can be paused at any point.

  4. Release 4 — switch reads to the new column

    Now that every row has a value, read mobile_number. This is the release most likely to expose a fault, and it is also fully reversible: both columns exist and both are still being written, so the previous artifact works unchanged.

  5. Release 5 — stop writing the old column

    Rollback becomes partial here. Rows created after this release have no value in phone, so a previous version that reads phone returns blanks for recent employees rather than failing. Partially recoverable is its own category, and it is worth naming out loud when you plan the release rather than discovering it during one.

  6. Release 6 — drop the old column, weeks later

    The only destructive step, deliberately separated from everything else by enough time that you would not consider rolling back past it. After this, going back means restoring a backup and losing whatever was written since it was taken. Rollback: not available.

The same sequence as migrations, with the reversible steps first
SQL
-- Release 1. Additive and invisible to the running version.
ALTER TABLE employees ADD COLUMN mobile_number varchar(32) NULL;

-- Release 3. Batched, so the table is never locked for long. Run until it
-- reports zero rows updated.
UPDATE employees AS e
SET    mobile_number = e.phone
FROM  (SELECT id
       FROM   employees
       WHERE  mobile_number IS NULL
         AND  phone IS NOT NULL
       ORDER  BY id
       LIMIT  5000) AS batch
WHERE  e.id = batch.id;

-- Release 6, weeks later. The only statement here you cannot undo.
ALTER TABLE employees DROP COLUMN phone;

-- What a rename is, underneath. A drop and an add in one statement, with no
-- moment when both columns exist. Never in the same release as the code change.
-- ALTER TABLE employees RENAME COLUMN phone TO mobile_number;
  • NULL in the first statement is what makes release 1 safe. The previous version inserts rows without mentioning mobile_number, and a nullable column accepts that. A NOT NULL column would reject every insert from the code currently in production.
  • If the column genuinely has to be NOT NULL, that constraint is a later release of its own: add it nullable, backfill, then add the constraint once every row has a value. Three releases instead of one, and each is reversible.
  • The batched update exists because one statement covering the whole table holds locks for as long as it runs. On a large employees table that is minutes, during which the API waits, which looks exactly like an outage to whoever is using the field staff app.
  • Run the batch in a loop with a short pause, and stop when it updates zero rows. Because the WHERE clause skips rows that already have a value, re-running it is harmless — which means an interrupted backfill can be resumed rather than reasoned about.
  • The DROP is separated from the rest by releases and by weeks, and that separation is the entire safety mechanism. There is no technical reason it cannot run on day one, and that is precisely why it needs a written rule rather than a judgement call.
  • The commented rename is the trap. It reads like a tidy single change and it removes both halves of your compatibility window at once: the old column is gone, so the old code cannot read it, and there is no release in which both shapes exist.
Three levers, fastest first
Shell
# 1. Flag off. Changes behaviour without deploying anything and without touching
#    the schema. Only covers changes you wrapped in a flag beforehand.
az appconfig kv set --name anvi-employees-config \
  --key EmployeesApi:UseMobileNumber --value false --label production --yes

aws ssm put-parameter --name /employees-api/production/use-mobile-number \
  --value false --type String --overwrite

# 2. Previous revision. Answer this question first, every time: what was running?
az containerapp revision list --name employees-api-prod --resource-group employees-prod \
  --query "[].{revision:name, active:properties.active, created:properties.createdTime}" -o table

az containerapp ingress traffic set --name employees-api-prod --resource-group employees-prod \
  --revision-weight employees-api-prod--r0147=100

# 3. AWS equivalent. Point the service back at the task definition revision that
#    was running before, by number.
aws ecs update-service --cluster employees-prod --service employees-api \
  --task-definition employees-api:214 --force-new-deployment
  • The flag is first because it is the only lever that needs no deployment. A configuration change reaches the running instances within their refresh interval, which is usually seconds to a couple of minutes, and it works during an incident where the pipeline is also broken.
  • Its limitation is absolute: a flag can only disable what you wrapped in one before you shipped. That is the argument for adding a flag around anything you would want to be able to switch off, and for keeping the check simple enough that its correctness is easy to see.
  • The revision list is the step people skip, and it is the one that answers what was running before. Write the current version somewhere a person can read it at deploy time — the environments lesson makes the same point — because reconstructing it from pipeline history during an incident costs minutes you do not have.
  • Shifting traffic weight is fast because the old revision still exists and is still warm. This is the blue-green style of return: no image pull, no start-up, seconds rather than minutes. It only works while the platform is still retaining that revision, which is a setting worth checking rather than assuming.
  • The AWS command is the same idea with a different noun. A task definition revision is an immutable numbered record, so rolling back is a pointer change — and it is only quick if you recorded which number was live.
  • None of these three commands touches the database. That is the boundary the rest of this lesson is about: code and configuration come back quickly, schema does not come back at all unless you planned for it.

Summary

  • Rollback is the cheapest incident response, and it only works if the artifact, the version record, the permissions and the rehearsal were prepared beforehand
  • Roll back when the previous version is known good and the change is reversible; fix forward when it is not, accepting that a change written during an incident carries real risk
  • A backward-incompatible migration removes rollback entirely, because the previous code cannot read the new schema
  • Expand and contract keeps the way back open: add, write both, backfill, switch reads, stop writing the old, and drop it weeks later
  • A feature flag is the fastest lever because it needs no deployment, and it only covers changes you wrapped in advance

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Think about it

Which of these can you undo by redeploying?

For each release, decide whether redeploying the previous artifact restores a working service: a release that adds a nullable column and nothing else; a release that renames a column and updates the code to match; a release that changes the JSON message a background worker consumes from the queue; a release whose new behaviour sits behind a feature flag that is currently off.

For the ones you answered no to, say what you would need to have done differently a week earlier.

Show solution

The nullable column is reversible. The previous version never referenced it, so it runs unchanged against the new schema. Additive changes are the only schema changes that are free.

The rename is not reversible. The previous code queries a column that no longer exists. What was needed a week earlier is the expand and contract sequence, with the drop held back for a separate release.

The message format change depends on a detail worth thinking about. If the worker was already deployed and messages in the new shape are sitting in the queue, rolling the worker back leaves it unable to parse them, and they fail or land in a dead-letter queue. The preparation is the same idea applied to messages rather than columns: make the consumer tolerate both shapes first, ship the producer change second, and remove the old handling much later.

The flagged release is reversible twice over. You can switch the flag off without deploying at all, and you can also redeploy the previous artifact, because the code path was never active. This is the cheapest position to be in, and it costs a flag and the discipline to remove it later.

The pattern across all four: reversibility is a property you build into the release, not something you can obtain once the release is out. By the time you want to roll back, the answer has already been decided.

Challenge

Write the runbook, then have someone else run it

Write the rollback runbook for a service you know. Include: where the currently deployed version is recorded, the exact commands in order, which permission each one needs, how to confirm the service is healthy afterwards, and what to do if the previous artifact is missing.

Then ask a colleague who has never rolled this service back to follow it in a non-production environment, without helping them. Time it and note every question they had to ask.

Show solution

The questions they ask are the deliverable. Each one is a step you know and the runbook does not say, which is exactly the gap that turns a five-minute recovery into a thirty-minute one at 2am.

The most common omissions are the version lookup, the permission the command needs, and how you know it worked. The last of those matters more than it looks: without a stated health check, people declare success when the command returns rather than when the service is actually serving.

The missing-artifact branch is worth writing even though it feels unlikely, because it is the case where rollback is not available and the team has to switch to fixing forward. Deciding that in advance saves an argument during the incident.

Timing it gives you the number you will quote when someone asks whether to roll back or fix forward. A recovery time you have measured makes that a comparison; a recovery time you have guessed makes it a gamble, and people under pressure tend to guess low.

There is a reasonable objection to this exercise: it costs an afternoon of two people's time for something that may not happen this quarter. That is true, and it is the same argument as any rehearsal. The cheaper version is to do it once with the most junior person on the team, because they will surface the assumptions everyone else has stopped noticing.

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

A release renamed employees.phone to employees.mobile_number and shipped the matching code. Error rates jump. Why does redeploying the previous version not fix it?
Why can a feature flag be faster than a rollback?

Saved in this browser only.

End of the published lessons

That is everything written so far in Cloud & DevOps

More lessons in this course are on the way. In the meantime, the course page shows the full roadmap, and the projects are the best way to consolidate what you have covered.