Scenario 04 — Migrate an app across clusters or namespaces¶
Move a stateful app — data and all — somewhere new. Unlike
disaster recovery, where the app keeps its name and
namespace, a migration changes the app's coordinate: a new namespace
(billing → payments), or 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 at a time,
before and after — the destination takes over the original identity and the
source is decommissioned (below). If you instead want several clusters
backing up to the same repository at the same time — active-active, or a
warm DR standby — that's a different, supported shape:
Share one repository across clusters
uses identityDefaults.cluster so each cluster writes under its own distinct
identity and never collides, which is exactly what this page's
continue-the-lineage approach forbids (see the danger box below).
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 (the source's
username + hostname), which is exactly 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: connects a Repository to the
same bucket, restores the source's latest snapshot by identity into a new PVC,
then sets up a SnapshotPolicy + SnapshotSchedule to protect the app going forward.
The decision that matters: continue or fork the lineage¶
The new SnapshotPolicy's identity determines 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 concurrently
corrupt the snapshot timeline — kopia has no cross-cluster write coordination of
its own. Pin-to-original 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 supported 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 kopia snapshot list against the repo. For a PVC source it's
<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 when the "source" is foreign tooling rather than another Kopiur cluster.
- Share one repository across clusters — the active-active/standby shape this page's danger box points at, instead of a one-time move.