playbook-pg-migration · git:20261028.645512f · 2026-10-28 · sha256 93de63c537417c5f
playbook-pg-migration git:20261028.645512fA
Immutable. This exact content never changes and is served at /api/v1/blob/93de63c537417c5f.
--- name: playbook-pg-migration description: Playbook for moving PostgreSQL between Kubernetes clusters or environments, especially with Crunchy Postgres Operator (PGO). Covers how to choose a method (PGO standby cluster from a pgBackRest repo or streaming, restore from backup, logical replication, dump/restore), pre-flight checks (versions, extensions, collation/glibc, sequences, roles), write freeze and cutover, data checks, rollback and the point of no return. Use whenever a task moves, clones, upgrades across clusters, or cuts over a PostgreSQL database. when_to_use: Use when moving / cloning / cutting over PostgreSQL to another cluster or environment (PGO, CloudNativePG, plain Postgres), even if the user only says "move the DB to the new cluster". Vietnamese triggers — "chuyển DB sang cluster mới", "di chuyển / clone / cutover PostgreSQL sang cluster hoặc môi trường khác". --- # Playbook: Move PostgreSQL between Kubernetes clusters <!-- devops-ai-kit · Last reviewed: 2026-10-28 · Always L3. See also: `db-reliability`, `disaster-recovery`, `k8s-storage`. --> The field names of the `PostgresCluster` CR and the exact behavior change between PGO versions. Ask `infra-researcher` to check the syntax for the **exact PGO and PostgreSQL versions** before you write manifests. This playbook explains how to think about the migration and what you must not miss. ## 1. Check the source (add to recon) - [ ] Versions: PostgreSQL major / minor, PGO (source and target), Kubernetes on both clusters. - [ ] DB size, biggest tables, WAL rate (to estimate the sync time). - [ ] Extensions and their versions (`\dx` on each database). Does the target have the same extensions / versions? - [ ] Roles, ownership, grants. Which user does the app use? Which Secret holds the password? - [ ] Tables **without a primary key** (important for logical replication). - [ ] Sequences (logical replication does **not** copy sequence values). - [ ] Locale / collation: provider (libc or ICU), glibc version of the source and target images. - Physical replication / restore between images with different glibc versions can **silently break the order of indexes on text columns**. - If they are different: use the same image line, or plan a `REINDEX` of text indexes after the move. - [ ] Current pgBackRest repo: type (S3 / Azure Blob / GCS / volume), retention, latest backup. Has anyone ever tested a restore? - [ ] Network between the two clusters (if you need streaming / logical), or both clusters can reach the object storage. - [ ] How the app connects: Service name, PgBouncer, connection string in a Secret, DNS / ExternalName. ## 2. Choose a method | Method | When to use | Downtime | Notes | |---|---|---|---| | **A. PGO standby cluster** (the target cluster runs in standby mode and gets WAL from a shared pgBackRest repo and/or streaming from the source primary) | Source is PGO, **same PG major**, you want short downtime | Short (only promote + switch connections) | Needs the same repo / credentials. Before promote, stop writes on the source cluster to avoid split-brain | | **B. Restore a pgBackRest backup into a new cluster** (dataSource points to the source's repo) | Downtime of restore + replay time is OK, or you want a test copy | Medium (for a cutover, writes must stay frozen during the whole restore) | Very good for a **rehearsal** before the real run | | **C. Logical replication** (publication / subscription) | Different PG major (move + upgrade together), or the source is not PGO | Short | Does not copy DDL, sequences, large objects. Tables need a PK / replica identity. You must sync sequences at cutover | | **D. pg_dump / pg_restore** | Small DB, downtime of dump + restore time is OK | Longest | Simplest. Use directory format `-Fd` + `-j` to run in parallel | Write the chosen method in the plan, and why you did not choose the others (suggest an ADR if the decision is for many DBs). ## 3. Prepare the target cluster (before cutover) - [ ] StorageClass follows the standard (`k8s-storage`). PVCs are big enough (≥ 1.5 × current data + growth). - [ ] The target cluster has its **own** pgBackRest repo, and **the first full backup succeeded before cutover**. - Never run production on the new cluster without a backup. - With method A, keep the shared repo (for WAL) separate in your mind from the backup repo used after promote. - [ ] Monitoring for the target cluster works (exporter, alerts for replication / backup / disk) **before** you switch traffic. - [ ] PgBouncer on the target side (if used), with pool size recalculated for the number of app pods. - [ ] Rehearse the full process at least once on a copy (method B is good for making the copy). Measure the real time. ## 4. Cutover (steps the plan must have, in this order) 1. Announce it and start the maintenance window. DNS TTL was lowered earlier. 2. Check again: lag close to 0 (A / C), latest target backup is OK, target monitoring is green. 3. **Write freeze on the source:** scale the writing apps to 0, or block writes in the DB. Check that no write connections are left (`pg_stat_activity`). 4. Wait until sync is complete: - A: the target has received / replayed the last WAL; - C: `pg_stat_subscription` shows no lag; - B / D: the restore is finished. 5. **Check the data** (section 5) before you allow writes again. 6. Promote the target (A: turn off standby mode in the way your PGO version does it; C: stop the subscription, `setval` sequences ≥ max(id) of each table). 7. — **Point of no return** — - After the app writes to the target, going back to the source means you lose new data or must merge it by hand. - The plan must say who decides to pass this point, and how much time they have. 8. Switch connections: update the app's Secret / connection string / Service / DNS. Scale the app up. 9. Smoke test the app (real reads + writes). Watch connection errors, latency, error rate. 10. Keep the source cluster **unchanged and with no writes** for the agreed rollback period. Remove the source later, in a separate change. ## 5. Data checks | Check | How | |---|---| | Row counts | Important tables: `count(*)` on both sides. Other tables: compare `n_live_tup` (approximate) and investigate big differences | | Checksum of important tables | Hash the data in PK order (e.g. `md5(string_agg(t::text, ',' ORDER BY id))` on small / medium tables, or by id range on big tables) | | Sequences | Current value ≥ `max(id)` of the matching table | | Extensions / roles / grants | Compare the lists on both sides | | Valid indexes | No `INVALID` indexes (`pg_index.indisvalid = false`). If glibc / collation changed, REINDEX is done | | App | The main business flows can read / write | ## 6. Rollback - **Before PONR:** allow writes on the source again and point connections back to the source. The target can be rebuilt. - **After PONR:** new data must be checked / moved back. This is hard: it needs its own plan, or you accept that there is no rollback after time X. - The plan states the deadline for the rollback decision (e.g. 30 minutes after writes are allowed again). ## 7. Docs to leave behind - A change record with the real time of each step (useful for the next migration). - A runbook for the target cluster: backup / restore (with the test restore), failover, upgrade, alerts. - A concept note on PGO / pgBackRest / Patroni if the user is new to them.