Data Migration

The data paths for migrating a running Zalando cluster to CloudNativePG. Manifest conversion is covered in Manifest mapping.

Choosing a path

PathDowntimeSize sweet spotPG version changeComplexity
1. initdb + import (declarative dump/restore)full stop during dump→restoresmall/medium (up to tens of GB)any (can jump majors)low
2. Logical replicationnear-zero (cutover only)medium/largesame or higher majormedium
3. pg_basebackup bootstrap (physical)short (cutover)largeidentical major requiredmedium
WARNING

WAL shipping from the Zalando archive is NOT a path. Spilo archives WAL with WAL-E/WAL-G in a layout barman-cloud cannot read; a CNPG cluster cannot recover from a Zalando S3 archive. Backup history does not carry over — take a fresh CNPG base backup immediately after cutover.

All paths need network reachability from the CNPG pods to the Zalando master service (<zalando-cluster>.<ns>.svc:5432) and a sufficiently privileged user on the source. Common preparation:

ZC=acid-orders                      # zalando cluster name, namespace $NS
# source superuser password (Zalando-managed secret):
kubectl -n $NS get secret postgres.$ZC.credentials.postgresql.acid.zalan.do \
  -o jsonpath='{.data.password}' | base64 -d
# CNPG external clusters want basic-auth secrets:
kubectl -n $NS create secret generic zalando-src-superuser \
  --type=kubernetes.io/basic-auth \
  --from-literal=username=postgres --from-literal=password='<above>'

Path 1 — declarative import (operator-driven dump/restore)

CNPG runs pg_dump/pg_restore for you at bootstrap (bootstrap.initdb.import): it connects to the source, dumps the selected database(s) and roles, restores into the new cluster, and then proceeds as a normal cluster. Recommended for small/medium databases — and the only path that also upgrades the major version in the same step.

spec:
  bootstrap:
    initdb:
      database: orders
      owner: orders_svc
      import:
        type: microservice          # one DB; 'monolith' = many DBs + roles
        databases: [orders]
        source:
          externalCluster: zalando-src
  externalClusters:
    - name: zalando-src
      connectionParameters:
        host: acid-orders.<ns>.svc
        user: postgres
        dbname: postgres
      password:
        name: zalando-src-superuser
        key: password
  • type: monolith with databases: ["*"] and roles: ["*"] migrates everything in one shot.
  • Quiesce writes on the source first — the import is a point-in-time dump.
  • Run ANALYZE afterwards (statistics are not dumped).

Path 2 — logical replication (minimal downtime)

Initial copy plus continuous change streaming; cut over when lag reaches ~0. The source must have wal_level=logical (Spilo default in recent versions — verify with SHOW wal_level;).

  1. Bootstrap the CNPG cluster schema-only (Path 1 import with schemaOnly: true, or restore pg_dump --schema-only manually — logical replication does not copy DDL).

  2. Publication on the Zalando primary (database orders):

    CREATE PUBLICATION mig_pub FOR ALL TABLES;
  3. Subscription on the CNPG cluster (declarative):

    apiVersion: postgresql.cnpg.io/v1
    kind: Subscription
    metadata:
      name: mig-sub
    spec:
      name: mig_sub
      dbname: orders
      publicationName: mig_pub
      cluster:
        name: acid-orders            # the NEW cnpg cluster
      externalClusterName: zalando-src
  4. Monitor pg_stat_subscription (CNPG side) and pg_stat_replication (source side); wait for initial sync to finish and lag to stay ~0.

  5. Cut over: stop writes → wait for lag 0 → sync sequences (logical replication does not replicate them — set them from the source's pg_sequences values) → drop the Subscription → repoint applications.

  6. Take the first CNPG base backup; decommission the source.

Caveats: tables need a replica identity (primary key) for UPDATE/DELETE; DDL changes during the migration window are not replicated; large objects are not replicated.

Path 3 — physical clone (pg_basebackup bootstrap)

A byte-level copy streamed from the running Zalando primary — same PostgreSQL major on both sides, all databases, no schema work (equivalent of Zalando's live clone).

spec:
  bootstrap:
    pg_basebackup:
      source: zalando-src
  externalClusters:
    - name: zalando-src
      connectionParameters:
        host: acid-orders.<ns>.svc
        user: standby                  # zalando replication user
        dbname: postgres
      password:
        name: zalando-src-standby      # from standby.<zc>.credentials...
        key: password

Requirements: the source must allow replication connections from the CNPG pods (the Zalando standby user is suitable) with max_wal_senders headroom.

The copy is point-in-time — writes after the basebackup are not replicated; stop writes before starting. Spilo artifacts (the standby role, Patroni helper objects) remain in the catalog afterwards — harmless, remove at leisure.

TIP

For a long-running synchronized standby (replicate now, promote at cutover), add replica: {enabled: true, source: zalando-src} on top of the same pg_basebackup bootstrap — the cluster keeps streaming from the Zalando primary until you flip replica.enabled to promote.

After any path

  1. First CNPG base backup (Backup resource, barman-cloud plugin) — verify phase: completed; configure the ScheduledBackup.
  2. Monitoring: PodMonitor + dashboards (the Grafana dashboards guide, How To section) — WAL segments awaiting archive should be 0.
  3. ANALYZE on the new cluster (paths 1–2).
  4. Validate application credentials, connection limits, and the Pooler.
  5. Keep the Zalando cluster paused (not deleted) during a rollback window.