CI/CDIntermediate 13 min Lesson 7 of 8

Rollback in the Pipeline

Make reverting a deployment a routine, automated action: triggers, artefact retention, database constraints and when to fix forward instead.

CI/CD · Lesson 7 of 8
0/8 done(0%)

What is it? #

Rollback is returning production to the previous known-good version. In a pipeline, it should be automatic for obvious failures and a single action otherwise.

The code side is straightforward when you keep previous artefacts: deploy the previous image or switch the release symlink.

The database side constrains everything. A destructive migration removes the rollback option, which is why backwards-compatible schema changes are a prerequisite for safe deployment.

Rollback must be tested. A path that has never been exercised is not a recovery plan.

Think of it like this #

A reverse gear that you have actually used. Knowing it exists is not the same as knowing it engages smoothly under load.

Practising in an empty car park is what makes it usable in traffic.

Simple example #

A deployment whose health check fails triggers an immediate automatic rollback. A subtler problem found twenty minutes later is rolled back by running a workflow with the previous version as input.

Code #

YAML
# Automatic rollback when verification fails
      - name: Verify deployment
        id: verify
        run: ./scripts/verify-production.sh ${{ github.sha }}

      - name: Roll back
        if: failure() && steps.verify.outcome == 'failure'
        run: |
          PREVIOUS=<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mo stretchy="false">(</mo><mi>g</mi><mi>h</mi><mi>a</mi><mi>p</mi><mi>i</mi><mi>r</mi><mi>e</mi><mi>p</mi><mi>o</mi><mi>s</mi><mi mathvariant="normal">/</mi></mrow><annotation encoding="application/x-tex">(gh api repos/</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:1em;vertical-align:-0.25em;"></span><span class="mopen">(</span><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal">ha</span><span class="mord mathnormal">p</span><span class="mord mathnormal">i</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">e</span><span class="mord mathnormal">p</span><span class="mord mathnormal">os</span><span class="mord">/</span></span></span></span>{{ github.repository }}/deployments \
                      --jq '[.[] | select(.environment=="production")][1].sha')
          ssh deploy@prod "IMAGE=ghcr.io/<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>g</mi><mi>i</mi><mi>t</mi><mi>h</mi><mi>u</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>r</mi><mi>e</mi><mi>p</mi><mi>o</mi><mi>s</mi><mi>i</mi><mi>t</mi><mi>o</mi><mi>r</mi><mi>y</mi></mrow><mo>:</mo><mi>s</mi><mi>h</mi><mi>a</mi><mo>−</mo></mrow><annotation encoding="application/x-tex">{{ github.repository }}:sha-</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal">u</span><span class="mord mathnormal">b</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">e</span><span class="mord mathnormal">p</span><span class="mord mathnormal">os</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.0278em;">or</span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="katex-base"><span class="katex-strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">s</span><span class="mord mathnormal">ha</span><span class="mord">−</span></span></span></span>PREVIOUS /srv/app/deploy.sh"
          ./scripts/verify-production.sh "$PREVIOUS"

      - name: Notify
        if: always()
        run: ./scripts/notify.sh "${{ job.status }}"
YAML
# A manual rollback workflow — one click, no improvisation
name: Rollback production
on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Commit SHA or image tag to roll back to'
        required: true

jobs:
  rollback:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Confirm the image exists
        run: docker manifest inspect ghcr.io/<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>g</mi><mi>i</mi><mi>t</mi><mi>h</mi><mi>u</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>r</mi><mi>e</mi><mi>p</mi><mi>o</mi><mi>s</mi><mi>i</mi><mi>t</mi><mi>o</mi><mi>r</mi><mi>y</mi></mrow><mo>:</mo><mi>s</mi><mi>h</mi><mi>a</mi><mo>−</mo></mrow><annotation encoding="application/x-tex">{{ github.repository }}:sha-</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal">u</span><span class="mord mathnormal">b</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">e</span><span class="mord mathnormal">p</span><span class="mord mathnormal">os</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.0278em;">or</span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="katex-base"><span class="katex-strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">s</span><span class="mord mathnormal">ha</span><span class="mord">−</span></span></span></span>{{ inputs.version }}
      - name: Deploy previous version
        run: ssh deploy@prod "IMAGE=ghcr.io/<span class="katex"><span class="katex-mathml"><math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><mrow><mrow><mi>g</mi><mi>i</mi><mi>t</mi><mi>h</mi><mi>u</mi><mi>b</mi><mi mathvariant="normal">.</mi><mi>r</mi><mi>e</mi><mi>p</mi><mi>o</mi><mi>s</mi><mi>i</mi><mi>t</mi><mi>o</mi><mi>r</mi><mi>y</mi></mrow><mo>:</mo><mi>s</mi><mi>h</mi><mi>a</mi><mo>−</mo></mrow><annotation encoding="application/x-tex">{{ github.repository }}:sha-</annotation></semantics></math></span><span class="katex-html" aria-hidden="true"><span class="katex-base"><span class="katex-strut" style="height:0.8889em;vertical-align:-0.1944em;"></span><span class="mord"><span class="mord"><span class="mord mathnormal" style="margin-right:0.0359em;">g</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal">h</span><span class="mord mathnormal">u</span><span class="mord mathnormal">b</span><span class="mord">.</span><span class="mord mathnormal" style="margin-right:0.0278em;">r</span><span class="mord mathnormal">e</span><span class="mord mathnormal">p</span><span class="mord mathnormal">os</span><span class="mord mathnormal">i</span><span class="mord mathnormal">t</span><span class="mord mathnormal" style="margin-right:0.0278em;">or</span><span class="mord mathnormal" style="margin-right:0.0359em;">y</span></span></span><span class="mspace" style="margin-right:0.2778em;"></span><span class="mrel">:</span><span class="mspace" style="margin-right:0.2778em;"></span></span><span class="katex-base"><span class="katex-strut" style="height:0.7778em;vertical-align:-0.0833em;"></span><span class="mord mathnormal">s</span><span class="mord mathnormal">ha</span><span class="mord">−</span></span></span></span>{{ inputs.version }} /srv/app/deploy.sh"
      - name: Verify
        run: ./scripts/verify-production.sh "${{ inputs.version }}"
TEXT
What makes rollback possible

previous artefacts kept    do not prune the last N images
immutable tags             the previous version is addressable
backwards-compatible
  migrations               the old code still works against the new schema
externalised config        no rebuild needed to change environment settings
feature flags              risky behaviour can be disabled without deploying
TEXT
Rollback triggers

automatic   health check fails after deployment
            error rate above a threshold within N minutes
            the deployed version does not match what was deployed

manual      a subtle problem discovered later
            a business decision to revert a feature

Automatic triggers must be reliable. A rollback fired by a flaky check
is its own incident.
TEXT
Roll back or fix forward?

roll back    the failure is immediate and clear
             the previous version is known good
             no irreversible migration has run

fix forward  the migration cannot be reversed
             the previous version has its own serious problem
             the fix is small, understood and already tested

Default to rolling back. Restore service first, investigate afterwards.

How it works #

Automatic rollback is attached to the verification step. If the new version does not become healthy, the pipeline redeploys the previous image and verifies that instead.

Finding the previous version means querying deployment history rather than assuming. Hardcoding "the previous tag" breaks when two deployments happen in quick succession.

The manual workflow takes a version as input and performs the same deployment path. Using the normal deployment script rather than a special rollback procedure means the path is exercised constantly.

Checking that the image exists before deploying prevents the worst case: a rollback that fails because the artefact was pruned, leaving production broken with no quick path back.

Feature flags reduce how often rollback is needed at all. A feature deployed disabled and enabled for a subset can be turned off in seconds without touching the deployment.

Notification on every outcome matters. A silent automatic rollback means production is running an older version and nobody knows why the new feature is missing.

The trigger reliability point is worth emphasising. An automatic rollback fired by a flaky health check causes an unnecessary deployment cycle, which is why verification must be as trustworthy as the tests.

Real-world use #

Teams that can roll back in under a minute deploy far more often, because the cost of being wrong is small.

The constraint is almost always the database. Once a migration has dropped a column, rolling back the code means the old version queries a column that no longer exists.

Artefact retention policy matters more than it seems. Pruning aggressively saves storage and removes rollback targets; keeping the last twenty images is cheap insurance.

Canary and blue-green deployments make rollback nearly instant by keeping the previous version running and switching traffic back, which the next lesson covers.

Practising rollback during a calm period — deploying a trivial change and reverting it — is what makes it reliable when it is needed.

Common mistakes #

  • Destructive migrations that make rollback impossible.
  • Pruning old images aggressively, removing the rollback target.
  • A rollback path that is never tested until an incident.
  • Automatic rollback triggered by an unreliable check.
  • Rolling back silently, so nobody knows production changed.

Practice #

Add automatic rollback on verification failure to a deployment pipeline, plus a manual rollback workflow taking a version as input. Deploy a deliberately broken version and confirm the automatic rollback restores service and notifies the team.

Quick quiz

  1. 1. What most often prevents a rollback?

  2. 2. Why query deployment history for the previous version?

  3. 3. Why check that the rollback image exists before deploying it?

  4. 4. What is the risk of automatic rollback on a flaky check?

  5. 5. When should you fix forward instead?

Summary

  • Automatic rollback on verification failure, manual rollback as one action.
  • Keep previous artefacts and use immutable tags.
  • Backwards-compatible migrations are what make rollback possible.
  • Verify the rollback target exists before deploying it.
  • Notify on every rollback and practise the path regularly.