Skip to content

Scenario 07 — Point-in-time rollback

You need a specific moment, not "yesterday." A bad deploy at 14:30 quietly corrupted data over the next hour. You want the volume exactly as it was at 14:00 — just before things went wrong — and you don't want to scroll through Snapshot CRs guessing which one that was.

This is what source.fromPolicy with asOf is for: it resolves through the SnapshotPolicy's identity (so it works even if the relevant Snapshot CR has aged out of the catalog) and picks the newest snapshot at or before an instant. As always, restore into a side-by-side PVC and verify before cutting over.

Step 1 — Choose the instant

You don't list snapshots — you name the time. asOf takes an RFC3339 timestamp and resolves to the newest snapshot at or before it. (Prefer counting backwards? Use offset: 0 = latest, 1 = previous, and so on.)

asOf vs offset

Use asOf when you know when things were good ("just before the 14:30 deploy"). Use offset when you know how many snapshots back ("the one before last"). Set one, not both.

Step 2 — Restore into a clone and verify

The bundle restores into a fresh postgres-data-1400 PVC; the live volume is untouched. Because fromPolicy defaults to onMissingSnapshot: Continue (deploy-or-restore), a deliberate rollback sets it to Fail so an instant with no snapshot is a loud error, not a silent empty volume.

# Scenario 07 — Point-in-time rollback (roll an app back to a known-good moment)
#
# A bad deploy at 14:30 corrupted the data over the next hour. You don't want
# "yesterday" and you don't want to hunt for the exact Snapshot CR — you want
# "whatever the data looked like at 14:00, just before it went wrong."
#
# `source.fromPolicy` resolves through the SnapshotPolicy's IDENTITY (not a specific
# Snapshot CR), so it works even if that Backup has aged out of the catalog. `asOf`
# picks the newest snapshot AT OR BEFORE an instant; `offset` picks by position
# (0 = latest, 1 = previous, ...). Restore into a NEW PVC and compare before cutting
# over — never overwrite the live volume on the first attempt.
#
# Field shapes verified against crates/api: source.fromPolicy =
# { name, namespace?, asOf?, offset? }; target.pvc creates a new PVC.
---
apiVersion: kopiur.home-operations.com/v1alpha1
kind: Restore
metadata:
  name: postgres-rollback-1400
  namespace: billing
spec:
  source:
    fromPolicy:
      name: postgres-data # the SnapshotPolicy protecting the live volume
      asOf: 2026-05-20T14:00:00Z # the last good moment, just before the bad deploy
      # offset: 1                # ...or by position instead of a timestamp
  target:
    pvc:
      name: postgres-data-1400 # a side-by-side clone — the live PVC is untouched
      storageClassName: fast-ssd
      capacity: 100Gi
      accessModes:
        - ReadWriteOnce
  policy:
    # fromPolicy DEFAULTS to Continue (deploy-or-restore). For a deliberate rollback
    # you want it to FAIL loudly if nothing matches that instant.
    onMissingSnapshot: Fail
    waitTimeout: 5m
#
# Watch it, then verify against the clone before you cut over:
#   kubectl get restore postgres-rollback-1400 -n billing -w
#   # spin a throwaway psql pointed at postgres-data-1400, confirm the data is right
#
# Cut over once you're satisfied: scale the app down, then either repoint it at the
# clone PVC, or do an in-place mirror restore into the live PVC (example 15):
#
#   spec:
#     source: { fromPolicy: { name: postgres-data, asOf: 2026-05-20T14:00:00Z } }
#     target: { pvcRef: { name: postgres-data } }   # the EXISTING live PVC
#     options: { enableFileDeletion: true }          # exact mirror of the snapshot
$ kubectl get restore postgres-rollback-1400 -n billing -w
NAME                     PHASE        AGE
postgres-rollback-1400   Resolving    2s
postgres-rollback-1400   Restoring    9s
postgres-rollback-1400   Completed    44s

Point a throwaway client at the clone PVC and confirm the data is the moment you wanted.

Step 3 — Cut over

Once you trust the clone, scale the app down and either repoint it at the clone, or do an in-place mirror restore into the live PVC — target.pvcRef + options.enableFileDeletion: true to make the live volume an exact mirror of the chosen instant (see example 15). The in-place form is shown commented at the bottom of the bundle.

Never roll back in place on the first try

Restoring over a mounted, running database is a corruption hazard, and if you pick the wrong instant the original is gone too. Clone, verify, then cut over.

See also