Оригинал: CNPG Recipe 24 — Migrating from Crunchy PGO to PostgreSQL 18 with CloudNativePG — 13 мая 2026.
Руководство по миграции кластера PostgreSQL 17 из Crunchy PGO v6 в PostgreSQL 18 под управлением CloudNativePG. Мы рассмотрим два пути: полностью декларативную офлайн-миграцию с использованием встроенного импорта pg_dump и онлайн-миграцию через нативную логическую репликацию PostgreSQL для минимизации времени простоя (near-zero downtime).
С точки зрения CloudNativePG, источник миграции — это просто эндпоинт PostgreSQL, доступный по сети. Неважно, управляется ли он Crunchy PGO, Zalando, Patroni, RDS или чем-то ещё. Главное — доступность базы, наличие пользователя с достаточными правами и (для онлайн-пути) поддержка логической репликации на источнике.
Этот рецепт опирается на Рецепт 5 и декларативную логическую репликацию из Рецепта 15. Кластер Crunchy PGO здесь рассматривается как «чёрный ящик»: мы разворачиваем его манифестом, фиксируем эндпоинт сервиса и секрет с учетными данными, и передаём эти данные в CloudNativePG. Знание внутренних механизмов PGO не требуется.
Офлайн-путь проще: вся миграция описывается в одном манифесте Cluster, полностью декларативно. Требуется окно обслуживания, пропорциональное объёму данных.
Онлайн-путь использует логическую репликацию для непрерывной передачи данных от источника к целевому кластеру. Это сокращает окно переключения (cutover) до нескольких секунд независимо от объёма данных, но требует дополнительных шагов по настройке и удалению объектов репликации.
Если окно pg_dump допустимо для вашей нагрузки — используйте офлайн-путь. В противном случае переходите к онлайн-миграции.
Пререквизиты
Для локального развёртывания используйте инструменты из Рецепта 1:
Настройка локального окружения
Создайте Kind-кластер и установите CloudNativePG:
kind create cluster --name cnpg-migration
kubectl config use-context kind-cnpg-migrationУстановите оператор CloudNativePG из официального манифеста:
kubectl apply --server-side -f \
https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/release-1.29/releases/cnpg-1.29.1.yamlДождитесь готовности оператора и установите каталог образов расширений:
kubectl rollout status deployment -n cnpg-system cnpg-controller-manager
kubectl apply -f \
https://raw.githubusercontent.com/cloudnative-pg/artifacts/refs/heads/main/image-catalogs-extensions/catalog-minimal-trixie.yamlКаталог предоставляет расширения (например, pgaudit) в виде OCI-образов, которые монтируются в поды через функцию Kubernetes ImageVolume (доступно по умолчанию в K8s 1.35+).
Развёртка исходного кластера PostgreSQL 17
Если вы уже используете Crunchy PGO, пропустите этот раздел. Для примера установим PGO v6:
git clone https://github.com/CrunchyData/postgres-operator.git
cd postgres-operator
git checkout v6.0.1
kubectl apply -k config/namespace
kubectl apply --server-side -k config/default
cd ..Развернём исходный PostgresCluster. Мы создаём базу app с обычным пользователем (app) и специальным пользователем для миграции (cnpg), которого будет использовать CloudNativePG. Параметр wal_level: logical необходим для онлайн-пути.
[postgres-cluster-source.yaml]
apiVersion: postgres-operator.crunchydata.com/v1beta1
kind: PostgresCluster
metadata:
name: crunchy
spec:
postgresVersion: 17
patroni:
dynamicConfiguration:
postgresql:
parameters:
wal_level: logical
instances:
- name: instance1
replicas: 1
dataVolumeClaimSpec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
backups:
pgbackrest:
repos:
- name: repo1
volume:
volumeClaimSpec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
users:
- name: app
databases:
- app
- name: cnpg
databases:
- app
options: "SUPERUSER"После развёртывания нам понадобятся два значения:
- Эндпоинт Primary:
crunchy-primary.default.svc(порт 5432) - Секрет с паролем:
crunchy-pguser-cnpg, ключpassword
Загрузка тестовых данных
Создадим таблицу orders и вставим 1000 строк для симуляции нагрузки:
[sample-data-init.yaml]
apiVersion: batch/v1
kind: Job
metadata:
name: sample-data-init
spec:
template:
spec:
restartPolicy: Never
containers:
- name: psql
image: ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie
env:
- name: PGPASSWORD
valueFrom:
secretKeyRef:
name: crunchy-pguser-cnpg
key: password
command:
- psql
- -h
- crunchy-primary.default.svc
- -U
- cnpg
- app
- -c
- |
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
description TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
INSERT INTO orders (description)
SELECT 'Order ' || g FROM generate_series(1, 1000) AS g;Проверяем данные и текущее значение последовательности:
kubectl run psql-check --rm -it --restart=Never \
--image=ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie \
--env="PGPASSWORD=$(kubectl get secret crunchy-pguser-cnpg \
-o jsonpath='{.data.password}' | base64 -d)" \
-- psql -h crunchy-primary.default.svc -U cnpg app \
-c "SELECT count(*) FROM orders;" \
-c "SELECT last_value FROM orders_id_seq;"Совместимость расширений
Crunchy PostgreSQL по умолчанию устанавливает pgaudit. Целевой кластер должен также иметь доступ к этому расширению, иначе pg_restore завершится ошибкой. В этом рецепте мы используем imageCatalogRef и объявляем pgaudit в spec.postgresql.extensions. CloudNativePG монтирует образ расширения как read-only том, делая его доступным для PostgreSQL 18 без изменения базового образа.
Перед реальной миграцией проверьте список всех расширений на источнике (SELECT extname FROM pg_extension) и убедитесь, что все они доступны в целевом образе или каталоге.
Офлайн-миграция
Полностью декларативный путь. Применяем следующий манифест Cluster:
[cluster-offline.yaml]
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg-app
spec:
instances: 1
imageCatalogRef:
apiGroup: postgresql.cnpg.io
kind: ClusterImageCatalog
name: postgresql-minimal-trixie
major: 18
storage:
size: 5Gi
postgresql:
extensions:
- name: pgaudit
parameters:
pgaudit.log: "all, -misc"
pgaudit.log_catalog: "off"
pgaudit.log_parameter: "on"
pgaudit.log_relation: "on"
bootstrap:
initdb:
import:
type: microservice
databases:
- app
source:
externalCluster: crunchy
externalClusters:
- name: crunchy
connectionParameters:
host: crunchy-primary.default.svc
port: "5432"
user: cnpg
dbname: app
password:
name: crunchy-pguser-cnpg
key: passwordCloudNativePG подключается к источнику, выполняет pg_dump базы app и восстанавливает схему и данные в новом кластере PostgreSQL 18. Импорт происходит один раз при инициализации.
Дождитесь готовности:
kubectl wait --for=condition=Ready cluster/pg-app --timeout=600sСверьте количество строк и значения последовательностей на источнике и в целевом кластере:
# Источник
kubectl run psql-count --rm -it --restart=Never \
--image=ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie \
--env="PGPASSWORD=$(kubectl get secret crunchy-pguser-cnpg \
-o jsonpath='{.data.password}' | base64 -d)" \
-- psql -h crunchy-primary.default.svc -U cnpg app \
-c "SELECT count(*) FROM orders;" \
-c "SELECT last_value FROM orders_id_seq;"
# Целевой кластер
kubectl cnpg psql pg-app -- app \
-c "SELECT count(*) FROM orders;" \
-c "SELECT last_value FROM orders_id_seq;"После сверки масштабируйте pg-app до нужного количества реплик и перенаправьте приложение на pg-app-rw.default.svc.
Онлайн-миграция
Используйте этот путь, если объём данных слишком велик для окна pg_dump. Логическая репликация работает непрерывно, сокращая окно переключения до секунд.
Развёртка целевого кластера
Манифест почти идентичен офлайн-пути, но с параметром schemaOnly: true. Это значит, что при инициализации импортируется только схема, а данные будут поступать через подписку.
[cluster-online.yaml]
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: pg-app
spec:
instances: 1
imageCatalogRef:
apiGroup: postgresql.cnpg.io
kind: ClusterImageCatalog
name: postgresql-minimal-trixie
major: 18
storage:
size: 5Gi
postgresql:
extensions:
- name: pgaudit
parameters:
pgaudit.log: "all, -misc"
pgaudit.log_catalog: "off"
pgaudit.log_parameter: "on"
pgaudit.log_relation: "on"
bootstrap:
initdb:
import:
type: microservice
schemaOnly: true
databases:
- app
source:
externalCluster: crunchy
externalClusters:
- name: crunchy
connectionParameters:
host: crunchy-primary.default.svc
port: "5432"
user: cnpg
dbname: app
password:
name: crunchy-pguser-cnpg
key: passwordНастройка логической репликации
Создайте публикацию на источнике с помощью плагина cnpg:
kubectl cnpg publication create pg-app \
--external-cluster crunchy \
--publication migration \
--all-tablesТеперь создайте декларативный ресурс Subscription в CloudNativePG:
[subscription.yaml]
apiVersion: postgresql.cnpg.io/v1
kind: Subscription
metadata:
name: pg-app-migration
spec:
cluster:
name: pg-app
dbname: app
name: migration
externalClusterName: crunchy
publicationName: migration
subscriptionReclaimPolicy: deleteCloudNativePG создаст подписку и начнёт начальную синхронизацию таблиц. Проверьте логи:
kubectl logs \
-l cnpg.io/cluster=pg-app \
--follow \
| grep -i "logical replication"Вы должны увидеть сообщение logical replication apply worker for subscription "migration" has started.
Верификация и переключение (cutover)
Перед переключением проверьте отставание репликации (replication lag):
kubectl run psql-lag --rm -it --restart=Never \
--image=ghcr.io/cloudnative-pg/postgresql:18-minimal-trixie \
--env="PGPASSWORD=$(kubectl get secret crunchy-pguser-cnpg \
-o jsonpath='{.data.password}' | base64 -d)" \
-- psql -h crunchy-primary.default.svc -U cnpg app -c "
SELECT slot_name,
confirmed_flush_lsn,
pg_current_wal_lsn(),
pg_current_wal_lsn() - confirmed_flush_lsn AS lag_bytes
FROM pg_replication_slots
WHERE slot_name = 'migration';"Когда lag_bytes стабильно близок к нулю, данные синхронизированы. Однако логическая репликация не реплицирует состояние последовательностей (sequences). Синхронизируйте их перед переключением:
kubectl cnpg subscription sync-sequences pg-app \
--subscription migrationРекомендуется выполнить этот шаг один раз перед окном обслуживания и повторно непосредственно перед перенаправлением трафика.
Когда будете готовы:
- Остановите запись на источнике.
- Дождитесь, пока
lag_bytesстанет равным 0. - Выполните финальный
sync-sequences. - Масштабируйте
pg-appи перенаправьте приложение наpg-app-rw.default.svc.
После подтверждения стабильной работы удалите объекты репликации:
# Удаляем ресурс Subscription (это удалит SQL подписку)
kubectl delete subscription pg-app-migration
# Удаляем публикацию на источнике
kubectl cnpg publication drop pg-app \
--external-cluster crunchy \
--publication migrationОчистка
Удалите исходный кластер:
kubectl delete postgrescluster crunchyСекции bootstrap.initdb.import и externalClusters в манифесте Cluster нужны только при инициализации. После миграции их можно удалить из манифеста и применить его снова — это не вызовет сбоев в работе кластера.
Сравнение образов и безопасность
Переход на CloudNativePG меняет стек образов. Ниже приведено сравнение размеров и уязвимостей.
Размер сжатых образов
| Образ | Роль | Размер |
|---|---|---|
crunchy-postgres:ubi9-17.9-2610 | Источник PGO | ~346 МБ |
postgresql:18-minimal-trixie | Цель CNPG | ~87 МБ |
Образ расширения pgaudit | Volume расширения | ~44 КБ |
plugin-barman-cloud | Плагин бэкапов | ~40 МБ |
| Итоговый стек CNPG | ~127 МБ |
Уязвимости (CVE) — docker scout quickview
| Образ | Пакеты | Critical | High | Medium | Low |
|---|---|---|---|---|---|
crunchy-postgres:ubi9-17.9-2610 | 625 | 2 | 156 | 1053 | 201 |
postgresql:18-minimal-trixie | 140 | 0 | 4 | 6 | 39 |
Образ minimal-trixie содержит только PostgreSQL 18, расширения поставляются отдельными OCI-образами. Стек CNPG (~127 МБ) значительно меньше образа Crunchy (~346 МБ). Количество CVE также резко снижено: 0 критических против 2 и всего 4 высоких против 156. Меньше пакетов — меньше поверхность атаки.
Заключение
Оба пути миграции сводятся к манифесту Cluster и данным для подключения к источнику. Офлайн-путь — самый короткий и декларативный. Онлайн-путь делает окно переключения независимым от объёма данных. Аналогичный подход применим и для Percona Operator for PostgreSQL
, так как он использует те же соглашения об именах сервисов и секретов.
Манифесты в этом рецепте минималистичны. В продакшне обязательно настройте бэкапы через Barman Cloud Plugin, установите лимиты ресурсов и разверните минимум три инстанса. Подробности смотрите в документации CloudNativePG .