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.
Moving an app vs. running it in several clusters at once
This page is a one-time move. The app runs in exactly one place before and after. The destination takes over the original identity, and you decommission the source, as described below.
If you instead want several clusters backing up to the same repository at the same time, whether active-active or a warm DR standby, that is a different and also supported shape. Share one repository across clusters uses identityDefaults.cluster so each cluster writes under its own distinct identity and never collides. That is exactly what this page's continue-the-lineage approach forbids, as the danger box below explains.
Why you can't just use fromPolicy in the destination
kopia stores each snapshot under username@hostname:path, and hostname defaults to the source namespace. In the destination namespace, a fromPolicy restore would compute the destination's identity and find nothing.
So the one-time data carry restores by the raw source.identity, meaning the source's own username plus hostname. That is what identity mode is for.
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¶
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. |
Continue-lineage is for a MOVE, not active-active
If you pin the destination to the original identity, decommission the source first. Two clusters writing the same username@hostname:path at once will corrupt the snapshot timeline, because kopia has no cross-cluster write coordination of its own. Pinning to the original identity means "the app lives here now", not "the app runs in both places".
If you actually want two or more clusters writing to this repository at the same time, don't reach for a shared identity. Set identityDefaults.cluster on the repository instead, so each cluster gets its own distinct <namespace>.<cluster> identity and never collides. See Share one repository across clusters for that shape, including the safe order of operations to turn it on for a repository already in production.
# 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: v1
kind: Secret
metadata:
name: postgres-repo-creds
namespace: payments
type: Opaque
stringData:
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/v1alpha1
kind: Repository
metadata:
name: postgres-primary
namespace: payments
spec:
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/v1alpha1
kind: Restore
metadata:
name: postgres-migrate-in
namespace: payments
spec:
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/v1alpha1
kind: SnapshotPolicy
metadata:
name: postgres-data
namespace: payments
spec:
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/v1alpha1
kind: SnapshotSchedule
metadata:
name: postgres-data-nightly
namespace: payments
spec:
policyRef:
name: postgres-data
schedule:
cron: "H 2 * * *"
jitter: 30m
runOnCreate: false
Verify the carry-over¶
$ kubectl get restore postgres-migrate-in -n payments -w
NAME PHASE AGE
postgres-migrate-in Resolving 3s
postgres-migrate-in Restoring 11s
postgres-migrate-in Completed 38s
$ kubectl get pvc postgres-data -n payments
NAME STATUS VOLUME CAPACITY AGE
postgres-data Bound pvc-... 100Gi 40s
Start 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.
Finding the source's exact identity
If you're unsure what the source recorded, read it off a source Snapshot's status, or run kopia snapshot list against the repository. For a PVC source it is <config-name>@<namespace>:/pvc/<pvcName>, unless the source pinned a custom identity.
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.