Scenario 04 — Migrate an app across clusters or namespaces
Move a stateful app, data and all, somewhere new. In disaster recovery the app keeps its name and namespace. A migration is different: it changes the app’s coordinate, either to a new namespace (billing becomes payments) or to a whole new cluster reusing the same repository.
That coordinate change is the catch.
The flow
Section titled “The flow”flowchart LR
subgraph src[Source — namespace billing]
S[(snapshots<br/>postgres-data@billing)]
end
subgraph dst[Destination — namespace payments]
REPO[Repository<br/>same bucket, connect] --> RST[Restore<br/>by raw identity]
RST --> PVC[PVC postgres-data]
BC[SnapshotPolicy<br/>identity pinned to billing] --> SCH[SnapshotSchedule]
end
S -. restore by identity .-> RST
Applied in the destination namespace, the bundle does three things. It connects a Repository to the same bucket. It restores the source’s latest snapshot by identity into a new PVC. Then it sets up a SnapshotPolicy and a SnapshotSchedule to protect the app going forward.
The decision that matters: continue or fork the lineage
Section titled “The decision that matters: continue or fork the lineage”The new SnapshotPolicy’s identity decides whether the destination’s future snapshots extend the original timeline or start a fresh one:
| Choice | How | Result |
|---|---|---|
| Continue the lineage | pin identity to the original username/hostname (the bundle does this) |
new snapshots dedup against the carried-over history; one logical timeline. |
| Fork a fresh lineage | delete the identity block; let it default to the destination namespace |
a clean new timeline under the new coordinate. |
# Scenario 04 — Migrate an app to a new cluster or namespace## Unlike DR (scenario 03, where the app's name and namespace are unchanged), a# migration MOVES the app somewhere with a DIFFERENT coordinate — a new namespace# (`payments`) or a new cluster — while reusing the same repository. The catch is# kopia's identity model: a snapshot is stored under `username@hostname:path`,# and hostname defaults to the SOURCE namespace. So in the destination you cannot# resolve the source's snapshots by `fromPolicy` (its identity would compute the# DESTINATION namespace). You restore by the RAW identity instead.## This bundle, applied in the destination namespace, does the move:# 1. Repository — connects to the SAME bucket the source wrote to.# 2. Restore — by source.identity (the source's username@hostname:path) into# a new PVC in the destination. This is the one-time data carry.# 3. SnapshotPolicy + SnapshotSchedule — protect the app going forward.## IDENTITY CHOICE for the new SnapshotPolicy (the important decision):# * To CONTINUE the original snapshot lineage (keep dedup history, one logical# timeline), pin `identity` to the ORIGINAL username/hostname — done below.# * To start a FRESH lineage under the new coordinate, delete the `identity`# block and let it default to the destination namespace.## WARNING: pin-to-original is for a MOVE — decommission the source first. Two# clusters writing the SAME identity concurrently corrupts the snapshot timeline.## Field shapes verified against crates/api. source.identity REQUIRES an explicit# spec.repository (there's no Backup/SnapshotPolicy to infer it from).---apiVersion: v1kind: Secretmetadata: name: postgres-repo-creds namespace: paymentstype: OpaquestringData: AWS_ACCESS_KEY_ID: "REPLACE_ME" AWS_SECRET_ACCESS_KEY: "REPLACE_ME" KOPIA_PASSWORD: "REPLACE_ME_with_the_ORIGINAL_repo_password"---apiVersion: kopiur.home-operations.com/v1alpha1kind: Repositorymetadata: name: postgres-primary namespace: paymentsspec: backend: s3: bucket: my-backups prefix: billing/postgres/ # the SAME prefix the source app used endpoint: s3.us-east-1.amazonaws.com region: us-east-1 auth: secretRef: name: postgres-repo-creds encryption: passwordSecretRef: name: postgres-repo-creds key: KOPIA_PASSWORD create: enabled: false # connect to the existing repo; do not initialize---# One-time data carry: restore the source app's latest snapshot by its RAW# identity (username/hostname as recorded by the SOURCE in namespace `billing`).apiVersion: kopiur.home-operations.com/v1alpha1kind: Restoremetadata: name: postgres-migrate-in namespace: paymentsspec: repository: kind: Repository name: postgres-primary source: identity: username: postgres-data # the SOURCE SnapshotPolicy name hostname: billing # the SOURCE namespace (NOT payments) sourcePath: /pvc/postgres-data # PVC sources record /pvc/<pvcName> # snapshotID: k1f1ec0a8 # pin an exact snapshot, or omit for latest target: pvc: name: postgres-data storageClassName: fast-ssd capacity: 100Gi accessModes: - ReadWriteOnce policy: onMissingSnapshot: Fail # the snapshot must be there — this is the migration---apiVersion: kopiur.home-operations.com/v1alpha1kind: SnapshotPolicymetadata: name: postgres-data namespace: paymentsspec: repository: name: postgres-primary sources: - pvc: name: postgres-data # CONTINUE the original lineage: keep writing under billing/postgres-data so the # new cluster's snapshots dedup against the carried-over history. Remove this # block to fork a fresh timeline under payments/postgres-data instead. identity: username: postgres-data hostname: billing retention: keepDaily: 14 keepWeekly: 6---apiVersion: kopiur.home-operations.com/v1alpha1kind: SnapshotSchedulemetadata: name: postgres-data-nightly namespace: paymentsspec: policyRef: name: postgres-data schedule: cron: "H 2 * * *" jitter: 30m runOnCreate: falseVerify the carry-over
Section titled “Verify the carry-over”$ kubectl get restore postgres-migrate-in -n payments -wNAME PHASE AGEpostgres-migrate-in Resolving 3spostgres-migrate-in Restoring 11spostgres-migrate-in Completed 38s
$ kubectl get pvc postgres-data -n paymentsNAME STATUS VOLUME CAPACITY AGEpostgres-data Bound pvc-... 100Gi 40sStart the app in payments against the restored PVC, confirm it, then tear down the source. The first scheduled backup in payments will dedup against the existing data rather than re-uploading it.
See also
Section titled “See also”- Restores →
identitysource: the raw-identity restore mode. - Backups → identity: how identity is resolved, and the fork guards that protect existing history.
- Scenario 05, adopt an existing repo: a close cousin, for when the “source” is foreign tooling rather than another Kopiur cluster.
- Share one repository across clusters: the active-active or standby shape this page’s danger box points at, instead of a one-time move.