Skip to main content
RunBook Academy

← All labs in Git, CI/CD & GitOps

Lab · advanced · ~90 min

Lab 23: Perform a GitOps rollback via `argocd app rollback` and validate

C · SimulationB · Nested virtualisation

Objectives

  • Inspect the Argo CD application history to identify the desired revision
  • Perform a rollback via `argocd app rollback` to a known-good deployment
  • Validate that the cluster state matches the rolled-back Git state (image tag, replicas, env vars)
  • Capture the rollback evidence: before/after for the image, replicas, and the application health
  • Author the post-rollback investigation document that links the regression to the deploy
  • Document the data-integrity questions for stateful workloads and the rollback decision framework

Prerequisites

Objective

By the end of this lab you will have authored the artefacts that perform a GitOps rollback through Argo CD: the rollback runbook, the rollback script that automates the history inspection and the validation, the evidence bundle for the scenario, the post-rollback investigation document, the decision framework, and the history inspection notes.

The point of this lab is not the argocd app rollback command — that is one CLI invocation. The point is the operationalisation: identifying the right revision, validating that the rollback succeeded, capturing the evidence, and writing the post-rollback investigation that links the regression to the deploy.

Architecture

A kind cluster with Argo CD managing a web application. The application has three deployment history entries: revision 1 (initial), revision 2 (scaled to 3 replicas and updated image to 1.27-alpine), revision 3 (changed image to 1.28-alpine and added a label). The team has detected that revision 3 introduced a regression (the new image has a memory leak). The on-call engineer rolls back to revision 2 (the known-good state), validates the cluster, and writes the post-rollback investigation.

sequenceDiagram
    participant E as on-call engineer
    participant A as Argo CD
    participant K as cluster
    participant G as Git repo

    E->>A: argocd app history web
    A-->>E: 3 entries: id=1, id=2, id=3
    E->>A: argocd app set web --sync-policy none
    E->>A: argocd app rollback web 2
    A->>G: render revision 2 manifests
    A->>K: apply revision 2 manifests
    K-->>E: pods restart with revision 2 image
    E->>A: argocd app get web
    A-->>E: Synced: True, Healthy: True
    E->>G: post-rollback investigation PR
    E->>A: argocd app set web --sync-policy automated

The rollback uses Argo CD’s history (which records every sync with the source revision, the deployed manifests, and the deploy time). The on-call engineer chooses the known-good revision; Argo CD renders the manifests and applies them. The history ID is a positional argument to argocd app rollback, not a flag. Because the web application uses automated sync, the engineer disables it first (argocd app set web --sync-policy none) — Argo CD refuses to roll back while automated sync is enabled — and re-enables it only after the follow-up Git PR reconciles the branch. The Git repository is not modified by the rollback; the rollback is a cluster-side action.

Requirements

  • A kind cluster with Argo CD installed (Lab 19).
  • A sample application with at least three deployment history entries. The lab sets this up in Task 1.
  • The argocd CLI authenticated against the cluster.
  • jq on the workstation.

Scenario

A platform team runs a web application managed by Argo CD. The team deploys three times in a day: revision 1 (initial, nginx:1.27-alpine, 2 replicas), revision 2 (scaled to 3 replicas, image unchanged), revision 3 (changed image to nginx:1.28-alpine, added a label). After revision 3 deploys, the team notices that the web Pods are being OOMKilled and the application health is Degraded. The on-call engineer inspects the history, rolls back to revision 2, and writes the post-rollback investigation.

Tasks

Task 1 — Build three deployment history entries

LAB="$HOME/argocd-rollback-lab"
rm -rf "$LAB"
mkdir -p "$LAB"
cd "$LAB"

git init -b main
git config user.email 'ops@example.com'
git config user.name  'Ops'

mkdir -p app-source

# Revision 1: initial.
cat > app-source/web-deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27-alpine
          resources:
            limits:
              memory: 256Mi
          ports:
            - containerPort: 80
EOF

cat > app-source/web-service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 80
EOF

cat > application.yaml <<'EOF'
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: web
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/runbook-academy/argocd-rollback-lab.git
    targetRevision: main
    path: app-source
  destination:
    server: https://kubernetes.default.svc
    namespace: production
  syncPolicy:
    automated:
      prune: true
      selfHeal: true
    syncOptions:
      - CreateNamespace=true
EOF

git add application.yaml app-source/
git commit -m 'r1: initial deploy (nginx:1.27-alpine, 2 replicas)'

# Apply the Application CR and wait for sync.
kubectl apply -f application.yaml
sleep 45
argocd app sync web
sleep 30

# Revision 2: scale to 3 replicas.
cat > app-source/web-deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27-alpine
          resources:
            limits:
              memory: 256Mi
          ports:
            - containerPort: 80
EOF

git add app-source/web-deployment.yaml
git commit -m 'r2: scale to 3 replicas'

sleep 45
argocd app sync web
sleep 30

# Revision 3: change image to 1.28-alpine and add a label.
cat > app-source/web-deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
    version: v1.28
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
        version: v1.28
    spec:
      containers:
        - name: web
          image: nginx:1.28-alpine
          resources:
            limits:
              memory: 256Mi
          ports:
            - containerPort: 80
EOF

git add app-source/web-deployment.yaml
git commit -m 'r3: nginx 1.28-alpine (regression: OOMKilled)'

sleep 45
argocd app sync web
sleep 30

# Verify the history.
argocd app history web

The lab’s setup creates three sync history entries. The output of argocd app history web lists each entry with its ID, the source revision, the deployer, and the timestamp.

Task 2 — Inspect the application history

# check-shell-blocks: allow-invalid
cd "$HOME/argocd-rollback-lab"

cat > history-inspection.md <<'EOF'
# History inspection: web — 2026-08-22

This document is the on-call engineer's record of the
Argo CD history for the `web` application at the time
of the incident. The history is the input to the
rollback decision.

## History entries

| ID | Source revision | Deployer | Deployed at | Healthy |
|----|-----------------|----------|-------------|---------|
| 3  | r3 (1.28-alpine) | ci-bot | 2026-08-22 13:45 | Degraded |
| 2  | r2 (3 replicas) | ci-bot | 2026-08-22 12:30 | Healthy |
| 1  | r1 (initial)   | ci-bot | 2026-08-22 09:00 | Healthy |

## Chosen revision

The on-call engineer chooses **ID 2** (revision r2) as
the rollback target. Rationale:

- ID 2 is the last known-good state (image
  `nginx:1.27-alpine`, 3 replicas, no extra label).
- ID 1 is also known-good but has only 2 replicas;
  ID 2 has 3 (the team had scaled up for the
  deployment).
- ID 3 is the offending state (image `nginx:1.28-alpine`
  with the regression).

## Verification before rollback

The on-call engineer captures the current cluster state
before the rollback:

$ kubectl get deployment web -n production -o yaml spec.replicas: 3 spec.template.spec.containers[0].image: nginx:1.28-alpine spec.template.metadata.labels.version: v1.28 status.readyReplicas: 1 (degraded; 2 OOMKilled)


The current state is ID 3; the desired state is ID 2.

EOF

git add history-inspection.md
git commit -m 'rollback: history inspection'

The history inspection document is the on-call record. The chosen revision is ID 2; the rationale is captured in the document.

Task 3 — Build the rollback runbook

# check-shell-blocks: allow-invalid
cd "$HOME/argocd-rollback-lab"

cat > rollback-runbook.md <<'EOF'
# Rollback runbook: `argocd app rollback`

This runbook is the on-call engineer's reference for
rolling back a Kubernetes application managed by Argo
CD. The runbook is intentionally short: under pressure,
the engineer follows four steps (inspect, choose,
rollback, validate) and the team has a review at the
end of the incident.

## When to use this runbook

Use this runbook when:

- A regression was introduced by a recent deploy and
  the cluster state is unhealthy.
- The fix is to revert the cluster to a previous
  deployment (not to forward-fix).
- The application is managed by Argo CD (otherwise,
  use `kubectl rollout undo` or a Git revert).

Do not use this runbook when:

- The regression is a data integrity issue. Rollback
  does not revert database state; a database
  rollback or a forward-fix migration is required.
- The cluster has drifted from Git in a way that
  would cause the rollback to fail. Run the drift
  detection (Lab 21) first.
- The fix is permanent. Permanent fixes go through
  Git (a revert or a forward-fix PR).

## Step 1: inspect the history

argocd app history <APP_NAME>


The output is a table of sync entries. Each entry has
an ID, a source revision, a deployer, and a timestamp.
The on-call engineer notes the entry that is the
"last known-good" state.

## Step 2: choose the revision

The on-call engineer chooses the ID of the last
known-good state. The decision framework
(`rollback-decision-tree.md`) covers the cases:

- The previous entry is known-good: roll back to it.
- The previous entry is also unhealthy: roll back to
  the most recent known-good entry.
- No known-good entry exists: roll back is not the
  right answer; consider a forward-fix or a database
  rollback.

## Step 3: rollback

argocd app set <APP_NAME> —sync-policy none argocd app rollback <APP_NAME> <ID>


Argo CD refuses to roll back while automated sync is
enabled, so the first command disables it. The second
command takes the history ID as a positional argument;
it renders the manifests from the chosen revision and
applies them. The cluster is reverted without
modifying Git. The rollback is recorded in Argo CD's
history as a new entry (with the deployer "admin" or
the engineer's name). Automated sync stays off until
the post-incident Git PR reconciles the branch;
re-enabling it earlier re-applies the offending
revision.

If the rollback fails (for example, due to an
immutable field), the engineer runs
`argocd app diff <APP_NAME>` to see the diff, and
applies the change manually with `kubectl`.

## Step 4: validate

argocd app get <APP_NAME> kubectl get deployment <DEPLOYMENT_NAME> -n <NAMESPACE>


The application must reach `Synced: True, Healthy:
True`. The cluster state must match the chosen
revision (image tag, replicas, env vars). The
on-call engineer captures the before/after in the
evidence bundle (`rollback-evidence.md`).

## Post-incident: Git PR

Within 24 hours of the rollback, the team opens a Git
PR to either revert the offending commit or to fix the
regression. The PR is the permanent record; the
rollback is the immediate mitigation. After the PR
merges, re-enable automated sync:

argocd app set <APP_NAME> —sync-policy automated


EOF

git add rollback-runbook.md
git commit -m 'rollback: runbook'

The runbook is the on-call reference. The four steps are the spine; the argocd and kubectl commands are the verbs.

Task 4 — Build the rollback script

# check-shell-blocks: allow-invalid
cd "$HOME/argocd-rollback-lab"

cat > argocd-rollback.sh <<'EOF'
#!/usr/bin/env bash
#
# argocd-rollback.sh — perform a GitOps rollback via
# `argocd app rollback` and validate the cluster state.
#
# Required: argocd CLI authenticated; jq; kubectl;
# a Slack webhook URL in $SLACK_WEBHOOK_URL.
#
# Usage: APP=web TARGET_ID=2 OPERATOR=jane.doe \
#        DEPLOYMENT_NAME=web NAMESPACE=production \
#        ./argocd-rollback.sh

set -euo pipefail

: "${APP:?APP is required}"
: "${TARGET_ID:?TARGET_ID is required}"
: "${OPERATOR:?OPERATOR is required}"
DEPLOYMENT_NAME="\${DEPLOYMENT_NAME:-$APP}"
NAMESPACE="\${NAMESPACE:-production}"
SLACK_WEBHOOK_URL="\${SLACK_WEBHOOK_URL:-}"

NOW="$(date -u +%Y-%m-%dT%H:%M:%SZ)"

echo "=== rolling back ${APP} to history id ${TARGET_ID} ==="
echo "  now:       ${NOW}"
echo "  operator:  ${OPERATOR}"

# Capture before-state.
BEFORE_IMAGE="$(kubectl get deployment "$DEPLOYMENT_NAME" \
  -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.containers[0].image}')"
BEFORE_REPLICAS="$(kubectl get deployment "$DEPLOYMENT_NAME" \
  -n "$NAMESPACE" -o jsonpath='{.spec.replicas}')"
BEFORE_READY="$(kubectl get deployment "$DEPLOYMENT_NAME" \
  -n "$NAMESPACE" -o jsonpath='{.status.readyReplicas}')"

echo "before:"
echo "  image:    ${BEFORE_IMAGE}"
echo "  replicas: ${BEFORE_REPLICAS}"
echo "  ready:    ${BEFORE_READY}"

# Inspect history.
echo "=== history ==="
argocd app history "$APP"

# Disable automated sync: `argocd app rollback` refuses to
# run while automated sync is enabled, and automated sync
# would re-apply the offending revision on the next tick.
argocd app set "$APP" --sync-policy none

# Roll back (the history ID is a positional argument).
argocd app rollback "$APP" "$TARGET_ID"

# Wait for sync.
echo "waiting for sync..."
sleep 30

# Wait for Healthy.
for i in $(seq 1 30); do
  HEALTH="$(argocd app get "$APP" -o json | \
    jq -r '.status.health.status // "Unknown"')"
  SYNC="$(argocd app get "$APP" -o json | \
    jq -r '.status.sync.status // "Unknown"')"
  echo "  attempt ${i}: sync=${SYNC} health=${HEALTH}"
  if [ "$SYNC" = "Synced" ] && [ "$HEALTH" = "Healthy" ]; then
    break
  fi
  sleep 10
done

if [ "$SYNC" != "Synced" ] || [ "$HEALTH" != "Healthy" ]; then
  echo "ERROR: ${APP} did not reach Synced+Healthy in 5 minutes" >&2
  exit 1
fi

# Capture after-state.
AFTER_IMAGE="$(kubectl get deployment "$DEPLOYMENT_NAME" \
  -n "$NAMESPACE" -o jsonpath='{.spec.template.spec.containers[0].image}')"
AFTER_REPLICAS="$(kubectl get deployment "$DEPLOYMENT_NAME" \
  -n "$NAMESPACE" -o jsonpath='{.spec.replicas}')"
AFTER_READY="$(kubectl get deployment "$DEPLOYMENT_NAME" \
  -n "$NAMESPACE" -o jsonpath='{.status.readyReplicas}')"

echo "after:"
echo "  image:    ${AFTER_IMAGE}"
echo "  replicas: ${AFTER_REPLICAS}"
echo "  ready:    ${AFTER_READY}"

# Announce in Slack.
if [ -n "$SLACK_WEBHOOK_URL" ]; then
  curl -s -X POST -H 'Content-Type: application/json' \
    --data "$(jq -n \
      --arg app "$APP" \
      --arg id "$TARGET_ID" \
      --arg ts "$NOW" \
      --arg op "$OPERATOR" \
      --arg bi "$BEFORE_IMAGE" \
      --arg ai "$AFTER_IMAGE" \
      '{text: ("[ROLLBACK] rolled back `\` + $app + "` to history id " + $id + " at " + $ts + ". Operator: " + $op + ". Image: " + $bi + " -> " + $ai)}')" \
    "$SLACK_WEBHOOK_URL" >/dev/null
fi

echo "rollback complete; ${APP} is Synced+Healthy at history id ${TARGET_ID}."
echo "NOTE: automated sync is DISABLED for ${APP}. After the Git revert/fix"
echo "PR merges, re-enable it: argocd app set ${APP} --sync-policy automated"
EOF

chmod +x argocd-rollback.sh

git add argocd-rollback.sh
git commit -m 'rollback: automated rollback script'

The script automates the four steps of the runbook: capture before-state, inspect history, rollback, and validate. It disables automated sync before the rollback (Argo CD refuses to roll back while automated sync is enabled) and leaves it disabled: re-enabling happens only after the follow-up Git PR merges. The script fails loudly if the application does not reach Synced+Healthy within 5 minutes.

Task 5 — Perform the rollback

cd "$HOME/argocd-rollback-lab"

APP=web
TARGET_ID=2
OPERATOR=jane.doe
DEPLOYMENT_NAME=web
NAMESPACE=production
SLACK_WEBHOOK_URL="\${SLACK_WEBHOOK_URL:-}"

APP="$APP" TARGET_ID="$TARGET_ID" OPERATOR="$OPERATOR" \
  DEPLOYMENT_NAME="$DEPLOYMENT_NAME" NAMESPACE="$NAMESPACE" \
  SLACK_WEBHOOK_URL="$SLACK_WEBHOOK_URL" \
  ./argocd-rollback.sh

The script performs the rollback, validates the cluster, and announces in Slack (if configured). The output is captured in Task 6’s evidence bundle.

Task 6 — Capture the rollback evidence

# check-shell-blocks: allow-invalid
cd "$HOME/argocd-rollback-lab"

cat > rollback-evidence.md <<'EOF'
# Rollback evidence: web — 2026-08-22

This document is the canonical evidence bundle for the
rollback performed at 14:55 UTC on 2026-08-22. The
bundle captures the before-state, the rollback action,
the after-state, and the validation.

## Before-state

$ kubectl get deployment web -n production -o yaml spec.replicas: 3 spec.template.spec.containers[0].image: nginx:1.28-alpine spec.template.metadata.labels.version: v1.28 status.readyReplicas: 1 status.conditions:

  • type: Progressing status: “False” reason: ProgressDeadlineExceeded

The cluster was unhealthy: 1 of 3 replicas ready, 2
OOMKilled. The image was `nginx:1.28-alpine`.

## Rollback action

$ argocd app set web —sync-policy none $ argocd app rollback web 2


Automated sync was disabled first — Argo CD refuses
to roll back while it is enabled, and automated sync
would re-apply the offending revision on the next
tick. The rollback command (history id 2 as a
positional argument) rendered the manifests from
revision r2 and applied them to the cluster. The
rollback was recorded in the history as a new entry
with deployer "admin" (jane.doe).

## After-state

$ kubectl get deployment web -n production -o yaml spec.replicas: 3 spec.template.spec.containers[0].image: nginx:1.27-alpine spec.template.metadata.labels: (no version label) status.readyReplicas: 3 status.conditions:

  • type: Available status: “True”

The cluster is healthy: 3 of 3 replicas ready, image
back to `nginx:1.27-alpine`.

## Validation

$ argocd app get web Name: web Project: default Server: https://kubernetes.default.svc Namespace: production Sync Status: Synced Health Status: Healthy

$ argocd app history web ID DATE REVISION DEPLOYER 3 2026-08-22 14:55:00 r2 (rollback) jane.doe 2 2026-08-22 12:30:00 r2 ci-bot 1 2026-08-22 09:00:00 r1 ci-bot


The application is `Synced: True, Healthy: True`. The
history shows the rollback as a new entry (ID 3 with
deployer "jane.doe"). The Git repository remains at
revision r3 (the offending commit is not reverted; the
PR is opened in Task 7). Automated sync remains
disabled until that PR merges; it is then re-enabled
with `argocd app set web --sync-policy automated`.

EOF

git add rollback-evidence.md
git commit -m 'rollback: evidence bundle'

The evidence bundle is the on-call record. The before/after and the validation are the artefacts the team reviews at the post-incident review.

Task 7 — Author the post-rollback investigation

# check-shell-blocks: allow-invalid
cd "$HOME/argocd-rollback-lab"

cat > post-rollback-investigation.md <<'EOF'
# Post-rollback investigation: web — 2026-08-22

This document is the team's investigation of the
regression that triggered the rollback. The
investigation links the regression to the offending
commit and identifies the root cause.

## Summary

The team rolled back the `web` application from
revision r3 (`nginx:1.28-alpine`) to revision r2
(`nginx:1.27-alpine`) at 14:55 UTC on 2026-08-22.
The regression was a memory leak in `nginx:1.28-alpine`
that caused the Pods to OOMKill under load. The
rollback restored the application to a known-good
state.

## Timeline

- 13:45 — r3 deployed (`nginx:1.28-alpine`).
- 14:30 — first OOMKill observed.
- 14:35 — PagerDuty page fired.
- 14:40 — on-call engineer (jane.doe) acknowledged.
- 14:50 — investigation complete; regression linked to
  image change.
- 14:55 — rollback to history id 2 performed.
- 15:00 — cluster healthy; incident resolved.
- 15:30 — post-rollback investigation opened.

## Root cause

The `nginx:1.28-alpine` image has a known memory leak
in the gzip module (CVE-2026-1234). The leak is
triggered when the module is enabled and the request
rate is high. The team's `web` application enables the
gzip module by default; the production traffic rate
triggered the leak within 4 hours of deploy.

## Detection

The detection was within the team's SLO (60 seconds
from first OOMKill to the page). The alert is
`kube_pod_container_status_terminated_reason ==
"OOMKilled"`. The alert has been in place since
2026-01; the team has a runbook for it.

## Response

The on-call engineer chose rollback over forward-fix
because:

- The fix (image bump to `nginx:1.28.1-alpine`) was
  not yet built or signed.
- The customer impact was 35% error rate; rollback
  was the fastest path to recovery.
- The data integrity question was not relevant
  (the regression was HTTP 5xx, not data loss).

The rollback was performed in 5 minutes (history
inspection, rollback command, validation). The team's
SLO for rollback is 15 minutes; the actual time was
within SLO.

## Permanent fix

The team opens a PR to update the image to
`nginx:1.28.1-alpine` (which fixes the memory leak).
The PR is reviewed and merged; Argo CD syncs the new
image; the history records the new revision.

## Lessons learned

1. The team's pre-prod load test should have caught
   the memory leak. The test runs at 2× production
   traffic; the leak triggered at 4×. The test
   threshold is increased to 5×.
2. The team did not have a `pinned` image tag; the
   `1.28-alpine` tag moved without notice. The team
   pins image tags to specific digests.
3. The team's CVE subscription did not include
   nginx. The team subscribes to the nginx security
   announcements.

## Action items

- [x] jane.doe: rollback (done at 14:55).
- [ ] platform-team: increase pre-prod load test
  threshold to 5× (due 2026-08-29).
- [ ] platform-team: pin all image tags to digests
  (due 2026-09-12).
- [ ] sre-team: subscribe to nginx security
  announcements (due 2026-08-25).

EOF

git add post-rollback-investigation.md
git commit -m 'rollback: post-rollback investigation'

The post-rollback investigation is the team’s record. The root cause, the response, the permanent fix, and the lessons learned are the fields the team reviews at the post-incident review.

Task 8 — Build the rollback decision tree

# check-shell-blocks: allow-invalid
cd "$HOME/argocd-rollback-lab"

cat > rollback-decision-tree.md <<'EOF'
# Rollback decision tree

This document is the team's reference for choosing
between rollback, revert, and forward-fix. The decision
is made under pressure; the tree is the on-call
engineer's guide.

## Question 1: is the cluster state recoverable by
rollback?

If the cluster state is recoverable by re-applying a
previous Git revision, rollback is on the table. If
not (for example, a PersistentVolume was resized and
cannot be shrunk), rollback is not the right answer.

## Question 2: is the data integrity at risk?

Rollback does not revert database state. If the
regression involved a database migration or a data
corruption, rollback is not the right answer. The
on-call engineer considers:

- Forward-fix migration: apply a new migration that
  undoes the offending one.
- Database snapshot restore: restore the database
  from a pre-deploy snapshot.
- Manual data repair: identify and repair the
  corrupted data.

## Question 3: is the fix in a known-good image or
manifest?

If the fix is in a new image (for example, a
patched `nginx:1.28.1-alpine`) and the image is built,
signed, and pushed, forward-fix is faster than
rollback. The on-call engineer:

- Builds the fix in the CI pipeline.
- Promotes the image through staging → production.
- The new deploy replaces the offending revision.

If the fix is not yet built, rollback is the path.

## Question 4: is the regression customer-visible?

If the regression is customer-visible (HTTP 5xx, data
corruption), rollback is the priority. The team's SLO
for customer-visible incidents is 15 minutes; the
rollback path is faster than the forward-fix path.

If the regression is internal-only (for example, a
metric that is not collected), the team can take
the forward-fix path.

## Decision matrix

| Cluster recoverable | Data integrity | Fix available | Customer-visible | Decision |
|---------------------|----------------|---------------|------------------|----------|
| Yes | No | Yes | Yes | Forward-fix (fast) |
| Yes | No | Yes | No | Forward-fix |
| Yes | No | No | Yes | Rollback |
| Yes | No | No | No | Rollback or forward-fix |
| Yes | Yes | (any) | (any) | Database snapshot + forward-fix |
| No | (any) | (any) | (any) | Forward-fix or restore |

## Default

The team's default is rollback when in doubt. The
rollback is faster (5 minutes vs 30 minutes for
forward-fix) and the Git PR is opened in parallel.
The team's discipline: rollback first, postmortem
second, PR third.

EOF

git add rollback-decision-tree.md
git commit -m 'rollback: decision tree'

The decision tree is the on-call reference. The matrix at the bottom is the answer to “what do I do when I see this regression?”. The default is rollback when in doubt.

Task 9 — Open the Git PR for the permanent fix

# check-shell-blocks: allow-invalid
cd "$HOME/argocd-rollback-lab"

# Revert the offending commit.
cat > app-source/web-deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 3
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.28.1-alpine
          resources:
            limits:
              memory: 256Mi
          ports:
            - containerPort: 80
EOF

git add app-source/web-deployment.yaml
git commit -m 'fix: bump nginx to 1.28.1-alpine (CVE-2026-1234)'

# Show the PR description.
cat > pr-description.md <<'EOF'
# PR: bump nginx to 1.28.1-alpine

## Summary

The team rolled back `web` from `nginx:1.28-alpine` to
`nginx:1.27-alpine` at 14:55 UTC on 2026-08-22 due to a
memory leak in `1.28-alpine` (CVE-2026-1234). This PR
bumps the image to `1.28.1-alpine`, which contains the
upstream fix.

## Changes

- `app-source/web-deployment.yaml`: change image to
  `nginx:1.28.1-alpine`.

## Validation

- Pre-prod load test (5× production traffic for 4
  hours): no OOMKill.
- Pre-prod security scan: no critical CVEs.
- Image signature verified (cosign verify).

## Rollback plan

The image is pinned to a digest; the rollback path is
the `argocd app rollback` procedure (Lab 23).

## References

- CVE-2026-1234
- Post-rollback investigation:
  `post-rollback-investigation.md`

EOF

git add pr-description.md
git commit -m 'pr: description for the nginx 1.28.1 bump'

# After the PR merges and Argo CD syncs the fixed
# revision, re-enable automated sync (disabled since
# the rollback in Task 5):
argocd app set web --sync-policy automated

The PR is the permanent record. The rollback was the immediate mitigation; the PR is the closing-the-gap artefact. Re-enabling automated sync is the last step: done before the fix (or a revert) merges, the next sync would re-apply the offending revision r3. The team’s discipline: no rollback without a follow-up PR.

Task 10 — Validate the deliverables

cd "$HOME/argocd-rollback-lab"

# Verify the runbook has all four steps.
grep -c "^## Step" rollback-runbook.md
# expected: 4

# Verify the rollback script passes syntax check.
bash -n argocd-rollback.sh && echo "rollback: syntax ok"

# Verify the evidence bundle is in place.
test -s rollback-evidence.md && echo "evidence: ok"

# Verify the post-rollback investigation has all
# required fields.
grep -c "^## " post-rollback-investigation.md
# expected: 7 (Summary, Timeline, Root cause, Detection,
#              Response, Permanent fix, Lessons learned)

# Verify the decision tree has the matrix.
grep -c "^| " rollback-decision-tree.md
# expected: 6+ rows

# Verify the application is Synced+Healthy.
argocd app get web -o json | \
  jq -r '.status.sync.status + " " + .status.health.status'
# expected: Synced Healthy

# Verify automated sync was re-enabled after the fix
# PR (Task 9); it was disabled for the rollback.
kubectl -n argocd get application web \
  -o jsonpath='{.spec.syncPolicy.automated}'
# expected: non-empty (automated sync restored)

The deliverables are validated: the runbook has all four steps, the script passes syntax check, the evidence bundle is in place, the investigation has all required fields, the decision tree has the matrix, the application is Synced+Healthy, and automated sync has been restored after the fix merged.

Task 11 — Capture the deliverables

cd "$HOME/argocd-rollback-lab"

cp rollback-runbook.md \
   argocd-rollback.sh \
   rollback-evidence.md \
   post-rollback-investigation.md \
   rollback-decision-tree.md \
   history-inspection.md \
   pr-description.md \
   "$HOME/"

ls -l "$HOME"/rollback-runbook.md \
       "$HOME"/argocd-rollback.sh \
       "$HOME"/rollback-evidence.md \
       "$HOME"/post-rollback-investigation.md \
       "$HOME"/rollback-decision-tree.md

The deliverables are in $HOME/.

Validation

  • rollback-runbook.md documents all four steps: inspect, choose, rollback, validate.
  • argocd-rollback.sh is executable, has set -euo pipefail, and validates the cluster state.
  • rollback-evidence.md captures before/after and the validation.
  • post-rollback-investigation.md documents the regression and the permanent fix.
  • rollback-decision-tree.md documents the decision matrix.
  • history-inspection.md documents the history and the chosen revision.
  • pr-description.md is the description for the follow-up PR.

Expected Outcome

A rollback runbook, an automated rollback script, an evidence bundle, a post-rollback investigation, a decision tree, a history inspection document, and a PR description.

$HOME/argocd-rollback-lab/
├── rollback-runbook.md             # the on-call reference
├── argocd-rollback.sh              # the automated script
├── rollback-evidence.md            # the evidence bundle
├── post-rollback-investigation.md  # the regression postmortem
├── rollback-decision-tree.md       # the decision framework
├── history-inspection.md           # the history record
└── pr-description.md               # the follow-up PR

The runbook is the spine; the script is the verb; the evidence and the investigation are the institutional knowledge.

Troubleshooting

argocd app history shows only the initial deployment. The application was not synced via Argo CD; the history records syncs, not commits. The on-call engineer uses kubectl rollout history as a fallback.

argocd app rollback refuses to run because automated sync is enabled. This is by design: rollback cannot run while the sync policy is automated, because the controller would immediately re-sync to the branch tip. Disable automated sync (argocd app set web --sync-policy none), roll back, and re-enable automated sync only after the follow-up Git PR merges.

The rollback fails with field is immutable. The manifest changes an immutable field. Either set Replace=true in syncOptions or accept the immutability and edit the resource via a separate manifest.

The cluster is not healthy after the rollback. The chosen revision is not actually known-good. The on-call engineer rolls back further (to a previous revision) or considers a forward-fix.

The PR is not accepted. The team reviews the PR within 24 hours. If the format is not accepted, update the PR description and resubmit.

argocd app rollback modifies the Git state. This should not happen; the rollback is a cluster-side action. If the Git state is modified, the team reviews the Argo CD configuration for any sync hooks that auto-commit.

Cleanup

LAB="$HOME/argocd-rollback-lab"

cp -r "$LAB"/* "$HOME"/ 2>/dev/null
rm -rf "$LAB"

# Revert the Git state to revision r2 (the known-good).
git checkout main
git revert --no-edit HEAD

If the kind cluster is no longer needed, delete it:

kind delete cluster --name argocd-lab

What You Learned

  • Argo CD’s history is the rollback lever. The history records every sync with the source revision, the deployed manifests, and the deploy time. The rollback chooses an entry and re-applies the manifests.
  • Rollback is a cluster-side action. The Git repository is not modified by the rollback. The team’s discipline: open a Git PR in parallel to either revert the offending commit or to fix the regression — the GitOps-preferred rollback is that git revert plus a sync.
  • Automated sync must be off for the rollback. Argo CD refuses to roll back while automated sync is enabled. The flow: disable it (argocd app set web --sync-policy none), roll back (the history ID is positional: argocd app rollback web 2), merge the Git PR, then re-enable it (argocd app set web --sync-policy automated). Re-enabling before the branch is reconciled re-applies the bad revision.
  • The decision tree is the on-call reference. The matrix covers the cases (cluster recoverable, data integrity, fix available, customer-visible). The default is rollback when in doubt.
  • The evidence bundle is the audit trail. The before/after, the rollback action, and the validation are the artefacts the team reviews at the post-incident review.
  • The post-rollback investigation links the regression to the deploy. The root cause, the detection, the response, the permanent fix, and the lessons learned are the fields the team fills in.
  • The PR is the permanent record. The rollback is the immediate mitigation; the PR is the closing-the-gap artefact. The team’s discipline: no rollback without a follow-up PR.
  • Pinned image tags prevent surprise regressions. A 1.28-alpine tag can move without notice; a digest-pinned image cannot. The team’s policy: all production images are pinned to digests.

Deliverables

  • · rollback-runbook.md — the on-call runbook for `argocd app rollback`
  • · argocd-rollback.sh — the script that performs the rollback and validates the cluster state
  • · rollback-evidence.md — the evidence bundle for the scenario rollback
  • · post-rollback-investigation.md — the postmortem for the regression
  • · rollback-decision-tree.md — the decision framework for choosing between rollback, revert, and forward-fix
  • · history-inspection.md — the documented history and the chosen revision

Verification status

Last reviewed
2026-08-25
Executed end to end
not yet run on hardware

The commands and configuration here have been reviewed against the verified software versions, but nobody has run this lab start to finish on a system meeting its prerequisites. Treat the Expected Outcome as the intended result rather than an observed one, and keep the Cleanup section to hand.