✻ Урок 9.5 · Тема 9: Секреты и GitOps
CloudNativePG: PostgreSQL-оператор
Содержание урока
Зачем это нужно
Ты уже гонял PostgreSQL (популярная база данных: программа, которая хранит таблицы с данными приложения, как электронный архив с картотекой) в StatefulSet и делал pg_dump (команду, которая выгружает всю базу в один файл-копию) по крону (урок 5.5). У такой схемы три дыры:
- Если под с primary (главным экземпляром базы, куда пишут) умрёт, никто не поднимет второй экземпляр и не назначит его главным. База лежит, пока ты не вмешаешься руками.
- Восстановление из бэкапа придётся вспоминать по памяти: какой файл, какой командой, в какую базу.
- Бэкапы лежат в том же кластере, что и база. Умер кластер, умерли и бэкапы.
Оператор (operator) закрывает эти дыры. Это как приглашённый дежурный администратор, который знает именно эту базу: ты не объясняешь ему, что делать при каждой поломке, он знает сам. Технически это программа, которая работает внутри Kubernetes и знает, как эксплуатировать конкретную систему: как настроить репликацию, как переключиться на запасной экземпляр, как снять бэкап и восстановиться. Ты описываешь желаемое в YAML («хочу две копии базы и бэкап каждую ночь»), а оператор приводит систему к этому виду и держит её такой.
Два слова, которые будут везде. Репликация (replication) это когда база постоянно копирует свои изменения на второй экземпляр, как помощник, который переписывает за бухгалтером каждую запись. Запасная копия при этом почти всегда свежая, и если главный сломался, её можно назначить главным. Бэкап (backup) это отдельная копия на случай ошибок людей и потери всего сразу, подробно разберём ниже.
Внимание, путаница в словах: у CNPG ресурс называется Cluster, и это не кластер Kubernetes, а одна база с её копиями (главная и запасные машины вместе). Слово «кластер» в уроке будет значить обе вещи: по контексту и написанию Cluster с большой буквы в коде отличай.
На работе это частый вопрос собеседований: «стоит ли держать БД в Kubernetes и как это делают взрослые».
Шаг проекта: в notes-gitops появляется Cluster notes-db (CloudNativePG), приложение ходит в notes-db-rw.notes.svc:5432 (внутрикластерный адрес «записывающего» входа в базу: как телефон дежурного, по которому всегда отвечает нынешний главный; rw значит read-write, «чтение и запись»), пароль приходит из Vault через ESO (хранилище секретов и программа-доставщик из урока 9.2), бэкапы уходят в MinIO (программа-хранилище файлов, которая умеет то же, что облачное S3: «камера хранения» для копий, подробно ниже), а StatefulSet postgres и CronJob pg-backup удалены. Версия приложения 0.7.0.
Что нужно знать
- Урок 5.5: хранилище и StatefulSet с PostgreSQL: PVC (запрос на диск), почему БД не Deployment,
pg_dumpиз пода. - Урок 5.8: Job, CronJob, DaemonSet: CronJob
pg-backup, который мы сегодня заменим. - Урок 6.4: управляемые сервисы: что даёт облачная БД и чего оператору не хватает.
- Урок 9.2: Vault и External Secrets Operator: пароль БД в Vault,
ExternalSecret. - Урок 9.3: GitOps, Flux:
HelmRelease,dependsOn, всё меняется коммитом. - Урок 9.4: cert-manager: паттерн «CRD плюс контроллер» ты уже видел.
Картина целиком
Представь дежурного инженера в серверной. Ему оставили инструкцию: «Всегда держи два работающих сервера базы: главный и запасной. Каждую ночь делай копию. Если главный сломался, сделай главным запасной и заведи новый запасной». Инженер не спит, постоянно смотрит на серверы и, как только видит расхождение с инструкцией, исправляет его. Оператор это такой дежурный инженер, только программа. Инструкция это ресурс Cluster в YAML.
Вот как это выглядит в нашем проекте:
flowchart LR
subgraph G["git notes-gitops"]
G1["cnpg-cluster.yaml<br>cnpg-secret.yaml<br>cnpg-backup.yaml"]
end
G1 -->|"Flux"| CL
V["Vault, ESO<br>Secret notes-db-app<br>(пароль)"] --> CL
subgraph K["Kubernetes, namespace notes"]
CL["ресурс Cluster notes-db"]
OP["оператор CNPG<br>namespace cnpg-system<br>работает в цикле"] -->|"читает и сравнивает"| CL
OP -->|"создаёт и чинит"| P1["под notes-db-1 (primary)"]
OP --> P2["под notes-db-2 (replica)"]
P1 -->|"WAL"| P2
SV["Service notes-db-rw"] --> P1
end
APP["приложение notes<br>notes-db-rw:5432"] --> SV
P1 -->|"базовые копии и журнал WAL"| M["MinIO, бакет cnpg-backups"]
Части схемы связаны так. Git хранит желаемое. Flux кладёт это в кластер. Оператор читает Cluster и создаёт поды с базой, диски и Service. Пароль приходит отдельной дорогой, через Vault и ESO. Приложение знает один адрес, notes-db-rw, и не знает, какой именно под сейчас главный. Копии базы и журнал изменений уезжают в MinIO, то есть за пределы подов с базой. Дальше каждая часть разбирается по очереди.
Теория
Почему базу в Kubernetes нельзя просто «запустить»
Kubernetes отлично управляет приложениями без памяти (stateless): убил под, запустил такой же, ничего не потерялось. База данных приложение с памятью (stateful): её данные лежат на диске, и потерять их нельзя. Плюс у базы есть роли: один экземпляр главный, остальные копии.
Аналогия: кассир в магазине и главный бухгалтер. Кассира можно заменить любым другим, инструкция у всех одна. Главного бухгалтера заменить нельзя: он знает состояние книг, и если он выбыл, нужно решить, кто из помощников продолжит с последней записи и не потеряет ли он что-то. Аналогия перестаёт работать в том, что у бухгалтера одна голова, а у базы можно держать копии на нескольких машинах.
В уроке 5.5 ты использовал StatefulSet: он даёт поду постоянное имя (postgres-0) и постоянный диск (PVC, PersistentVolumeClaim, «заявка на диск»). Это решает «не потерять данные при перезапуске». Но StatefulSet ничего не знает про PostgreSQL. Он не знает, что такое главный и запасной, что реплика отстаёт от главного на сколько-то записей, что перед переключением надо убедиться, что старый главный точно перестал писать. Всё это в лучшем случае делает скрипт, который кто-то написал и поддерживает.
Разберём на примере. Допустим, в StatefulSet три пода PostgreSQL. postgres-0 главный, умер ночью в 03:10. StatefulSet послушно создаст postgres-0 заново с тем же диском. Пока он стартует (полминуты), запись невозможна. Но допустим, диск повреждён и под не стартует. StatefulSet будет бесконечно пытаться, а postgres-1 и postgres-2 так и останутся репликами: никто не скажет одному из них «теперь ты главный». В 03:10 у тебя простой, и он продлится до момента, пока дежурный не проснётся и не выполнит команду повышения руками.
Осторожно: «Kubernetes сам следит за подами, значит и за базой». Нет: он следит за тем, что под жив, а не за тем, что база правильно устроена. Под может быть Running, а база внутри в режиме «только чтение».
Главное: StatefulSet возвращает под и диск, но не знает про роли, отставание реплик и безопасное переключение.
Проверь понимание: StatefulSet возвращает упавший под с тем же диском. Почему этого мало, чтобы сказать «база отказоустойчива»?
Ответ
Диск сохранится, но StatefulSet не умеет повышать реплику до главного, не следит за отставанием реплик и не переключает клиентов. Если главный под не может стартовать, простой не заканчивается сам.
Чтобы всё это делалось само, нужен оператор.
Оператор: CRD плюс контроллер
Знания «как эксплуатировать PostgreSQL» есть у людей. Оператор кладёт эти знания в программу, которая работает круглосуточно и реагирует быстрее человека.
Аналогия: термостат в квартире. Ты задаёшь «22 градуса» (желаемое), термостат сам меряет температуру (фактическое) и включает или выключает нагрев, пока они не совпадут. Ты не стоишь рядом с рукой на кране. Аналогия неточна тем, что термостат умеет одно действие, а оператор десятки: создать под, повысить реплику, снять бэкап, обновить версию.
Два кусочка.
- CRD (Custom Resource Definition, «описание собственного типа ресурса»). Kubernetes из коробки знает
Pod,Service,Deployment. CRD добавляет в его API новый тип, напримерClusterв группеpostgresql.cnpg.io. После установки CRD командаkubectl get clusterначинает работать. Ты паттерн уже видел в уроке 9.4: тамCertificateиClusterIssuer. - Контроллер (controller): программа-под, которая в бесконечном цикле согласования (reconcile loop) делает три шага: прочитать желаемое из
specобъекта, посмотреть фактическое (какие поды, диски, роли есть), устранить разницу.
flowchart LR
SP["spec: instances: 2"] --> CMP["сравнить"]
CMP --> FACT["фактически 1 под"]
FACT --> ACT["создать notes-db-2"]
ACT --> EV["событие: под умер,<br>изменился spec"]
EV --> CMP
Разберём на примере. В spec написано instances: 2. Контроллер смотрит: под notes-db-1 есть, второго нет. Создаёт диск и под notes-db-2, настраивает его как реплику. Через минуту смотрит снова: оба есть, разницы нет, ничего не делает. Ты удалил notes-db-2. Контроллер видит событие «под исчез», снова видит разницу «нужно 2, есть 1» и создаёт его заново. Ты изменил instances: 3, разница появилась снова, третий под создан. Заметь, что ты никогда не говорил «создай под», ты говорил только, что должно быть.
CloudNativePG (CNPG) не использует StatefulSet. Он сам управляет подами и дисками напрямую, поэтому может выбрать, какую реплику повысить, и не должен ждать упорядоченного старта. Каждый инстанс (instance, отдельный экземпляр базы) это свой под notes-db-1, notes-db-2 со своим PVC. Сам оператор ставится один раз на кластер (Helm-чарт cloudnative-pg, у нас через HelmRelease из 9.3) и живёт в своём namespace, а базы описываются ресурсами Cluster в тех namespace, где они нужны приложениям.
Осторожно: «Оператор это продвинутый Helm». Helm один раз превращает шаблоны в манифесты и уходит. Оператор остаётся в кластере и работает всё время.
Главное: оператор это CRD плюс контроллер в бесконечном цикле согласования, а не Helm, который отработал и ушёл.
Проверь понимание: чем оператор отличается от Helm-чарта, который просто рисует StatefulSet?
Ответ
Helm один раз рендерит манифесты и уходит. Оператор живёт в кластере постоянно: замечает смерть primary, повышает реплику, перенастраивает Service, запускает бэкапы по расписанию. Helm описывает начальное состояние, оператор поддерживает и меняет его в ходе жизни.
Первая обязанность оператора: держать копию данных.
Репликация и WAL: откуда у запасного экземпляра данные
Если данные есть только в одном месте, смерть этого места равна потере данных. Значит, нужна копия, причём свежая: копия недельной давности не спасёт вчерашние заказы.
Аналогия: главный бухгалтер ведёт журнал, а помощник в соседней комнате слушает по громкой связи, как тот диктует каждую новую запись, и переписывает её в свою копию. Если бухгалтер упал в обморок, у помощника почти всё уже есть. «Почти», потому что последнюю фразу он мог не дослушать. Это точно описывает асинхронную репликацию.
PostgreSQL перед тем как изменить таблицу, записывает описание изменения в специальный журнал: WAL (write-ahead log, «журнал упреждающей записи»: сначала запись в журнал, потом в таблицы). Это нужно, чтобы после аварии базу можно было довести до согласованного состояния, проиграв журнал. Реплика (replica, копия базы, доступная только для чтения) подключена к главному экземпляру (primary) и получает WAL потоком: это потоковая репликация (streaming replication). Полученные записи она применяет к своим данным. Так её данные почти всегда совпадают с главными, отставая на доли секунды.
Слово «асинхронная» значит: primary подтверждает клиенту «записано», не дожидаясь, пока реплика получит запись. Это быстро, но есть цена.
Разберём на примере. В 10:31:12.400 клиент вставил заметку номер 4, primary ответил «ок». Запись 4 отправилась в сторону реплики по сети. В 10:31:12.401 primary умер, а запись не успела дойти. Реплика повышена до главного, но записи 4 у неё нет: клиент получил «ок», а данных нет. Величина этой потери называется RPO (recovery point objective, допустимая потеря данных, измеряется в секундах или записях). У асинхронной репликации RPO не строго ноль. Как понятие RPO и RTO (время восстановления) разбираются в уроке 10.3, здесь только механика.
Прикинь сам: primary умер, повышена реплика, у которой записи 4 нет. Что увидит клиент, который уже получил «ок» на запись 4, если прочитает заметки?
Заметки 4 в списке не будет, хотя «ок» он уже получил. Это и есть ненулевой RPO у асинхронной репликации.
Что делает оператор, когда primary пропал:
- Через пробы и своё соединение замечает, что primary не отвечает.
- Смотрит, какая из реплик получила больше всего WAL (наименьшее отставание).
- Повышает её до primary (promote, «повысить»).
- Переставляет Service
notes-db-rwна новый под. - Старый под, когда вернётся, перенастраивает в реплику нового главного.
Весь путь занимает обычно от нескольких секунд до полуминуты. Параметр failoverDelay по умолчанию равен 0, но время уходит на обнаружение сбоя и на промоут.
Отдельный термин: switchover это плановое переключение, когда ты сам просишь «переставь главного» (например, перед обновлением узла). Тогда оператор сначала дожидается, пока реплика получит всё, и потери нет. Failover это аварийное переключение, когда главный умер сам.
Осторожно: «Реплика заменяет бэкап». Нет: если кто-то по ошибке выполнил DROP TABLE, это удаление тоже честно доедет до реплики за доли секунды. Реплика защищает от смерти машины, бэкап от ошибок людей.
Главное: реплика получает WAL потоком и защищает от смерти машины, но не от ошибки человека, и RPO у асинхронной репликации не ноль.
Проверь понимание: почему после failover может пропасть заметка, на которую приложение уже получило ответ «создана»?
Ответ
Репликация асинхронная: primary подтвердил запись, не дожидаясь реплики. Если он умер до отправки записи, повышенная реплика её не содержит. Это ненулевой RPO.
Как приложение находит главный под?
Сервисы rw, ro и r: один адрес вместо имён подов
Приложению нужен постоянный адрес базы, а главный под может переехать с notes-db-1 на notes-db-2. Если прописать имя пода, после failover приложение будет стучаться в реплику.
Аналогия: номер горячей линии. Ты звонишь по одному номеру, а диспетчер соединяет с тем, кто сегодня дежурит. Смена дежурного тебя не касается.
Service (стабильный адрес и балансировщик внутри кластера, урок 5.3) выбирает поды по меткам (селектор). CNPG вешает на каждый под метку с его ролью и создаёт три Service:
| Service | Куда ведёт | Для чего |
|---|---|---|
notes-db-rw |
только primary | запись и чтение, его использует приложение |
notes-db-ro |
только реплики | тяжёлые запросы «только чтение», отчёты |
notes-db-r |
любой инстанс | чтение, когда всё равно откуда |
rw расшифровывается read-write, ro read-only, r read.
flowchart LR
APP["приложение"] --> SVC["notes-db-rw<br>ClusterIP 10.96.201.33<br>селектор: роль = primary"]
SVC -->|"до failover"| A["notes-db-1 [primary]<br>notes-db-2 [replica]"]
SVC -.->|"после failover"| B["notes-db-2 [primary]<br>notes-db-1 [replica]"]
Разберём на примере. До сбоя за notes-db-rw стоит один endpoint (адрес пода, куда реально идёт трафик), скажем 10.244.1.12:5432 (это notes-db-1). После failover оператор переставляет метку роли, и endpoint становится 10.244.2.9:5432 (notes-db-2). ClusterIP 10.96.201.33 не изменился, приложение шлёт запросы туда же. Изменилось только то, за кем стоит этот адрес.
Что видит приложение: текущие открытые соединения рвутся (старый сервер исчез), поэтому оно должно переподключиться. Хороший клиент делает это сам, с повторами.
Осторожно: что достаточно «любого» сервиса. Запись в notes-db-ro завершится ошибкой cannot execute INSERT in a read-only transaction: реплика не принимает запись.
Главное: приложение ходит в
notes-db-rw, а оператор сам переставляет, кто за ним стоит.
Проверь понимание: почему приложение должно ходить в
notes-db-rw, а не вnotes-db-1?
Ответ
Имя пода привязано к конкретному инстансу, а primary может переехать на notes-db-2. Service notes-db-rw всегда указывает на текущий primary, оператор переставляет селектор сам.
Теперь о защите от ошибок людей.
Бэкапы: базовая копия плюс WAL в объектное хранилище
Реплика не спасает от ошибки человека («удалил таблицу», «выкатил миграцию, которая испортила данные»). Нужны копии, из которых можно вернуться в прошлое, причём хранящиеся отдельно от самой базы.
Аналогия: фотография документа плюс видеозапись всего, что с ним делали потом. Фото (базовая копия) показывает состояние на вчера 03:00. Видео (WAL) позволяет «промотать» до любого момента после этого, например до 14:02:59.
Есть два вида бэкапа PostgreSQL.
- Логический (
pg_dump): выгрузка содержимого таблиц в SQL-файл. Просто и переносимо между версиями, но это снимок на одну секунду времени, а на больших базах медленно. Твой CronJobpg-backupиз урока 5.8 делал именно так. - Физический: копия самих файлов базы (base backup, базовая копия) плюс непрерывная архивация WAL. CNPG использует инструмент Barman. Он периодически делает базовую копию, а каждый готовый сегмент WAL сразу отправляет в S3-совместимое объектное хранилище (S3 это стандарт API для хранения файлов-«объектов» в бакетах, «корзинах»; в облаке это, например, Yandex Object Storage, у нас MinIO).
Из базовой копии плюс WAL можно восстановить состояние на любой момент между копией и последним архивным сегментом. Это PITR (point-in-time recovery, восстановление на момент времени).
Ресурсы, которыми это описывается:
- блок
backupвCluster: куда складывать (destinationPath,endpointURL, ключи) и как долго хранить (retentionPolicy); ScheduledBackup: расписание базовых копий, как cron (планировщик задач по расписанию, урок 1.7);Backup: разовая копия по запросу.
WAL при этом архивируется непрерывно, вне расписания. Поэтому RPO бэкапа измеряется секундами и минутами, а не «раз в сутки», как у ночного CronJob.
Разберём на примере. Сегодня 14:03 кто-то выполнил DROP TABLE notes. Последняя базовая копия сделана в 03:00. Восстановление в новый кластер на 14:02:59: оператор берёт копию 03:00 и проигрывает WAL с 03:00 до 14:02:59, удаление в WAL идёт следующим, и до него проигрывание не доходит. Таблица на месте. Ночной pg_dump в той же ситуации вернул бы состояние 03:00 и потерял бы 11 часов работы.
Расписание "0 0 3 * * *" в ScheduledBackup читается слева направо: секунда 0, минута 0, час 3, любой день месяца, любой месяц, любой день недели, то есть каждый день в 03:00:00. Обрати внимание: у CNPG шесть полей, первое секунды, в отличие от классического cron с пятью.
Прикинь сам: что значит
"0 30 2 * * 1"вScheduledBackup?
Секунды 0, минуты 30, час 2, любой день месяца, любой месяц, понедельник: каждый понедельник в 02:30:00.
Осторожно: Хранить бэкапы там же, где живёт база. Бэкап на диске рядом с базой умирает вместе с машиной. В учебном kind MinIO стоит в том же кластере, поэтому это защита от логических ошибок, но не от потери машины. Это осознанный долг. В продакшене бакет находится в другом ЦОДе или облаке. Второе: бэкап, который никогда не восстанавливали, это гипотеза, а не бэкап.
Проверь актуальность: в новых релизах CNPG встроенный barmanObjectStore объявлен устаревшим в пользу плагина Barman Cloud, актуальную схему смотри на странице проекта cloudnative-pg.io. Для урока используем встроенный вариант, он короче: в версии курса (CNPG 1.30.1) он ещё принимается, но на новых проектах начинай с плагина.
Главное: базовая копия плюс непрерывный WAL дают восстановление на любой момент, а бэкап рядом с базой умирает вместе с ней.
Проверь понимание: что лучше для восстановления после случайного
DROP TABLEв 14:03: ночнойpg_dumpили базовая копия плюс WAL?
Ответ
Базовая копия плюс WAL: восстанавливаешь новый кластер на 14:02:59 (PITR). С ночным дампом потеряешь всё, что произошло после ночи.
Куда именно уезжают копии?
Объектное хранилище S3: куда уезжают копии
Бэкапы надо хранить отдельно от базы и дёшево, а файлов накапливается много: копия каждую ночь и непрерывный поток WAL. Обычный диск под подом, который исчезнет вместе с подом, не подходит. Нужно место, которое живёт само по себе и принимает файлы по сети.
Аналогия: автоматическая камера хранения на вокзале. Ты приносишь вещь (файл), получаешь ячейку с номером (адрес), потом по номеру забираешь обратно. Камера ничего не знает про содержимое и не умеет его менять, она только хранит. Аналогия перестаёт работать на том, что в камере нельзя «дописать строчку в середину» вещи: объект кладут и заменяют целиком.
Объектное хранилище (object storage) работает так: есть бакеты (bucket, «корзины»: верхнеуровневые контейнеры вроде cnpg-backups), а внутри бакета лежат объекты (файлы с именем-путём). S3 (Simple Storage Service) это протокол, придуманный Amazon: по нему программы общаются с хранилищем через HTTP (создать бакет, положить объект, скачать объект). Сегодня S3-совместимыми называют любые хранилища, которые понимают тот же протокол: Yandex Object Storage, AWS S3, MinIO. Поэтому CNPG, научившись разговаривать с S3, работает с любым из них, и меняется только адрес (endpointURL) и ключи доступа (ACCESS_KEY_ID как логин, ACCESS_SECRET_KEY как пароль).
flowchart LR
B["CNPG (Barman)<br>базовая копия<br>файл WAL"] -->|"HTTP PUT, ключи доступа"| M["S3-хранилище (MinIO)<br>бакет cnpg-backups/<br>notes-db/base/2026...<br>notes-db/wals/0000000100..."]
Разберём на примере. Адрес записи в манифесте: destinationPath: s3://cnpg-backups/ и endpointURL: http://minio.minio.svc:9000. Читаем: «бакет называется cnpg-backups, искать его нужно не у Amazon, а у нашего MinIO по адресу minio.minio.svc:9000». Если убрать endpointURL, программа пойдёт в настоящий AWS, не найдёт такого бакета и получит ошибку доступа. Поэтому в лаборатории адрес MinIO указывается явно.
Осторожно: «бакет это папка на диске, её можно смонтировать как каталог». Нет: это не файловая система, доступ идёт только по HTTP и по ключам. И «MinIO это база данных»: MinIO лишь хранит файлы.
Главное: S3 это протокол по HTTP, поэтому CNPG работает с любым совместимым хранилищем, а
endpointURLвыбирает сервер.
Проверь понимание: зачем CNPG нужен
endpointURL, если протокол S3 один и тот же?
Ответ
Протокол один, а серверов много. Без endpointURL клиент по умолчанию идёт в облачный AWS S3. Адрес указывает, к какому именно S3-совместимому серверу обращаться, в нашем случае к MinIO внутри кластера.
Теперь вернёмся к приложению.
Как приложение подключается к базе: из чего состоит адрес
Когда приложение «не может подключиться к базе», причина почти всегда в одной из пяти частей адреса. Если знаешь части, ищешь точно.
Аналогия: почтовый адрес: город, улица, дом, квартира, имя получателя. Ошибка в любом поле, и письмо не дойдёт, но каждая ошибка выглядит по-своему.
Данные для подключения к PostgreSQL складываются в строку подключения (connection string, DSN): postgresql://пользователь:пароль@хост:порт/база. Части:
- хост: где база. У нас
notes-db-rw.notes.svc(имя Service, namespace иsvc, схема имён из урока 5.3); - порт:
5432, стандартный порт PostgreSQL; - база:
notes, название базы внутри сервера (на одном сервере PostgreSQL может быть много баз); - пользователь и пароль: кто подключается. CNPG создаёт их при запуске и кладёт в Secret (
notes-db-app).
flowchart LR
D["postgresql://notes:пароль@notes-db-rw.notes.svc:5432/notes"] --> U["пользователь: notes"]
D --> PW["пароль: из Secret"]
D --> H["хост: notes-db-rw.notes.svc"]
D --> PO["порт: 5432"]
D --> DB["база: notes"]
Разберём на примере. Приложение пишет could not translate host name (не удалось найти хост): ошибка в части «хост», например опечатка в имени Service или другой namespace. Connection refused (отказ в соединении): хост найден, но на порту никто не слушает (под не готов, нет главного). password authentication failed: хост и порт верны, не подходит пароль (Secret устарел или пароль в Vault сменили). database "notes" does not exist: сервер жив, а такой базы нет. Каждая фраза указывает на свою часть адреса, и поиск сразу сужается.
Осторожно: «пароль в строке подключения надо зашить в код». Нет: он приходит из Secret переменной окружения (урок 5.6), в git его нет.
Главное: ошибка подключения почти всегда в одной из пяти частей адреса, и каждая даёт свой текст ошибки.
Проверь понимание: приложение пишет
password authentication failed. Чей пароль проверять в первую очередь и какую часть адреса можно не трогать?
Ответ
Проверь пароль в Secret, который читает приложение, и совпадает ли он с тем, что знает база (и с тем, что в Vault). Хост и порт можно не трогать: соединение дошло до сервера, иначе ошибка была бы про хост или отказ в соединении.
Как проверить состояние самой базы?
Как читать состояние кластера: kubectl cnpg status
Когда что-то идёт не так, ты открываешь одну команду и за минуту понимаешь, где проблема: нет главного, отстаёт реплика, не идёт архивация WAL. Без умения читать её остаётся гадать.
Аналогия: приборная панель автомобиля: скорость, топливо, температура, лампочки. Смотришь не на весь двигатель, а на несколько показаний.
kubectl cnpg status notes-db -n notes (плагин CNPG) собирает данные с разных мест и печатает блоками. Схематично (значения у тебя будут другие):
Cluster Summary
Name: notes/notes-db кто это
Primary instance: notes-db-1 кто сейчас главный
Status: Cluster in healthy state общий итог
Instances: 2 Ready instances: 2 сколько должно быть / сколько готово
Continuous Backup status
First Point of Recoverability: ... самый ранний момент, на который можно откатить
Working WAL archiving: OK WAL уезжает в S3
Instances status
notes-db-1 primary ... роль каждого пода
notes-db-2 replica ... lag отставание реплики
Разберём на примере. Смотри сверху вниз. Primary instance подтверждает, куда смотрит -rw. Instances 2 / Ready 2 значит, что запасной есть. Working WAL archiving: OK значит, что бэкапы действительно уезжают. Если вместо OK стоит Failing или Not working, а диск primary растёт, ищи проблему с адресом S3 или ключами: WAL копится, потому что отправить его некуда. First Point of Recoverability пустой значит, что ни одной базовой копии нет и восстанавливаться не из чего.
Осторожно: «Ready instances равно числу инстансов, значит бэкапы в порядке». Это разные блоки: живость подов и работоспособность архивации проверяются отдельно.
Главное: живость подов и работа архивации WAL проверяются в разных блоках, смотри оба.
Проверь понимание: в блоке Cluster Summary всё зелёное, а
Working WAL archivingнеOK. Что под угрозой?
Ответ
Восстановление и место на диске. Новые WAL не попадают в хранилище, значит откат на недавний момент невозможен, а локальные WAL копятся на диске primary и со временем могут его заполнить.
Откуда в кластере берутся пароль и стартовые данные?
Секреты и bootstrap: как база получает пароль и стартовые данные
Новая база должна на старте получить имя базы, владельца и пароль. Пароль нельзя класть в git (урок 9.1).
bootstrap в Cluster описывает, как появляется первый инстанс. Вариант initdb создаёт пустую базу (database: notes) и роль-владельца (owner: notes). Пароль владельца берётся из Secret типа kubernetes.io/basic-auth с ключами username и password. Мы порождаем этот Secret через ExternalSecret из Vault (урок 9.2), поэтому в git пароля нет. Если Secret не задать, CNPG сам сгенерирует пароль и положит в Secret notes-db-app: это допустимо, но тогда источник правды не Vault.
Вариант recovery создаёт кластер не пустым, а из бэкапа: так делается восстановление. Есть и initdb.import для миграции данных из другого сервера, но мы сделаем ручной pg_dump и psql: так виден каждый шаг и это то, что просят объяснять на собеседовании.
Разберём на примере. Порядок при первом создании: ESO читает пароль из Vault и создаёт Secret notes-db-app. Оператор видит Cluster, запускает короткоживущий Job notes-db-1-initdb: он создаёт файлы базы, роль notes с паролем из Secret и базу notes. Потом стартует под notes-db-1, и после него, копируя данные с primary, notes-db-2.
Осторожно: «Сменил пароль в Vault, значит сменился пароль в базе». Нет: ESO обновит Secret, но пароль роли внутри PostgreSQL остаётся старым, пока его не поменяют командой ALTER ROLE или средствами оператора.
Главное:
initdbсоздаёт пустую базу,recoveryподнимает из бэкапа, а смена пароля в Vault не меняет роль в PostgreSQL.
Проверь понимание: что будет, если
Clusterприменился раньше, чем ESO создалnotes-db-app?
Ответ
Инициализация не сможет прочитать Secret и будет ждать: под не стартует, а в событиях появится ошибка про отсутствующий Secret. Когда ESO создаст Secret, оператор при следующем цикле согласования продолжит. Чтобы этого не было, порядок задают через dependsOn.
Что из этого создаёт оператор?
Что оператор создаёт внутри: под, диск, Secret, Service
Зачем это знать. Когда что-то сломается, ты должен понимать, какие объекты появились из одного Cluster, и искать причину в нужном.
Аналогия: заказ «две комнаты в отеле». Администратор оформляет не только сами комнаты, но и ключи, счёт и место на парковке. Ты заказал одно, получил комплект.
Из одного Cluster notes-db с instances: 2 оператор создаёт:
flowchart TD
C["Cluster notes-db, instances: 2"] --> P1["Pod notes-db-1"] --> D1["PVC notes-db-1<br>диск 2Gi, данные PostgreSQL"]
C --> P2["Pod notes-db-2"] --> D2["PVC notes-db-2"]
C --> SV["Service notes-db-rw, -ro, -r"]
C --> SE["Secret notes-db-app<br>пароль владельца, через ESO"]
C --> CE["Secret notes-db-ca, notes-db-server<br>сертификаты для шифрования соединений"]
C --> PDB["PodDisruptionBudget<br>не выключать оба инстанса разом"]
Внутри каждого пода работает не «голый» PostgreSQL, а программа оператора instance manager (менеджер экземпляра), которая запускает PostgreSQL как дочерний процесс. Она отвечает на пробы Kubernetes (проверки «жив ли ты», урок 5.4), сообщает оператору о состоянии базы, отправляет WAL в хранилище и по команде оператора повышает или понижает роль. Поэтому оператору не нужно заходить в под по ssh и запускать команды, он говорит с менеджером по внутреннему API.
Разберём на примере. Ты удалил PVC notes-db-2 вместе с подом. Оператор видит, что инстанса нет, создаёт новый диск, разворачивает на него копию с primary (это называется клонированием реплики) и запускает под. Данные на новом диске получены от primary по сети, а не восстановлены из бэкапа. Если бы ты удалил PVC primary, реплика стала бы главным, а на месте потерянного создался бы новый инстанс-реплика.
Осторожно: что данные лежат в поде. Данные лежат на PVC, под лишь запускает PostgreSQL с этим диском. Удаление пода безопасно, удаление PVC уничтожает данные этого инстанса.
Главное: данные лежат на PVC, а не в поде: удаление пода безопасно, удаление PVC уничтожает данные инстанса.
Проверь понимание: ты удалил под
notes-db-2, но не PVC. Данные пропали?
Ответ
Нет. Оператор создаст под заново и подключит к нему тот же PVC. Реплика лишь догонит WAL за время, пока пода не было.
Как база живёт дальше, годами?
Обновления, размер диска и пулы соединений
База живёт годами: выходят новые минорные версии PostgreSQL с исправлениями, данные растут, диски кончаются. Делать это руками на боевой базе страшно.
Вот как это устроено.
- Минорное обновление (
18.5на18.6): меняешьimageNameвCluster. Оператор делает rolling update (поочерёдную замену): сначала реплики по одной, потом primary. По умолчанию (primaryUpdateMethod: restart) primary перезапускается на месте, а сprimaryUpdateMethod: switchoverоператор сначала переключает роль на уже обновлённую реплику и обновляет бывший primary. Клиенты видят короткий разрыв соединений в любом случае. - Мажорное обновление (
17на18): формат файлов на диске меняется, простой заменой образа не обойтись. Нужен отдельный план: импорт в новый кластер (bootstrap.initdb.import) или поддерживаемый оператором in-place механизм. Всегда репетируй на копии из бэкапа. - Расширение диска: увеличиваешь
storage.size. Если класс хранилища разрешает расширение томов (allowVolumeExpansion), оператор расширит PVC без остановки базы. Уменьшить диск нельзя. - Пулы соединений. Каждое соединение с PostgreSQL это отдельный процесс на сервере, а у приложения с десятками подов соединений сотни. Для этого есть ресурс
Pooler: прокси PgBouncer, который держит небольшое число реальных соединений и раздаёт их клиентам. В нашем уроке его нет, но это стандартный следующий шаг.
Разберём на примере с primaryUpdateMethod: switchover. У Cluster два инстанса. Меняем 18.5 на 18.6. Шаг 1: остановлена и перезапущена notes-db-2 (реплика), primary работает. Шаг 2: оператор ждёт, пока реплика догонит WAL, и делает switchover: notes-db-2 стала primary. Шаг 3: перезапущена notes-db-1, теперь она реплика. Итог: на каждом этапе есть работающий primary, разрыв только на момент switchover.
Осторожно: «Поменяю тег и посмотрю» на боевой базе. Без свежего бэкапа и без репетиции на копии любое обновление это ставка.
Главное: минорное обновление идёт с реплик, а primary обновляется последним (перезапуском или через switchover), диск можно расширять, но не уменьшать.
Проверь понимание: почему при минорном обновлении оператор начинает с реплик, а не с primary?
Ответ
Пока обновляются реплики, primary продолжает обслуживать записи, и приложение не страдает. Primary обновляется последним, после плановой передачи роли уже обновлённой реплике.
Всё это бесполезно, если бэкап не проверен.
Как проверить, что бэкап настоящий
Есть старая шутка: «бэкапа нет, пока ты не восстановился из него». Причины сломанных бэкапов скучные: истёк ключ доступа, переполнился бакет, изменился endpoint, а тревоги нет. Узнать об этом в день аварии слишком поздно.
Три уровня проверки.
- Статус.
kubectl get backupпоказываетcompletedилиfailed.kubectl cnpg status notes-dbпоказывает, что архивация WAL работает (нетNot working) и когда была последняя успешная копия. - Алерты. Метрики CNPG сообщают возраст последнего бэкапа и последней архивации WAL. Алерт «бэкап старше 26 часов» ловит тихую поломку расписания.
- Учебное восстановление (restore drill): раз в квартал поднять отдельный кластер из бэкапа, сверить количество строк и записать, сколько это заняло. Это ты делаешь в задании 4.
Правило хранения «3-2-1»: минимум 3 копии данных, на 2 разных носителях, 1 из них в другом месте. У нас primary и реплика (две копии на одном кластере) плюс MinIO: копия в другом месте пока условная, и это записано как долг.
Разберём на примере. MinIO переехал на новый адрес, а в endpointURL остался старый. С этого момента WAL копится на диске primary, потому что отправить его некуда (PostgreSQL не удаляет сегменты, пока они не заархивированы). Через сутки диск на 2Gi заполнен, primary останавливается. Без алерта на возраст архивации ты узнаешь об этом по отвалившемуся приложению. Поэтому в интервью частый вопрос «диск primary заполняется WAL»: причина обычно в сломанной архивации.
Осторожно: Статус completed для базовой копии не гарантирует, что работает и архивация WAL: это два независимых процесса, проверяй оба.
Главное: бэкап настоящий, когда проверены статус, алерты на возраст и учебное восстановление, и это два независимых процесса: копия и WAL.
Проверь понимание: почему сломанная архивация WAL может привести к остановке базы?
Ответ
PostgreSQL хранит на диске WAL, пока он не отправлен в архив. Если архив недоступен, сегменты копятся, диск заполняется, и база перестаёт принимать запись.
Что в этот момент видит приложение?
Поведение приложения при failover
Failover это событие, в котором участвует не только база, но и клиент. Половина «ложных» инцидентов после переключения это клиент, который не умеет переподключаться.
Аналогия: ты разговаривал по телефону с дежурным, тот сменился, трубку положили. Тебе надо перезвонить на горячую линию, а не сидеть с молчащей трубкой.
При failover открытые соединения обрываются. Клиент, который держал их в пуле (наборе заранее открытых соединений), обнаружит это при следующем запросе и получит ошибку. Правильный клиент выкидывает «мёртвое» соединение, открывает новое к notes-db-rw (адрес тот же) и повторяет запрос с паузой. Неправильный держит мёртвые соединения и отвечает 500 до перезапуска.
Отдельная ловушка: проба готовности /readyz в приложении (урок 5.4) проверяет базу. Пока идёт failover, проба падает, Kubernetes убирает поды приложения из Service, и снаружи вместо коротких ошибок получается 502 или 503 на все запросы. Это правильное поведение, но его нужно ожидать.
Разберём на примере. Время t0: primary умер. t0+2 секунды: запросы в notes падают с connection reset. t0+10: реплика повышена, endpoints notes-db-rw обновлены. t0+11: новые соединения работают. Приложение с ретраями теряет запросы за 10 секунд и восстанавливается само. Приложение без ретраев с пулом остаётся в ошибках до рестарта, и его RTO определяет не база, а время, за которое кто-то заметит и перезапустит под.
Прикинь сам: primary умер в 10:00:00, реплика повышена к 10:00:10. Когда восстановится приложение без ретраев с пулом соединений?
Само оно не восстановится: мёртвые соединения остаются в пуле до рестарта пода. Время восстановления зависит от того, когда кто-то заметит.
Осторожно: «Failover прошёл, значит приложение восстановилось». Проверять нужно и базу, и клиента.
Главное: после failover восстанавливается не только база, но и клиент, поэтому приложение должно уметь переподключаться.
Проверь понимание: что происходит с пулом соединений приложения без логики переподключения после failover?
Ответ
В пуле остаются соединения к умершему primary. Каждый запрос на них падает с ошибкой, пока приложение не пересоздаст соединения или его не перезапустят.
Остался вопрос, нужна ли такая схема вообще.
Миграция и вопрос «стоит ли БД в Kubernetes»
Как мигрировать. Порядок всегда один: снять дамп из старой базы, залить в новую, проверить количество строк, переключить приложение, и только потом удалять старое. Если удалить старое раньше, ошибка в любом из шагов станет необратимой. Флаг --no-owner у pg_dump говорит «не записывай в дамп, кто владелец объектов»: в новой базе владельцем будет та роль, от которой ты заливаешь.
Стоит ли вообще. Плюсы: единый подход с остальным, GitOps, автоматизация, нет отдельной платформы. Минусы: ты сам отвечаешь за хранилище, обновления и бэкапы, а качество зависит от дисков и сети кластера. Управляемая облачная БД (урок 6.4) снимает эту ответственность за деньги. Если в команде нет людей на эксплуатацию БД, надёжнее управляемый сервис. С оператором и проверенными бэкапами вариант рабочий.
Главное: мигрируют в порядке дамп, заливка, проверка, переключение, удаление, а решение «база в Kubernetes» зависит от людей на эксплуатацию.
Проверь понимание: почему порядок «дамп, заливка, проверка, переключение, удаление» нельзя менять?
Ответ
Пока старая база цела, любую ошибку можно исправить и повторить. Удаление старого до проверки убирает единственный источник правды: если дамп оказался неполным, данные потеряны.
Дальше в уроке 10.3 те же понятия RPO и RTO разобраны на уровне всей системы.
Практика
Предполагается кластер kind-notes из урока 9.3 с Flux, Vault, ESO и cert-manager. Репозиторий notes-gitops склонирован в ~/notes-gitops.
Все задания, кроме первого шага с MinIO, выполняются в kubectl и в git-репозитории notes-gitops. Если команда kubectl cnpg не находится, это плагин CNPG для kubectl: поставь его по инструкции на cloudnative-pg.io (в Homebrew пакет называется kubectl-cnpg), он нужен для kubectl cnpg status.
Если у тебя 8 ГБ
Два инстанса PostgreSQL плюс MinIO лишние. Поставь instances: 1 в cnpg-cluster.yaml. Failover в задании 3 не получится, вместо него убей под и посмотри, как оператор его пересоздаёт и подключает тот же PVC. Остальные задания не меняются.
Задание 1. Оператор через Flux и MinIO для бэкапов
Цель: поставить CNPG v1.30.1 как HelmRelease и поднять MinIO в namespace minio под бэкапы.
Предскажи: сколько новых CRD появится после установки оператора: 0, 1 или больше? И в каком namespace они видны?
Ответ
Больше одного: clusters, backups, scheduledbackups, poolers и другие. CRD кластерные, namespace у типа нет, у объектов есть.
Шаги:
- Добавь оператор в инфраструктуру. Файл описывает три объекта: namespace
cnpg-system(отдельный, чтобы оператор жил отдельно от приложений),HelmRepository(откуда Flux берёт чарты, раз в часinterval: 1hпроверяет обновления индекса) иHelmRelease(что и с какими настройками поставить, урок 9.3):
cd ~/notes-gitops
mkdir -p infrastructure/controllers/cnpg
cat > infrastructure/controllers/cnpg/release.yaml <<'YAML'
apiVersion: v1
kind: Namespace
metadata:
name: cnpg-system
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: cnpg
namespace: cnpg-system
spec:
interval: 1h
url: https://cloudnative-pg.github.io/charts
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: cnpg
namespace: cnpg-system
spec:
interval: 10m
chart:
spec:
chart: cloudnative-pg
# чарт 0.29.1 ставит оператор v1.30.1
version: "0.29.1"
sourceRef:
kind: HelmRepository
name: cnpg
install:
crds: CreateReplace
upgrade:
crds: CreateReplace
YAML
cat > infrastructure/controllers/cnpg/kustomization.yaml <<'YAML'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- release.yaml
YAML
Разбор ключевого: chart.spec.version: "0.29.1" закрепляет версию чарта (она ставит оператор v1.30.1; соответствие чарта и оператора сверь на странице проекта при обновлении). install.crds: CreateReplace и upgrade.crds: CreateReplace говорят Helm создавать CRD и обновлять их при апгрейде (по умолчанию Helm CRD при обновлении не трогает, и новый оператор упирается в старую схему ресурсов). Оператор при этом ставится в cnpg-system, а базы будут в notes.
- Подключи каталог в
infrastructure/controllers/kustomization.yaml(добавь строку- cnpgвresources). - Поднимай MinIO для бэкапов. MinIO это S3-совместимое хранилище, которое можно запустить одним подом.
$(openssl rand -base64 24)подставляет в команду 24 случайных байта в base64, это пароль;--from-literalкладёт значение в Secret без файла. Версия образа MinIO курсом не закреплена: возьми свежий тег релиза со страницы quay.io/minio/minio, неlatest.
kubectl create namespace minio
kubectl -n minio create secret generic minio-creds \
--from-literal=rootUser=minio \
--from-literal=rootPassword="$(openssl rand -base64 24)"
- Манифест MinIO и бакет
cnpg-backupsсоздай по своему шаблону Deployment из урока 5.2 (порт 9000, аргументыserver /data, PVC 2Gi, переменныеMINIO_ROOT_USERиMINIO_ROOT_PASSWORDиз Secretminio-creds) и Serviceminio, затем создай бакет клиентомmc(командная утилита MinIO):mc alias set local http://minio.minio.svc:9000 minio <пароль>иmc mb local/cnpg-backups(например, из временного пода с образомquay.io/minio/mc, тег возьми там же, где тег MinIO). Бакет (bucket) это верхнеуровневая «папка» в S3. Секрет для CNPG в namespacenotesнужен отдельный, потому что Secret из одного namespace нельзя использовать в другом. Ключи в нём названы так, как их ждёт CNPG (ACCESS_KEY_ID,ACCESS_SECRET_KEY):
kubectl -n notes create secret generic minio-creds \
--from-literal=ACCESS_KEY_ID=minio \
--from-literal=ACCESS_SECRET_KEY="$(kubectl -n minio get secret minio-creds -o jsonpath='{.data.rootPassword}' | base64 -d)"
- Закоммить и запушь, дождись Flux.
flux reconcile ... --with-sourceпросит Flux не ждать интервала, а сразу забрать свежий коммит и применить;rollout statusждёт, пока Deployment оператора полностью готов;grep cnpg.ioоставляет из списка CRD только наши:
git add -A && git commit -m "cnpg: оператор" && git push
flux reconcile kustomization infrastructure --with-source
kubectl -n cnpg-system rollout status deploy/cnpg-cloudnative-pg
kubectl get crd | grep cnpg.io
Что должно получиться:
deployment "cnpg-cloudnative-pg" successfully rolled out
backups.postgresql.cnpg.io 2026-09-29T10:12:04Z
clusters.postgresql.cnpg.io 2026-09-29T10:12:04Z
poolers.postgresql.cnpg.io 2026-09-29T10:12:04Z
scheduledbackups.postgresql.cnpg.io 2026-09-29T10:12:04Z
Как читать вывод: первая строка говорит, что под оператора запущен и готов. Дальше список CRD: слева имя типа (clusters.postgresql.cnpg.io это Cluster в группе postgresql.cnpg.io), справа время создания. Даты у тебя будут свои. Полный набор CRD может быть больше четырёх (в нём есть и другие типы, например для публикаций и подписок), важно, что четыре главных на месте. CRD видны без указания namespace: это кластерные объекты.
Объясни себе:
- Почему
install.crds: CreateReplace, а не оставить по умолчанию? - Почему оператор в отдельном namespace, а базы в
notes?
Типичные ошибки:
no matches for kind "Cluster" in version "postgresql.cnpg.io/v1": CRD ещё не установлены, приложение применилось раньше оператора: проверьdependsOnиз 9.3, оператор вcontrollers, кластер вapps.Error: chart "cloudnative-pg" version "0.29.1" not found: такой версии нет:helm search repo cnpg/cloudnative-pg --versionsи укажи реальную.pod has unbound immediate PersistentVolumeClaims: у kind нет свободного PV на MinIO: проверьkubectl get sc, должен бытьstandard.
Задание 2. Cluster с паролем из Vault
Цель: описать Cluster notes-db, пароль взять из Vault через ESO и увидеть три Service.
Предскажи: сколько подов появится при instances: 2 и чем они отличаются в kubectl get pods -L cnpg.io/instanceRole?
Ответ
Два: notes-db-1 с ролью primary и notes-db-2 с ролью replica. Также сначала появится короткоживущий Job notes-db-1-initdb.
Шаги:
- Пароль уже лежит в Vault по пути
secret/notes/db(урок 9.1), полеpassword. Опиши ExternalSecret (объект ESO, который читает значение из Vault и создаёт из него обычный Secret, урок 9.2), создающий Secretnotes-db-app. Разбор:refreshInterval: 1hзначит сверять с Vault раз в час;secretStoreRefуказывает на хранилищеvault-backend; вdataописано, какое поле брать из Vault (property: password); вtemplateзаготовка итогового Secret: типkubernetes.io/basic-authи два ключа. Выражение с точкой в двойных фигурных скобках подставляет значениеpassword.
cd ~/notes-gitops
cat > apps/notes/cnpg-secret.yaml <<'YAML'
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: notes-db-app
namespace: notes
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: vault-backend
target:
name: notes-db-app
template:
type: kubernetes.io/basic-auth
data:
username: notes
password: "{{ .password }}"
data:
- secretKey: password
remoteRef:
key: notes/db
property: password
YAML
- Опиши кластер. Построчно:
instances: 2это два инстанса;imageNameобраз PostgreSQL от проекта CNPG (версия закреплена);storage.sizeразмер PVC на каждый инстанс;bootstrap.initdbсоздаёт базуnotesи владельцаnotesс паролем из Secret;backup.retentionPolicy: "7d"хранить бэкапы 7 дней;destinationPathпутьбакет/папкав S3;endpointURLадрес MinIO внутри кластера (minioэто Service,minioвторой раз namespace);s3Credentialsоткуда брать ключи (имя Secret и ключи в нём);wal.compression: gzipсжимать WAL перед отправкой:
cat > apps/notes/cnpg-cluster.yaml <<'YAML'
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: notes-db
namespace: notes
spec:
# 2 инстанса: primary и реплика (в режиме 8 ГБ поставь 1)
instances: 2
imageName: ghcr.io/cloudnative-pg/postgresql:18.6
storage:
size: 2Gi
bootstrap:
initdb:
database: notes
owner: notes
secret:
name: notes-db-app
backup:
retentionPolicy: "7d"
barmanObjectStore:
destinationPath: s3://cnpg-backups/notes-db
endpointURL: http://minio.minio.svc:9000
s3Credentials:
accessKeyId:
name: minio-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: minio-creds
key: ACCESS_SECRET_KEY
wal:
compression: gzip
YAML
- Добавь оба файла в
apps/notes/kustomization.yaml, закоммить, запушь и дождись.kubectl wait --for=condition=Readyблокируется, пока у объекта не появится условие «готов» (или не пройдут 300 секунд);-L cnpg.io/instanceRoleдобавляет колонку со значением метки роли;grep notes-dbоставляет только наши Service:
git add -A && git commit -m "cnpg: cluster notes-db" && git push
flux reconcile kustomization apps --with-source
kubectl -n notes wait cluster/notes-db --for=condition=Ready --timeout=300s
kubectl -n notes get pods -L cnpg.io/instanceRole
kubectl -n notes get svc | grep notes-db
Что должно получиться:
cluster.postgresql.cnpg.io/notes-db condition met
NAME READY STATUS RESTARTS AGE INSTANCEROLE
notes-db-1 1/1 Running 0 2m primary
notes-db-2 1/1 Running 0 80s replica
notes-db-r ClusterIP 10.96.41.7 <none> 5432/TCP 2m
notes-db-ro ClusterIP 10.96.12.90 <none> 5432/TCP 2m
notes-db-rw ClusterIP 10.96.201.33 <none> 5432/TCP 2m
Как читать вывод: condition met значит, что кластер готов. В таблице подов колонка READY 1/1 говорит, что единственный контейнер работает; в последней колонке роль: primary пишет, replica копирует. Возраст 80s у второго пода показывает, что реплика создаётся после primary. Три строки Service это -r, -ro, -rw, у каждого свой ClusterIP (внутренний адрес, у тебя будут другие) и порт 5432, стандартный порт PostgreSQL.
Объясни себе:
- Почему пароля нет в git, но кластер его получает?
- Что случится, если ESO ещё не создал
notes-db-app, а Cluster уже применился?
Типичные ошибки:
secret "notes-db-app" not found: ExternalSecret не синхронизировался:kubectl -n notes get externalsecret, смотри статус и путь в Vault.Error: cannot fetch secret ... permission denied: политикаnotes-readне покрывает путь: поправь политику Vault из 9.1.unable to create pod: exceeded quotaилиPendingуnotes-db-2: на kind не хватает памяти: режим 8 ГБ,instances: 1.
Перед убийством primary попроси нейросеть составить список того, что ты ожидаешь увидеть в
kubectl cnpg statusдо и после. Сверь с реальным выводом: расхождение укажет, чего ты не понял.
Задание 3. Failover: убей primary
Цель: увидеть переключение и замерить, сколько записей приложение не смогло сделать.
Предскажи: после kubectl delete pod notes-db-1 какой под станет primary и сменится ли адрес notes-db-rw?
Ответ
Primary станет notes-db-2. ClusterIP сервиса notes-db-rw останется прежним, изменится только endpoint за ним. Удалённый notes-db-1 вернётся как реплика.
Шаги:
- В первом терминале запусти запись раз в секунду через временный под (пароль берём из Secret). Разбор:
kubectl run writerзапускает подwriter;--rm -it --restart=Neverудалит его после выхода и подключит терминал;--env=PGPASSWORD=...передаёт пароль клиентуpsqlчерез переменную окружения, а$(... | base64 -d)достаёт пароль из Secret (в Secret всё хранится в base64,-dдекодирует).psql -h notes-db-rw -U notes notes -tAc "...":-hхост,-Uпользователь, следующееnotesимя базы,-tбез заголовков,-Aбез выравнивания,-cвыполнить запрос. Сам запрос выводит текущее время и признакpg_is_in_recovery()(tзначит «я реплика»,f«я primary»).2>&1 | head -1сводит ошибки и вывод в поток и оставляет первую строку,sleep 1пауза секунду:
kubectl -n notes run writer --rm -it --restart=Never \
--image=ghcr.io/cloudnative-pg/postgresql:18.6 \
--env="PGPASSWORD=$(kubectl -n notes get secret notes-db-app -o jsonpath='{.data.password}' | base64 -d)" \
--command -- bash -c 'while true; do psql -h notes-db-rw -U notes notes -tAc "select now(), pg_is_in_recovery()" 2>&1 | head -1; sleep 1; done'
- Во втором удали primary и смотри роли:
kubectl -n notes delete pod notes-db-1 --wait=false
kubectl -n notes get pods -L cnpg.io/instanceRole -w
Что должно получиться: в терминале записи пара строк ошибок, потом снова успешные ответы.
2026-09-29 10:31:12.4+00|f
2026-09-29 10:31:13.4+00|f
psql: error: connection to server at "notes-db-rw" (10.96.201.33), port 5432 failed: Connection refused
psql: error: connection to server at "notes-db-rw" (10.96.201.33), port 5432 failed: Connection refused
2026-09-29 10:31:21.9+00|f
Как читать вывод: строки с временем и f это успешные записи на primary. Две строки Connection refused это время, когда за notes-db-rw не было ни одного работающего endpoint. Потом снова время и f: теперь отвечает новый primary. Между 10:31:13 и 10:31:21 прошло 8 секунд: это окно недоступности. Твои числа и количество ошибок будут другими.
Окно недоступности обычно от нескольких секунд до полуминуты. Это твой измеренный RTO для сбоя primary, запиши его в docs/ для урока 10.3.
Объясни себе:
- Почему ошибки были, если реплика уже стояла?
- Что бы произошло, если бы приложение держало пул соединений без переподключения?
Типичные ошибки:
Error from server (NotFound): pods "notes-db-1" not found: primary уже былnotes-db-2: смотри-L cnpg.io/instanceRoleи удаляй того, у когоprimary.FATAL: the database system is not yet accepting connections: соединился с только что поднявшимся инстансом: подожди и проверьkubectl cnpg status notes-db -n notes.
Вставь в нейросеть вывод
kubectl describe backupи спроси, на каком шаге остановилась копия. Сверь ответ с блокомEventsсам.
Задание 4. Бэкап и восстановление в новый Cluster
Цель: снять бэкап в MinIO и поднять из него отдельный кластер.
Предскажи: можно ли восстановить в кластер с тем же именем notes-db, пока старый жив?
Ответ
Нет: имена конфликтуют, поды и PVC уже существуют. Восстанавливаем в новый Cluster с другим именем, например notes-db-restore, проверяем данные и потом решаем, что с ним делать.
Шаги:
- Расписание и ручной бэкап.
backupOwnerReference: selfзначит, что созданные по расписанию объектыBackupпринадлежат самомуScheduledBackupи удаляются вместе с ним.kubectl apply -f - <<'YAML'применяет манифест, который идёт сразу за командой (а-f -значит «читать из стандартного ввода»),get backup -wпоказывает изменения на лету, выход поCtrl+C:
cat > apps/notes/cnpg-backup.yaml <<'YAML'
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: notes-db-daily
namespace: notes
spec:
# формат с секундами: каждый день в 03:00
schedule: "0 0 3 * * *"
cluster:
name: notes-db
backupOwnerReference: self
YAML
kubectl -n notes apply -f - <<'YAML'
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: notes-db-manual
namespace: notes
spec:
cluster:
name: notes-db
YAML
kubectl -n notes get backup -w
- Когда
PHASEстанетcompleted, создай кластер для проверки (не коммить, это временный эксперимент):
kubectl -n notes apply -f - <<'YAML'
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: notes-db-restore
namespace: notes
spec:
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:18.6
storage:
size: 2Gi
bootstrap:
recovery:
source: notes-db
externalClusters:
- name: notes-db
barmanObjectStore:
destinationPath: s3://cnpg-backups/notes-db
endpointURL: http://minio.minio.svc:9000
s3Credentials:
accessKeyId:
name: minio-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: minio-creds
key: ACCESS_SECRET_KEY
YAML
kubectl -n notes wait cluster/notes-db-restore --for=condition=Ready --timeout=300s
kubectl -n notes exec notes-db-restore-1 -c postgres -- psql -U postgres notes -tAc "select count(*) from notes"
- Убери эксперимент:
kubectl -n notes delete cluster notes-db-restore.
Что должно получиться:
NAME AGE CLUSTER METHOD PHASE ERROR
notes-db-manual 40s notes-db barmanObjectStore completed
cluster.postgresql.cnpg.io/notes-db-restore condition met
3
Как читать вывод: METHOD barmanObjectStore это способ бэкапа, PHASE completed значит копия готова и лежит в бакете. condition met у второго кластера значит, что он поднялся из бэкапа. Последняя строка это результат select count(*): здесь 3 заметки. Ключ -c postgres выбирает контейнер: в поде CNPG может быть несколько.
Число строк равно числу заметок на момент бэкапа. Полноценный учебный restore drill с замером RTO проводится один раз в уроке 10.3, здесь только механика.
Объясни себе:
- Чем
Backupотличается отScheduledBackup? - Почему бэкап в MinIO внутри того же kind защищает только от логических ошибок, а не от потери машины?
Типичные ошибки:
Error: The specified bucket does not exist: бакетcnpg-backupsне создан в MinIO: создай черезmc mb.PHASE failed ... Access Denied: неверные ключи вminio-credsnamespacenotes: сверь с секретом вminio.WAL archiving failedвkubectl cnpg status: неверныйendpointURL(нуженhttp://, неhttps://, без TLS в MinIO): исправь адрес.
Задание 5. Шаг проекта: «Заметки» переезжают на CNPG
Цель: перенести данные из StatefulSet postgres, переключить приложение на notes-db-rw и удалить старое.
Предскажи: что произойдёт с приложением, если сначала удалить StatefulSet, а потом делать дамп?
Ответ
Дамп сделать будет не из чего, если удалён и PVC (у StatefulSet PVC остаётся, но проще ошибиться). Порядок всегда: дамп, восстановление, проверка, переключение, и только потом удаление старого.
Шаги:
- Дамп из старой БД и заливка в новую (
postgres-0из урока 5.5). Разбор:kubectl exec postgres-0 -- pg_dump ...запускаетpg_dumpвнутри старого пода;> /tmp/notes-dump.sql(перенаправление вывода, урок 1.2) сохраняет SQL в файл у тебя на машине;exec -i ... < файлподаёт файл на входpsqlв новом поде. СтрокаSET ROLE notes;перед дампом делает так, что все таблицы создаются от имениnotes, а не суперпользователяpostgres: иначе приложение не получит прав на таблицы:
kubectl -n notes exec postgres-0 -- pg_dump -U notes --no-owner notes > /tmp/notes-dump.sql
(echo "SET ROLE notes;"; cat /tmp/notes-dump.sql) | kubectl -n notes exec -i notes-db-1 -c postgres -- psql -U postgres notes
kubectl -n notes exec notes-db-1 -c postgres -- psql -U postgres notes -tAc "select count(*) from notes"
- Приложение читает
DATABASE_URLиз Secretnotes-db(создаётся ExternalSecret из 9.2). Поменяй хост наnotes-db-rwв шаблоне, откуда он собирается (apps/notes/externalsecret.yaml), значение станетpostgresql://notes:<пароль>@notes-db-rw.notes.svc:5432/notes. Закоммить и запушь, приложение перечитает Secret после рестарта:
git add -A && git commit -m "notes: БД на CNPG (notes-db-rw)" && git push
flux reconcile kustomization apps --with-source
kubectl -n notes rollout restart deploy/notes
kubectl -n notes rollout status deploy/notes
curl -s --cacert /tmp/notes-ca.crt https://notes.lab/notes
- Убедись, что заметки на месте, и только потом удали старое из git (
apps/notes/: файлы StatefulSetpostgres, Servicedb, CronJobpg-backup, PVCpg-backups), закоммить и запушь. Flux сprune: trueудалит объекты. PVCdata-postgres-0удали вручную после проверки:kubectl -n notes delete pvc data-postgres-0. - Сохрани
/tmp/notes-dump.sqlкак последнюю страховку до конца урока, потом удали файл.
Что должно получиться:
3
deployment "notes" successfully rolled out
[{"id":1,"text":"первая","created_at":"2026-09-29T09:01:11Z"},{"id":2,"text":"вторая","created_at":"2026-09-29T09:02:03Z"},{"id":3,"text":"третья","created_at":"2026-09-29T09:02:40Z"}]
Проверь, что старого нет:
kubectl -n notes get sts,cronjob
No resources found in notes namespace.
Состояние проекта: версия 0.7.0, Cluster notes-db, Service notes-db-rw, бэкапы в MinIO. Долг: MinIO без репликации.
Как читать вывод: 3 это число строк в новой базе, оно должно совпасть с тем, что было в старой. Строка rolled out подтверждает, что новые поды приложения запущены с новым DATABASE_URL. JSON последней строкой это ответ приложения: три заметки на месте. Даты у тебя будут свои. No resources found после проверки значит, что StatefulSet и CronJob удалены.
Объясни себе:
- Почему
pg_dumpс--no-owner? - Как убедиться, что в новой БД не меньше строк, чем в старой?
Типичные ошибки:
psql: error: connection to server ... FATAL: password authentication failed for user "notes": в Vault и вnotes-db-appразные пароли, либо вDATABASE_URLостался старый: сравниkubectl get secretобоих и обнови из Vault.ERROR: permission denied for table notesв логах приложения после переключения: таблицы созданы от имениpostgres, потому что залили безSET ROLE notes;. Выдай праваALTER TABLE notes OWNER TO notes;или повтори заливку в чистую базу.ERROR: role "old_owner" does not existпри заливке: дамп снят без--no-ownerи ссылается на роль старой базы, которой в новой нет: сними дамп заново с флагом.pg_dump: error: connection to server on socket ... failed: подаpostgres-0нет, StatefulSet уже удалён: восстанови из последнегоpg-backup(PVCpg-backups) или из/tmp/notes-dump.sql.
Сломай и почини
Скрипта поломки для этого урока нет: ты ломаешь сам, руками, через git. Выбери один из трёх сценариев и попроси коллегу (или себя завтрашнего) не подглядывать в разбор ниже.
- Запись в реплику. В
apps/notes/externalsecret.yamlпоменяй хост вDATABASE_URLсnotes-db-rwнаnotes-db-ro, закоммить, запушь, перезапусти Deploymentnotes. - Бэкап падает. В
apps/notes/cnpg-cluster.yamlзамениendpointURLнаhttps://minio.minio.svc:9000(у нашего MinIO нет TLS), закоммить, запушь, создай новыйBackup. - Primary удалён и не возвращается. Поставь
instances: 1(закоммить, запушь, дождись), затемkubectl -n notes delete pod notes-db-1 --wait=falseи удали PVCnotes-db-1командойkubectl -n notes delete pvc notes-db-1. Это учебный кластер, данные учебные. Перед этим убедись, что есть свежийBackup.
Чини через git (Flux откатит ручные правки), не через kubectl edit.
Симптом
Приложение отвечает 500 на POST /notes, в логах cannot execute INSERT in a read-only transaction, либо kubectl get backup показывает failed, либо в kubectl get pods подов notes-db нет или один из них не в Running.
Гипотезы
- Приложение ходит в
notes-db-roили в имя реплики, а не в-rw. - Бэкап: неверный endpoint, ключи или нет бакета.
- Primary недоступен: под не стартует, PVC потерян, нет места, отсутствует реплика для повышения.
Проверки
kubectl cnpg status notes-db -n notes
kubectl -n notes get cluster notes-db -o wide
kubectl -n notes describe backup "<имя>"
kubectl -n notes logs deploy/notes --tail=20
kubectl -n notes get endpoints notes-db-rw
cnpg status сводит состояние кластера, репликации и архивации WAL в один экран. get endpoints notes-db-rw показывает, есть ли за адресом записи живой primary: пустой список значит, что писать некуда. Подумай, какая проверка сужает поиск быстрее всего.
Исправление
Разбор сценариев
- Запись в реплику:
DATABASE_URLуказывает наnotes-db-roилиnotes-db-2. Проверьpsql -c "select pg_is_in_recovery()"через этот хост,tзначит реплика. Исправь хост наnotes-db-rwвexternalsecret.yaml, закоммить, перезапусти Deployment. - Бэкап
failed:kubectl describe backupи логи пода primary в контейнереpostgresпокажутAccess Denied,bucket does not existили ошибку соединения. ПоправьendpointURLили ключи вminio-creds, создай бакет, запусти новыйBackup. Убедись, что WAL идёт:kubectl cnpg statusбезNot workingв блоке WAL archiving. - Primary удалён и не возвращается: если есть реплика, оператор повысит её сам, дождись; если реплики нет (
instances: 1), нужен restore из бэкапа в новый Cluster (задание 4). Смотри событияkubectl -n notes get events --sort-by=.lastTimestamp. Профилактика:instancesне меньше 2 и рабочий бэкап.
ИИ в помощь
Нейросеть хорошо объясняет вывод kubectl cnpg status и пишет черновики манифестов, но путает поля CNPG между версиями. Общие правила: ИИ-помощник. Настоящие значения секретов в запрос не вставляй: заменяй их на CHANGE_ME.
Задача: разобрать состояние кластера.
Вот вывод `kubectl cnpg status notes-db -n notes`:
<вставь вывод>.
Скажи по блокам: кто primary, сколько реплик готово, работает ли архивация WAL и какое отставание. Что в этом выводе настораживает?
Проверь ответ: каждое утверждение найди в самом выводе. Типичная ошибка: нейросеть объявляет кластер здоровым по блоку Cluster Summary и пропускает Working WAL archiving.
Задача: написать ScheduledBackup.
Напиши `ScheduledBackup` CloudNativePG для кластера `notes-db`, чтобы копия делалась каждый день в 03:00. Объясни каждое поле расписания.
Проверь ответ: расписание должно быть из шести полей, первое секунды: "0 0 3 * * *". Типичная ошибка: расписание из пяти полей, как в обычном cron.
Задача: найти причину ошибки подключения.
Приложение пишет: <вставь ошибку>. Строка подключения: postgresql://notes:пароль@notes-db-rw.notes.svc:5432/notes. Укажи, какая из пяти частей адреса виновата, и дай команду проверки.
Проверь ответ: сверь с таблицей из теории: хост, порт, база, пользователь, пароль. Типичная ошибка: совет «перезапусти базу» вместо проверки Secret или имени Service.
Словарик урока
| Термин | Простыми словами |
|---|---|
| Оператор (operator) | Программа в кластере, которая знает, как эксплуатировать конкретную систему, и постоянно приводит её к описанному состоянию |
| CRD | Описание нового типа ресурса, который добавляют в API Kubernetes (Cluster, Backup) |
| Контроллер, reconcile loop | Программа и её цикл «прочитать желаемое, сравнить с фактическим, исправить разницу» |
| Instance (инстанс) | Один экземпляр базы: под и его диск |
| Primary | Главный инстанс, принимает запись |
| Replica (реплика) | Копия primary только для чтения, получает изменения потоком |
| WAL | Журнал изменений PostgreSQL, по которому реплика догоняет, а бэкап восстанавливается |
| Streaming replication | Реплика получает WAL от primary непрерывным потоком |
| Асинхронная репликация | Primary подтверждает запись, не дожидаясь реплики: быстро, но возможна потеря последних записей |
| Failover / switchover | Аварийное или плановое переключение роли primary на другую реплику |
| RPO / RTO | Допустимая потеря данных и допустимое время простоя (урок 10.3) |
| Base backup | Физическая копия файлов базы на момент времени |
| PITR | Восстановление на выбранный момент: базовая копия плюс проигрывание WAL |
| Barman | Инструмент, который CNPG использует для бэкапов и архивации WAL |
| S3, бакет | Стандарт хранения файлов-объектов по HTTP и «корзина» верхнего уровня; MinIO это S3 у нас в кластере |
ScheduledBackup / Backup |
Бэкап по расписанию / разовый бэкап |
bootstrap.initdb / recovery |
Создать пустую базу / поднять кластер из бэкапа |
notes-db-rw, -ro, -r |
Service на primary / на реплики / на любой инстанс |
| строка подключения (DSN) | адрес базы одной строкой: пользователь, пароль, хост, порт, имя базы |
| объектное хранилище | хранилище файлов-объектов в бакетах, доступ по HTTP (S3 и совместимые) |
endpointURL |
адрес S3-совместимого сервера (у нас MinIO); без него CNPG идёт в AWS |
kubectl cnpg status |
сводка по кластеру: главный, реплики, отставание, работа архивации WAL |
| репликация | постоянное копирование изменений главной базы на запасной экземпляр |
Cluster (CNPG) |
ресурс: одна база с её копиями, не кластер Kubernetes |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] Что такое оператор в Kubernetes и чем он отличается от Helm-чарта?
Ответ
Оператор это CRD плюс контроллер, который постоянно сверяет желаемое состояние с фактическим и знает, как эксплуатировать конкретную систему. Helm один раз рисует манифесты. Оператор реагирует на события: упал primary, пора сделать бэкап, надо обновить версию.
Что хотят услышать: reconcile loop, CRD, эксплуатационные знания в коде, пример (CNPG, cert-manager, Prometheus Operator).
Красный флаг: «это такой продвинутый Helm» или «это про установку».
2. [middle] [часто] Как в CNPG устроены бэкапы и восстановление на момент времени?
Ответ
Base backup плюс непрерывная архивация WAL в объектное хранилище. Для PITR создаётся новый Cluster с bootstrap.recovery и recoveryTarget.targetTime. Восстанавливаем в новое имя, проверяем и переключаем приложение.
Что хотят услышать: Barman, WAL, ScheduledBackup, восстановление в новый кластер, проверка бэкапов (restore drill).
Красный флаг: «делаю pg_dump раз в сутки» как единственный вариант, или бэкапы в том же кластере и не проверены.
3. [middle] [часто] Стоит ли держать PostgreSQL в Kubernetes?
Ответ
Зависит от команды и требований. Плюсы: единый подход, GitOps, автоматизация. Минусы: своя эксплуатация хранилища, апгрейдов и бэкапов, зависимость от качества CSI и сети. Если нет людей на эксплуатацию БД, надёжнее управляемый сервис. С оператором и проверенными бэкапами вариант рабочий.
Что хотят услышать: взвешенный ответ с критериями (SLA, команда, стоимость, данные), не догма, упоминание управляемых сервисов.
Красный флаг: категоричное «никогда» или «всегда» без аргументов.
4. [middle] Прод: приложение пишет в БД, primary в CNPG умер. Что происходит и что ты делаешь?
Ответ
Оператор обнаруживает потерю primary, повышает реплику с наименьшим отставанием и переключает Service -rw. Я смотрю kubectl cnpg status, роли подов, endpoints -rw, и что приложение переподключается. Если реплики не было, иду в восстановление из бэкапа.
Что хотят услышать: Service -rw, окно недоступности, переподключение пула, асинхронная репликация и возможная потеря последних транзакций, проверка после.
Красный флаг: «зайду в под и вручную сделаю pg_promote» без проверки состояния оператора.
5. [junior] [на скорость] Через какой Service приложение должно подключаться к CNPG и почему?
Ответ
К notes-db-rw: он всегда указывает на текущий primary. -ro ведёт на реплики, поэтому запись там завершится ошибкой read-only. Имена подов использовать нельзя, primary переезжает.
Что хотят услышать: три сервиса, назначение каждого, отличие от имени пода.
Красный флаг: «хожу на IP пода» или «на любой сервис, они одинаковые».
6. [middle] Приложение в 500, в логах «cannot execute INSERT in a read-only transaction». Твои действия?
Ответ
Это запись в реплику. Проверяю, на какой хост смотрит DATABASE_URL, и select pg_is_in_recovery() через него. Возможно, недавно был failover, а клиент держит старое соединение с бывшим primary, ставшим репликой. Исправляю хост на -rw или перезапускаю приложение, чтобы пул переподключился.
Что хотят услышать: pg_is_in_recovery, связь с failover, пулы соединений, таймауты и retry.
Красный флаг: «перезагружу базу», не разобравшись.
7. [middle] Нужно поднять новую версию PostgreSQL в CNPG с минимальным простоем. Как?
Ответ
Минорное обновление: меняю imageName, оператор делает rolling update: сначала реплики, потом primary (по умолчанию перезапуск на месте, с primaryUpdateMethod: switchover через переключение на обновлённую реплику). Для мажорного апгрейда нужен отдельный план (импорт в новый кластер или поддерживаемый in-place механизм), делаю сначала на копии из бэкапа. Перед этим свежий бэкап.
Что хотят услышать: rolling по репликам, primary последним (restart или switchover), свежий бэкап, репетиция на копии, различие минорных и мажорных версий.
Красный флаг: «просто поменяю тег и посмотрю».
8. [middle] Пароль БД в Vault сменили, а приложение не может подключиться. Что не так и как правильно менять пароль?
Ответ
Смена в Vault не меняет пароль роли в самой PostgreSQL: ESO обновит Secret, но пользователь notes в БД остался со старым. Нужно синхронно сменить и роль (ALTER ROLE или через управление ролями оператора), и Secret, и перезапустить приложение. Проверяю refreshInterval ESO и статус ExternalSecret.
Что хотят услышать: источник правды один, порядок смены, ESO обновляет Secret, но не БД, перечитывание в приложении.
Красный флаг: «сменил в Vault, значит всё обновится».
9. [middle] Диск primary заполняется WAL, что делать?
Ответ
Смотрю, не застрял ли архив WAL (kubectl cnpg status, метрики), не отвалился ли MinIO или ключи: пока архив не работает, WAL копятся на диске. Чиню архивацию, WAL уйдёт и очистится. Срочно: расширяю PVC (storage.size, если класс позволяет), но причину чиню всегда.
Что хотят услышать: связь архива WAL и диска, kubectl cnpg status, расширение тома, алерты на место и на возраст архивации.
Красный флаг: «удалю файлы WAL из pg_wal руками».
10. [middle] Прод отвечает 502 после failover в БД. Как разбираешься?
Ответ
Иду по цепочке: логи приложения, endpoints notes-db-rw, роли подов, readiness-пробы. Частая причина: пул не переподключился или проба readyz зависит от БД и поды выпали из Service. Смотрю время сбоя, перезапускаю проблемные поды, потом чиню переподключение и таймауты.
Что хотят услышать: цепочка проверок, readiness, поведение пула, метрики и логи вместо догадок.
Красный флаг: «увеличу реплики nginx».
11. [junior] Что такое WAL в PostgreSQL и зачем он нужен?
Ответ
WAL (write-ahead log) - журнал, в который Postgres сначала записывает изменения, а потом уже применяет к файлам данных. Благодаря ему база переживает падение: после старта повторяет журнал. Реплики получают тот же поток WAL и применяют его, а для восстановления на момент времени архивирую сегменты WAL в объектное хранилище. Если WAL не успевают архивироваться или реплика отстала, журнал копится на диске primary и может его заполнить.
Что хотят услышать: журнал до записи данных, восстановление после сбоя, репликация, PITR, риск заполнения диска.
Красный флаг: путает WAL с логами приложения.
12. [middle] Зачем нужен пулер соединений и есть ли он в CNPG?
Ответ
У Postgres каждое соединение - отдельный процесс, поэтому сотни подключений от множества подов дорого стоят по памяти. Пулер, например PgBouncer, держит небольшое число реальных соединений и раздаёт их клиентам. В CNPG для этого есть ресурс Pooler, который ставится перед кластером. Нужно помнить про режим пулинга: в transaction-режиме не работают некоторые возможности сессии, например prepared statements в старых драйверах или SET на сессию.
Что хотят услышать: соединение это процесс, пулер уменьшает нагрузку, Pooler в CNPG, ограничения режима transaction.
Красный флаг: просто поднимает max_connections до тысяч.
13. [middle] Как обновить ноду Kubernetes с кластером CNPG так, чтобы БД не упала?
Ответ
Кластер должен быть минимум из двух экземпляров, лучше трёх, с PodDisruptionBudget, который оператор создаёт сам. При kubectl drain с ноды выселяют под. Если это primary, оператор делает switchover на реплику, а потом выселяется бывший primary. Перед работой смотрю kubectl cnpg status <кластер>: реплики должны быть в синхроне. Если хранилище привязано к ноде (local PV), под будет ждать именно её, об этом нужно знать заранее.
Что хотят услышать: несколько экземпляров, PDB, switchover, проверка репликации до работ, особенность локальных дисков.
Красный флаг: дренит все ноды подряд и не проверяет реплики.
Проверено на версиях
Кластер Kubernetes для этого урока не запускался, вывод команд реалистичный, но не снят с живого кластера. Манифесты и версии проверены чтением и сверены с документацией проекта, а не прогоном.
- CloudNativePG: v1.30.1 (не прогонялось)
- PostgreSQL: 18.6 (не прогонялось)
- Flux: v2.9.5
- External Secrets Operator: v2.11.0
- Vault: v2.1.1
- MinIO: версия не закреплена, проверь актуальную версию на странице проекта
- Версия Helm-чарта
cloudnative-pg: проверь актуальную версию на странице проекта
Итог урока: ты умеешь
- умею объяснить, чем оператор отличается от Helm-чарта
- умею установить CloudNativePG через
HelmReleaseи увидеть его CRD - умею описать
Clusterс паролем из Vault через ESO - умею назвать, какой Service для записи, а какой для чтения
- умею вызвать failover и замерить окно недоступности
- умею настроить
ScheduledBackupв S3-совместимое хранилище и восстановить в новый Cluster - умею перенести данные
pg_dumpиpsqlи удалить старый StatefulSet - умею диагностировать запись в реплику и упавший бэкап
Дальше: Урок 9.6: прогрессивная доставка
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.