✻ Урок 5.5 · Тема 5: Kubernetes и Helm
Хранилище и StatefulSet: PostgreSQL в кластере
Содержание урока
Зачем это нужно
Поды в Kubernetes одноразовые: убил, создался новый, с пустой файловой системой. В уроке 5.2 «Заметки» писали в emptyDir (временный каталог, который живёт ровно столько же, сколько под), и данные пропадали вместе с подом. Для базы данных это неприемлемо: ей нужен диск, который переживает под, и стабильное имя, чтобы клиенты всегда находили её на месте.
На работе это первый вопрос к любому сервису с данными в кластере: где живут данные, кто их создаёт, что будет при удалении пода, при удалении StatefulSet и при удалении всего namespace. Ошибка здесь стоит потери данных, а не просто рестарта.
Шаг проекта: в k8s/base/40-postgres.yaml появляются StatefulSet postgres (образ postgres:18), headless Service db и PVC на 1 ГиБ; Secret notes-db с паролем создан командой и в git не попадает.
Четыре новых слова, чтобы не спотыкаться дальше. StatefulSet - контроллер (объект, который следит, чтобы нужные поды существовали), который делает поды с постоянными именами и своим диском у каждого, в отличие от Deployment из урока 5.2, где поды взаимозаменяемы. PVC (PersistentVolumeClaim) - заявка на диск: ты пишешь «нужен 1 ГиБ», а кластер находит или создаёт подходящий. Без заявки пришлось бы вручную знать, какой именно диск где лежит. Headless Service (безголовый сервис) - сервис из урока 5.3 без общего адреса: DNS отдаёт клиенту адрес конкретного пода, а не случайного из группы. Без него клиент не смог бы обратиться именно к postgres-0. Secret - объект для пароля: значение хранится отдельно от манифеста Deployment и не попадает в git (подробно разберём в уроке 5.6). Все четыре подробно разобраны ниже в теории. База доступна по адресу db.notes.svc:5432.
Что нужно знать
- Урок 4.3: тома и сети Docker - идея тома, который живёт отдельно от контейнера
- Урок 4.4: SQL и PostgreSQL -
psql, таблицаnotes - Урок 4.5: Compose и PostgreSQL - переменные
POSTGRES_*, инициализация базы - Урок 5.2: Поды и Deployment - почему под одноразовый,
emptyDir - Урок 5.3: Service и DNS кластера - Service, endpoints (список адресов подов, на которые Service раскидывает запросы), DNS-имена
<svc>.<ns>.svc
Если что-то из этого забылось, ничего страшного: каждый термин ниже напоминается одной фразой.
Картина целиком
Представь гостиницу с дежурными администраторами. Администратор (под) работает посменно: смена закончилась, пришёл другой человек, который ничего не помнит. Если администратор записывал брони на листке в кармане, при смене листок пропал. Поэтому брони записывают в журнал, который лежит в сейфе на ресепшене и переходит от смены к смене.
- Под это администратор: пришёл, поработал, ушёл. Его собственная память (файловая система контейнера) исчезает вместе с ним.
- Журнал в сейфе это данные базы. Технически такой отдельный каталог называется том (volume): папка, которая живёт отдельно от пода и подключается в него, как флешка в компьютер. Они должны жить отдельно от того, кто в них пишет.
- PVC (заявка на том) это записка «мне нужен сейф на 1 ГиБ». Ты не выбираешь конкретный сейф, ты описываешь, что нужно.
- StorageClass (класс хранилища) это завхоз, который по записке выдаёт или изготавливает подходящий сейф. Готовый сейф называется PV (постоянный том).
- StatefulSet это график, по которому смены имеют фиксированные имена («Первый администратор», «Второй администратор») и у каждого свой закреплённый сейф. Пришёл новый человек на смену «Первого»: он получает тот же сейф.
- Headless Service это табличка с именами на двери: по ней клиенты находят именно «Первого», а не любого свободного.
flowchart TD
C["клиент: под notes"] -->|"db.notes.svc:5432"| S["Service db (headless)<br>DNS отдаёт адрес пода postgres-0"]
S --> P["под postgres-0"]
SS["StatefulSet postgres<br>следит: жив ли postgres-0"] -->|"создаёт и следит"| P
P -->|"монтирует"| PVC["PVC data-postgres-0"]
PVC --> PV["PV: кусок диска узла"]
SC["StorageClass standard<br>создал PV по заявке"] -.-> PV
Аналогия ломается в одном месте: настоящий сейф нельзя случайно выбросить вместе с ресепшеном, а в Kubernetes удаление PVC или всего namespace уничтожает данные без предупреждений. Поэтому большая часть урока про то, что защищает данные, а что нет.
За урок ты разберёшь каждый кусок схемы: зачем данным отдельное хранилище, как устроены PV, PVC и StorageClass, чем StatefulSet отличается от Deployment, зачем нужен headless Service и какие подводные камни есть у самой PostgreSQL 18.
Теория
Зачем данным отдельное место
Начнём с проблемы. Контейнер это запущенная копия образа. Образ (image) только читается, а поверх него контейнеру выдают тонкий слой для записи. Когда контейнер удаляют, удаляется и этот слой. Поэтому всё, что программа записала «просто в файл», живёт до первой остановки контейнера.
Для приложения без состояния это отлично: любой под можно убить и заменить, ничего не потеряется. Для базы данных это катастрофа. Сначала база пишет данные в каталог, потом кто-то удаляет под (обновление, сбой узла, перенос на другой узел), и Kubernetes создаёт новый под с чистым слоем: база стартует пустой.
Решение придумали задолго до Kubernetes: том (volume) это каталог, который живёт отдельно от контейнера и подключается (монтируется, mount) в него по нужному пути. Монтирование значит: «показать этот каталог внутри контейнера в папке /var/lib/postgresql». Программа пишет в папку, а на самом деле данные ложатся в том. Контейнер умер, том остался, новый контейнер смонтировал тот же том и увидел свои данные. В Docker ты уже так делал в уроке 4.3.
В Kubernetes есть тома разных видов. Сравним два, которые уже встречались или встретятся:
| Вид тома | Что это | Пережил ли пересоздание пода |
|---|---|---|
emptyDir |
пустой каталог, создаётся вместе с подом | нет, удаляется вместе с подом |
| PVC (persistentVolumeClaim) | каталог или диск, который живёт отдельно от пода | да |
Разберём, что происходит по шагам, когда данные лежат в emptyDir, и когда на PVC:
flowchart LR
subgraph E["emptyDir"]
direction TB
E1["1. под создан, каталог создан"] --> E2["2. база пишет данные"] --> E3["3. под удалён, каталог удалён"] --> E4["4. новый под: каталог пустой"]
end
subgraph V["PVC"]
direction TB
V1["1. PVC создан, том создан"] --> V2["2. под создан, том подключён"] --> V3["3. база пишет данные"] --> V4["4. под удалён, том остался"] --> V5["5. новый под подключает тот же том: данные на месте"]
end
Осторожно, путаница: многие думают, что «том» и «файловая система контейнера» одно и то же, поэтому данные должны сохраняться сами. Отличить просто: спроси себя, какая команда удалит это. Если kubectl delete pod удаляет и данные, значит, это не том с сохранением, а временное место.
Прикинь сам: база записала данные в каталог внутри контейнера, и кто-то выполнил
kubectl delete pod. Что найдёт новый под?
Чистый слой: старый слой записи удалился вместе с контейнером, база стартует пустой. Данные сохраняет только том, подключённый отдельно.
Проверь понимание: чем
emptyDirотличается от тома на PVC? В каком случаеemptyDirвсё же подходит?
Ответ
emptyDir создаётся вместе с подом и удаляется вместе с ним, PVC живёт отдельно. emptyDir подходит для временных данных: кэш, промежуточные файлы, обмен файлами между контейнерами одного пода. Базе данных он не подходит.
Главное: том живёт отдельно от пода;
emptyDirудаляется вместе с подом, а том на PVC переживает его.
Чтобы получить такой том, в Kubernetes нужны три объекта, и вот зачем их три.
PV, PVC и StorageClass: как получить диск
Зачем три объекта вместо одного? Разработчик приложения не знает и не должен знать, какие диски есть в кластере: локальные SSD, сетевые диски облака или NFS. Он знает, сколько места нужно и как его использовать. А администратор знает, какие диски бывают, но не знает, сколько нужно каждому приложению. Три объекта разделяют эти заботы.
- PersistentVolume, PV (постоянный том): конкретный кусок хранилища в кластере. Пример: каталог
/var/local-path-provisioner/pvc-8b1a.../на диске узла, или сетевой диск облака. Это ресурс всего кластера, у него нет namespace (напомним: namespace это «папка» для объектов кластера, см. урок 5.1). - PersistentVolumeClaim, PVC (заявка на том): «мне нужен 1 ГиБ, чтение и запись с одного узла». Заявка лежит в namespace и подключается в под по имени.
- StorageClass (класс хранилища): рецепт, как создавать PV по заявке. В нём записано, кто создаёт диск (provisioner, «поставщик»), что делать с диском после удаления заявки и когда его создавать.
Аналогия: PVC это заявка на склад канцтоваров «нужна тетрадь на 48 листов», PV это конкретная тетрадь на полке, StorageClass это правило склада: «тетради берём у такого-то поставщика, использованные выбрасываем».
Как это происходит по шагам. Когда PVC создан, кластер ищет для него PV:
- Ты создаёшь PVC и указываешь размер, режим доступа и (необязательно)
storageClassName. Если имя класса не указано, берётся класс по умолчанию (в kind этоstandard). - Контроллер смотрит на класс и просит его поставщика создать том. Так создание тома по требованию называется динамическим provisioning.
- Поставщик создаёт PV, кластер связывает (bind) PV и PVC: PVC получает статус
Bound. - Под ссылается на PVC по имени, kubelet (агент на каждом узле, который запускает контейнеры) монтирует том в контейнер.
В kind по умолчанию есть StorageClass standard с поставщиком rancher.io/local-path: том создаётся как обычный каталог на диске того узла, где запущен под. Два свойства класса важны на практике:
reclaimPolicy(политика возврата) отвечает на вопрос «что делать с диском, когда заявку удалили».Deleteзначит: удалить и PV, и данные на нём.Retainзначит: оставить том, данные сохранены, администратор разберётся сам. Уstandardв kind стоитDelete, а в облаках продовые классы часто ставятRetain.volumeBindingMode(когда создавать том).Immediateзначит «сразу, при создании PVC».WaitForFirstConsumerзначит «подожди, пока появится под, который будет использовать том, и создай том там, где этот под запущен». Смысл: локальный диск нужен на том же узле, где работает под, а узел выбирается только при запуске пода. Поэтому пустой PVC долго висит вPending, и это нормально.
Ещё одно свойство заявки: режим доступа (accessModes). ReadWriteOnce (RWO) значит, что том монтируется на запись одним узлом. ReadWriteMany (RWX) значит, что несколькими узлами сразу (нужен специальный вид хранилища вроде NFS). База почти всегда работает на RWO: два процесса СУБД не должны писать в один каталог.
Обрати внимание на слабое место локальных дисков: они живут на конкретном узле. Если узел сломался, том недоступен, пока узел не вернётся. Поэтому в облаках диски сетевые (EBS в AWS, Network SSD в Yandex Cloud): их подключением управляет CSI-драйвер (Container Storage Interface, единый интерфейс, через который кластер подключает хранилища любых производителей), и такой диск можно отвязать от умершего узла и подключить к другому.
Разберём настоящий вывод. Так выглядит kubectl -n notes get sc,pvc,pv для нашей базы (-n notes нужен для PVC, а StorageClass и PV не имеют namespace) (значение pvc-8b1a... у тебя будет другим):
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
storageclass.storage.k8s.io/standard (default) rancher.io/local-path Delete WaitForFirstConsumer false 2d
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
persistentvolumeclaim/data-postgres-0 Bound pvc-8b1a0e0d-42a3-4a7c-b6b1-5a0c3c9e2f41 1Gi RWO standard <unset> 40s
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS CLAIM STORAGECLASS VOLUMEATTRIBUTESCLASS REASON AGE
persistentvolume/pvc-8b1a0e0d-42a3-4a7c-b6b1-5a0c3c9e2f41 1Gi RWO Delete Bound notes/data-postgres-0 standard <unset> 39s
Читаем: класс standard помечен (default), значит, PVC без storageClassName получит его. В PVC колонка VOLUME содержит имя PV: это ссылка «заявка связана с этим томом». В PV колонка CLAIM содержит notes/data-postgres-0: обратная ссылка «том занят этой заявкой» (namespace/имя). Имена PV генерируются из UID заявки, поэтому длинные и случайные.
Колонка STATUS у PVC принимает три значения. Pending («ожидает»): заявка принята, но подходящий том ещё не привязан (при WaitForFirstConsumer это нормально, пока нет пода). Bound («привязан»): заявка связана с томом, под может его использовать. Lost («потерян»): том, с которым была связана заявка, исчез (например, PV удалили руками), и данные под угрозой. В повседневной работе ты чаще видишь Bound, а Pending дольше пары минут при уже запущенном поде повод открыть kubectl describe pvc. Колонка CAPACITY показывает фактический размер тома: он может оказаться больше запрошенного (запросил 1Gi, получил 1Gi у нашего kind, но облачные диски иногда округляют вверх), и никогда не меньше.
Колонка VOLUMEATTRIBUTESCLASS есть у свежих версий kubectl (между STORAGECLASS и AGE). Это «класс атрибутов тома»: необязательная настройка вроде скорости диска, которую можно поменять у уже созданного тома. Мы её не используем, поэтому везде <unset> («не задано»). Если у тебя в выводе этой колонки нет, версия kubectl старше, и это нормально. Колонка REASON у PV пустая, пока с томом всё в порядке.
Осторожно, путаница: PVC часто принимают за сам диск. Это только заявка, диск это PV. Второе заблуждение: «удалил pod, значит, удалил и PVC». Нет, PVC живёт отдельно, и именно поэтому данные переживают под.
Прикинь сам: PVC создан,
get pvcпоказываетPending, а пода ещё нет. Это поломка?
Не обязательно: при WaitForFirstConsumer том создают после появления пода. Поломка, если под уже есть, а PVC всё ещё Pending.
Проверь понимание: PVC создан, а
kubectl get pvcпоказываетPending, и под ещё не запущен. Это поломка?
Ответ
Не обязательно. При WaitForFirstConsumer том создаётся только после появления пода. Смотри kubectl describe pvc: событие waiting for first consumer to be created before binding штатное. Поломка, если под уже есть, а PVC всё ещё Pending: тогда ищи неверный storageClassName или нехватку места.
Главное: PVC это заявка, PV это сам диск, StorageClass это рецепт, как диск создавать; база почти всегда работает на RWO.
Размер в заявке пишут особым образом, и в нём легко ошибиться.
Как читать размеры: 1Gi, 500Mi, 1G
В заявке PVC размер записан как 1Gi. Зачем две буквы? Kubernetes различает два вида приставок, и путаница между ними стоит места на диске.
- Десятичные приставки:
k,M,Gэто 1000, 1000 в квадрате, 1000 в кубе байт.1Gэто 1 000 000 000 байт. - Двоичные приставки:
Ki,Mi,Giэто 1024, 1024 в квадрате, 1024 в кубе байт.1Giэто 1 073 741 824 байта (гибибайт).
Разница около 7%: 1Gi больше 1G. В Kubernetes принято писать двоичные (Gi, Mi), и в ресурсах пода тоже: то, что ты увидишь в уроке 5.7 как memory: 128Mi, устроено так же. Если написать 1g (строчная), Kubernetes откажется: регистр важен.
Разобранный пример: базе «Заметок» нужно 1 ГиБ. Реальные заметки занимают килобайты, поэтому 1Gi хватит с огромным запасом. Но пустая база PostgreSQL сама занимает порядка десятков мегабайт (системные таблицы и журнал), так что 100Mi было бы слишком мало. Размер нужно выбирать с запасом на рост, журналы и служебные файлы, а не по объёму полезных данных.
Осторожно, путаница: «Заказал 1Gi, значит, все 1Gi мои под данные». Часть места занимают служебные файлы базы и файловая система тома. Полезное место всегда немного меньше.
Прикинь сам: что больше,
1Giили1G, и почему в манифестах пишутGi?
1Gi больше: 1 073 741 824 байта против 1 000 000 000. Двоичные приставки точнее описывают, как компьютер считает диск и память.
Проверь понимание: что больше:
1Giили1G, и почему в манифестах Kubernetes принято писатьGi?
Ответ
1Gi больше (1 073 741 824 байта против 1 000 000 000). Двоичные приставки точнее описывают, как компьютер считает память и диск, и в Kubernetes они стали стандартом записи размеров.
Главное:
Giэто степени 1024,Gэто степени 1000; регистр важен, а размер выбирают с запасом на служебные файлы.
Теперь понятно, где хранить данные, и можно ответить, почему под базу не подходит Deployment.
Почему база не Deployment
Deployment (урок 5.2) создаёт набор одинаковых взаимозаменяемых подов. Их имена случайные: notes-6d9c-x4kq, notes-6d9c-p7zt. Для приложения без состояния это идеально: любой под можно заменить любым другим. Для базы данных это плохо по трём причинам.
- Общий диск. В шаблоне Deployment один PVC на все реплики. Две копии PostgreSQL, пишущие в один каталог, портят данные: у каждой свои кэши в памяти, и они не знают друг о друге.
- Случайные имена. Новый под получает новое имя и новый адрес. Клиенту не за что зацепиться, чтобы всегда находить «ту самую» базу.
- Порядок обновления. Deployment при обновлении сначала поднимает новый под, потом убивает старый. На короткое время два процесса работают с одним каталогом: это недопустимо.
Разобранный пример: у тебя replicas: 2 и один PVC. Оба пода монтируют его (при RWO на одном узле это возможно). Оба стартуют PostgreSQL. Первый записал страницу данных, второй не знает об этом и записывает свою поверх. Итог: повреждённая база, а kubectl get pods при этом показывает два здоровых пода.
Осторожно, путаница: «Поставлю replicas: 1 и Deployment, всё будет нормально.» Пока под жив, да. Но при обновлении Deployment создаст второй под ещё до остановки первого, и вы вернулись к проблеме двух процессов на одном диске. Для этого случая есть стратегия Recreate, но она не решает вопрос стабильного имени и отдельного диска на каждую реплику.
Прикинь сам: у Deployment
replicas: 1, и под базой один PVC. Почему при обновлении это всё равно опасно?
Deployment сначала поднимает новый под, потом убивает старый. На короткое время два процесса СУБД работают с одним каталогом.
Проверь понимание: назови две из трёх причин, по которым базу не запускают Deployment’ом.
Ответ
Общий диск для реплик (две копии СУБД портят данные), случайные имена и адреса (клиенту не за что зацепиться), обновление «сначала новый, потом удалить старый» (два процесса на одном диске).
Главное: для базы Deployment плох тремя вещами: общий диск, случайные имена и порядок обновления «сначала новый».
Для таких задач в Kubernetes есть другой контроллер.
StatefulSet: поды с постоянной идентичностью
StatefulSet («набор с состоянием») это контроллер, который, как и Deployment, поддерживает нужное число подов, но даёт каждому свою личность. Что он гарантирует:
- Стабильное имя. Поды называются
<имя>-0,<имя>-1и так далее:postgres-0,postgres-1. Число это порядковый номер (ordinal), он не меняется. Если удалитьpostgres-0, новый под снова получит имяpostgres-0. - Свой диск у каждой реплики. В StatefulSet есть поле
volumeClaimTemplates(«шаблоны заявок на том»). Для каждого пода контроллер создаёт свой PVC по шаблону, имя составляется из имени шаблона и имени пода:data-postgres-0. Новыйpostgres-0подключит именно этот PVC. - Порядок. Поды запускаются по очереди:
postgres-1не стартует, покаpostgres-0не станет готов. Обновляются в обратном порядке, от большего номера к меньшему. - Стабильное сетевое имя (через headless Service, о нём дальше).
Как это выглядит по шагам при kubectl delete pod postgres-0:
flowchart TD
A["1. Под postgres-0 удалён (Terminating)"] --> B["2. StatefulSet видит: должно быть 1 под, а его нет"]
B --> C["3. Создаёт под с тем же именем postgres-0"]
C --> D["4. В шаблоне ссылка на том data: подставляется PVC data-postgres-0, он уже есть"]
D --> E["5. kubelet монтирует PVC, PostgreSQL находит старый каталог данных и продолжает"]
Важное свойство: StatefulSet не удаляет PVC при своём удалении и при уменьшении числа реплик (поле persistentVolumeClaimRetentionPolicy по умолчанию Retain). Это осознанная защита данных от случайного delete statefulset. Но защита неполная: удаление самого PVC (delete pvc) и удаление всего namespace уничтожают данные, а при политике Delete у StorageClass вместе с PVC пропадёт и PV.
Что защищает и что нет, удобно держать в таблице:
| Действие | Данные остались? | Почему |
|---|---|---|
kubectl delete pod postgres-0 |
да | PVC жив, новый под подключит его |
kubectl delete statefulset postgres |
да | StatefulSet не удаляет PVC |
kubectl delete pvc data-postgres-0 |
нет (для standard) |
политика Delete уничтожает PV |
kubectl delete namespace notes |
нет | вместе с namespace удаляются все PVC |
Осторожно, путаница: «StatefulSet сам делает бэкапы и репликацию». Нет. Он только обеспечивает имя, диск и порядок. Настройкой репликации PostgreSQL и переключением между primary (главный сервер, принимает запись) и replica (копия, только читает) занимаются отдельные программы: операторы баз данных (например CloudNativePG, обзор в уроке 9.5).
Прикинь сам: ты удалил под
postgres-0. Какое имя получит новый под и какой PVC подключит?
Имя postgres-0 и PVC data-postgres-0: StatefulSet возвращает ту же личность и тот же диск. Данные на месте.
Проверь понимание: ты удалил под
postgres-0командойkubectl delete pod. Что произойдёт с данными и с именем нового пода?
Ответ
StatefulSet-контроллер создаст под с тем же именем postgres-0 и подключит тот же PVC data-postgres-0. Данные на месте, база стартует с существующего каталога (сработает восстановление после аварийного останова, crash recovery: PostgreSQL по журналу доводит данные до согласованного состояния). Клиенты по имени найдут её снова.
Главное: StatefulSet даёт имя
<имя>-N, свой PVC на каждую реплику и порядок запуска; PVC при удалении самого StatefulSet не удаляются.
Чтобы клиенты находили конкретный под, StatefulSet работает в паре с особым сервисом.
Headless Service и стабильное имя
В уроке 5.3 ты видел обычный Service типа ClusterIP: у него есть один виртуальный адрес, и запросы к нему распределяются между подами. Клиент не знает, какой именно под ответил, и ему это не важно.
Для базы важно. Когда у базы появятся реплики, приложению нужно писать в primary, а читать можно с replica. Нужно уметь обратиться к конкретному поду. Для этого StatefulSet используется вместе с headless Service (Service «без головы»): в нём указано clusterIP: None, то есть виртуального адреса нет. Вместо него DNS-сервер кластера отвечает адресами самих подов.
Как формируются имена:
postgres-0 . db . notes . svc . cluster.local
| | | | |
под сервис namespace "это Service" домен кластера
Каждый под получает собственное DNS-имя <под>.<сервис>.<namespace>.svc.cluster.local. Какой сервис отвечает за имена подов, указано в поле serviceName StatefulSet (у нас db). Поэтому serviceName должен совпадать с именем headless Service.
Для одной реплики удобнее короткое имя сервиса: db.notes.svc (из того же namespace достаточно даже просто db). Для headless Service DNS вернёт адрес единственного пода. Разберём пример: nslookup db.notes.svc.cluster.local вернёт что-то вроде 10.244.2.7. Это IP пода postgres-0 из диапазона адресов подов (podCIDR, см. урок 5.1), а не виртуальный адрес сервиса. Сравни: у обычного сервиса notes в этом же кластере адрес 10.96.41.207 из диапазона сервисов.
Осторожно, путаница: Headless Service принимают за сервис без портов или «сломанный». Это нормальный сервис, просто без балансировки и виртуального адреса. Ещё одна путаница: serviceName не создаёт сервис, он только ссылается на существующий. Если сервиса db нет, StatefulSet всё равно запустится, но имён у подов в DNS не будет.
Прикинь сам: зачем StatefulSet нужен headless Service, если можно взять обычный ClusterIP?
Обычный сервис балансирует и скрывает, какой под ответил. Для базы нужен DNS-адрес каждого пода отдельно: писать в primary, читать с replica.
Проверь понимание: зачем StatefulSet нужен headless Service, если можно было бы использовать обычный ClusterIP?
Ответ
Обычный сервис балансирует запросы между подами и скрывает, какой именно ответил. Для базы нужно обращаться к конкретной реплике (записывать в primary, читать с replica), а значит нужен DNS-адрес каждого пода отдельно. Headless Service даёт такие имена.
Главное: headless Service (
clusterIP: None) отдаёт адреса подов, а имя пода<под>.<сервис>.<namespace>.svc.cluster.local;serviceNameтолько ссылается на сервис.
База запущена, и ей нужен пароль: с ним связана самая частая ловушка.
Пароль базы: Secret и «только при первом запуске»
Образ PostgreSQL настраивается переменными окружения (environment variable, пара «имя=значение», которую Kubernetes подставляет в контейнер при запуске): POSTGRES_DB (имя базы), POSTGRES_USER (пользователь), POSTGRES_PASSWORD (пароль). Ты уже видел их в уроке 4.5.
Пароль в манифест открытым текстом класть нельзя: манифест лежит в git. В Kubernetes для паролей есть объект Secret: он хранит пары «ключ: значение» отдельно от манифеста пода, а под ссылается на него полем secretKeyRef («значение возьми из ключа такого-то в таком-то Secret»). Подробно про Secret в уроке 5.6. Сейчас достаточно знать: мы создаём его командой, а не из файла, чтобы пароль не попал в репозиторий.
Теперь главная ловушка. Образ читает POSTGRES_PASSWORD только при первой инициализации пустого каталога данных. При старте скрипт образа смотрит: каталог пустой? Тогда создаёт кластер баз (программой initdb), задаёт пароль из переменной. Каталог уже заполнен? Тогда переменные POSTGRES_* игнорируются, и пароль остаётся тот, что записан внутри базы.
Разобранный пример:
1. Создан Secret с паролем "aaa", запущена база: initdb записал пароль "aaa" внутрь данных.
2. Ты пересоздал Secret с паролем "bbb" и перезапустил под.
3. Под получил переменную POSTGRES_PASSWORD=bbb, но каталог уже не пуст: переменная не читается.
4. Внутри базы пароль по-прежнему "aaa". Клиент с "bbb" получает: password authentication failed.
Это самая частая причина ошибки password authentication failed, её ты поймаешь в «Сломай и почини». Правильная смена пароля: ALTER USER notes PASSWORD '...' внутри базы, затем обновить Secret.
Прикинь сам: ты поменял пароль в Secret
notes-dbи перезапустил под. Поменяется ли пароль пользователяnotesв базе?
Нет. Образ читает POSTGRES_PASSWORD только при первом создании кластера баз в пустом каталоге. Пароль в готовой базе меняют командой ALTER USER.
Осторожно: пароль не кладут в манифест открытым текстом: Secret создают командой, а не файлом из git.
Проверь понимание: ты поменял значение в Secret
notes-dbи перезапустил под. Поменяется ли пароль пользователяnotesв базе?
Ответ
Нет. Переменная нужна только при создании кластера баз (initdb) в пустом каталоге. Пароль в существующей базе меняется командой ALTER USER notes PASSWORD '...', а Secret надо привести в соответствие.
Главное:
POSTGRES_PASSWORDработает один раз, приinitdbв пустом каталоге; смена Secret пароль в базе не меняет.
Вторая ловушка PostgreSQL 18 связана с путём, куда монтировать том.
PostgreSQL 18: куда смонтировать том
Ещё одна ловушка, на этот раз про путь. Образы postgres до 17-й версии хранили данные в /var/lib/postgresql/data, и том монтировали туда. Начиная с 18-й версии образ хранит данные в подкаталоге с номером версии: /var/lib/postgresql/18/docker. Причина: так проще обновлять базу с одной версии на другую, не смешивая данные разных версий. Объявленный образом том (VOLUME) теперь стоит на /var/lib/postgresql, на уровень выше.
Что из этого следует для манифеста:
- том монтируем в
/var/lib/postgresql(именно туда, а не в.../data), тогда каталог версии окажется внутри тома; - переменную
PGDATA(путь к каталогу данных) задавать не нужно.
Что будет при ошибке: если смонтировать том в /var/lib/postgresql/data, база запустится и будет работать. Но реальные данные лягут в /var/lib/postgresql/18/docker, то есть вне тома, в слой контейнера. Всё будет выглядеть нормально ровно до первого пересоздания пода, после которого база окажется пустой. Самая неприятная поломка та, которая не видна сразу.
Так выглядит каталог внутри контейнера (проверено на postgres:18):
/var/lib/postgresql:
18
/var/lib/postgresql/18:
docker
Осторожно, путаница: инструкции из интернета для PostgreSQL 15 и 16 предлагают монтировать .../data и задавать PGDATA. Для 18 они устарели: сверяйся с документацией образа для своей версии.
Прикинь сам: ты смонтировал том в
/var/lib/postgresql/data, база запустилась. Почему после удаления пода данные пропали?
В postgres:18 данные лежат в /var/lib/postgresql/18/docker, вне смонтированного каталога. Они писались в слой контейнера и ушли вместе с подом.
Проверь понимание: ты смонтировал том в
/var/lib/postgresql/data, база запустилась. Почему после удаления пода данные пропали?
Ответ
В образе postgres:18 данные лежат в /var/lib/postgresql/18/docker, а этот путь не входит в смонтированный каталог .../data. Данные писались в слой контейнера, который удаляется вместе с подом.
Главное: для PostgreSQL 18 том монтируют в
/var/lib/postgresql, аPGDATAне задают.
Посмотрим, какие статусы проходит том за свою жизнь.
Жизненный цикл тома: от заявки до удаления
У PVC и PV есть статусы (фазы), которые показывают, на каком шаге они сейчас. Знать их нужно, чтобы читать вывод kubectl get и понимать, что происходит с данными.
| Объект | Статус | Что значит |
|---|---|---|
| PVC | Pending |
заявка есть, тома ещё нет (ждём первый под или поставщик ещё создаёт диск) |
| PVC | Bound |
заявка связана с томом, можно использовать |
| PVC | Terminating |
заявку удалили, но она ещё используется подом: удаление ждёт |
| PV | Bound |
том отдан заявке |
| PV | Released |
заявку удалили, но том остался (при Retain): данные на месте, но новая заявка сама на него не сядет |
| PV | Failed |
автоматически вернуть том не получилось, нужен администратор |
Проследим, что происходит с данными при удалении PVC, для двух значений reclaimPolicy:
flowchart LR
subgraph D["reclaimPolicy: Delete (наш kind)"]
direction TB
D1["1. kubectl delete pvc data-postgres-0"] --> D2["2. PV удаляется автоматически"] --> D3["3. поставщик стирает каталог с данными"] --> D4["4. вернуть данные нельзя"]
end
subgraph R["reclaimPolicy: Retain (частая продовая настройка)"]
direction TB
R1["1. kubectl delete pvc data-postgres-0"] --> R2["2. PV остаётся в статусе Released"] --> R3["3. данные лежат на диске, никто их не трогает"] --> R4["4. администратор может вручную создать новый PVC и подключить том<br>(после очистки ссылки на старый PVC)"]
end
Ещё одна деталь: пока PVC подключён к работающему поду, kubectl delete pvc не удалит его сразу. Kubernetes защищает используемые тома: заявка переходит в Terminating, и удаление завершится только после остановки пода (это механизм финализаторов: отметка на объекте «не удаляй, пока не выполнено условие»). Поэтому в сценарии, где кто-то «почистил лишнее», сначала удаляют под или StatefulSet, а уже потом PVC исчезает.
Осторожно, путаница: «Retain значит, что данные в безопасности». Нет: Retain сохраняет только том. Если кто-то удалил PVC, вернуть его в работу без ручных действий нельзя, а без бэкапа при потере самого диска данные всё равно потеряны. Политика возврата это не бэкап.
Прикинь сам: какой статус будет у PV после
kubectl delete pvcприRetain? А приDelete?
При Retain PV остаётся в статусе Released, данные целы. При Delete PV удаляется вместе с данными.
Проверь понимание: какой статус будет у PV после
kubectl delete pvc, если у классаreclaimPolicy: Retain? А еслиDelete?
Ответ
При Retain том остаётся в статусе Released, данные сохранены. При Delete PV удаляется вместе с данными, и в списке его уже не будет.
Главное: PVC проходит
Pending,Bound,Terminating; PV остаётсяReleasedприRetain; политика возврата не бэкап.
Теперь соберём всё в один сквозной путь от apply до данных на диске.
Что происходит от kubectl apply до данных на диске
Соберём всё в один сквозной путь. Это полезно и для понимания, и для диагностики: когда что-то не работает, нужно знать, на каком шаге цепочка порвалась.
flowchart TD
A["kubectl apply -f 40-postgres.yaml"] --> B["1. API-сервер сохраняет Service db и StatefulSet postgres"]
B --> C["2. Контроллер StatefulSet видит: нужен postgres-0, его нет.<br>Создаёт PVC data-postgres-0 (по volumeClaimTemplates) и под postgres-0"]
C --> D["3. PVC в статусе Pending (WaitForFirstConsumer).<br>Планировщик выбирает узел для пода"]
D --> E["4. Поставщик local-path создаёт каталог на этом узле и объект PV.<br>PVC становится Bound"]
E --> F["5. kubelet запускает контейнер postgres:18, монтирует том в /var/lib/postgresql,<br>передаёт POSTGRES_* (пароль из Secret notes-db)"]
F --> G["6. Скрипт образа видит пустой каталог, запускает initdb,<br>создаёт базу notes и пользователя notes"]
G --> H["7. Под готов, Service db получает его адрес, DNS db.notes.svc отвечает"]
Теперь можно «читать» поломки по шагам. Pending у пода и PVC: цепочка застряла на шаге 3 или 4 (нет класса, нет места, не выбран узел). CreateContainerConfigError: шаг 5, kubelet не нашёл Secret или ключ. password authentication failed при живом поде: шаги 1-7 прошли, но пароль в Secret и в базе разошёлся из-за шага 6, который выполняется один раз.
Прикинь сам: под и PVC
Pending, в событияхstorageclass "fast-ssd" not found. На каком шаге цепочка порвалась?
На шаге 4: поставщик не смог создать том, потому что указанного класса нет. Чинить нужно storageClassName, а не под.
Осторожно: шаг 6 с initdb выполняется один раз, и повторный запуск его не повторяет.
Проверь понимание: под
postgres-0вPending, у PVC тожеPending, в событияхstorageclass ... "fast-ssd" not found. На каком шаге цепочка порвалась?
Ответ
На шаге 4: поставщик не смог создать том, потому что указанного класса хранилища нет. Значит, чинить нужно storageClassName, а не сам под.
Главное: цепочка из семи шагов подсказывает, где ломается:
Pendingзначит шаги 3-4,CreateContainerConfigErrorшаг 5, пароль разошёлся из-за шага 6.
Что нельзя менять у уже созданного StatefulSet, разберём дальше.
Обновление StatefulSet и что в нём нельзя менять
Когда ты меняешь шаблон пода (template) у StatefulSet, например версию образа, контроллер обновляет поды по очереди, от старшего номера к младшему: сначала postgres-2, потом postgres-1, потом postgres-0. Каждый следующий под обновляется только после того, как предыдущий стал готов. Смысл: в базе с репликами главный сервер обычно postgres-0, и его обновляют последним, после реплик. При этом каждый под сохраняет своё имя и свой PVC.
Есть одно жёсткое ограничение. Поле volumeClaimTemplates у существующего StatefulSet менять нельзя: API ответит ошибкой Forbidden: updates to statefulset spec for fields other than 'replicas', 'ordinals', 'template', 'updateStrategy' ... are forbidden. Причина: PVC уже созданы по старому шаблону, и контроллер не станет менять их задним числом. Если нужно другое значение (например другой размер или класс), есть три пути: удалить StatefulSet и PVC и начать заново (только когда данных нет), удалить StatefulSet без удаления подов (kubectl delete statefulset postgres --cascade=orphan) и создать заново с новым шаблоном, либо править сами PVC.
Расширение диска. Если у класса стоит allowVolumeExpansion: true, увеличить том можно на лету: правишь у PVC spec.resources.requests.storage (например 1Gi на 2Gi). У standard в kind расширение выключено (ALLOWVOLUMEEXPANSION false, помнишь колонку в выводе выше), поэтому в облаках заранее выбирают класс с расширением. Уменьшить том нельзя нигде.
Осторожно, путаница: «Поменяю размер в volumeClaimTemplates, и все PVC вырастут». Не вырастут: шаблон применяется только к новым PVC, а поменять его нельзя. Размер существующих PVC правят на самих PVC.
Прикинь сам: у StatefulSet три реплики, ты меняешь образ. В каком порядке обновятся поды и почему это важно?
Сначала postgres-2, затем postgres-1, затем postgres-0: главный сервер обычно postgres-0, и его обновляют после реплик.
Проверь понимание: у StatefulSet три реплики, ты меняешь образ. В каком порядке обновятся поды и почему это важно для базы?
Ответ
Сначала postgres-2, затем postgres-1, затем postgres-0. Поды с меньшими номерами часто главные, и их обновляют после реплик, чтобы главный сервер менялся последним и по пути можно было остановиться, если обновление ломает базу.
Главное:
volumeClaimTemplatesменять нельзя, размер существующих PVC правят на самих PVC, а уменьшить том нельзя нигде.
Соберём в таблицу, что происходит с данными при типичных сбоях.
Что случится при сбоях: сводка
Закрепим всё таблицей: что делает Kubernetes и что происходит с данными в типичных ситуациях. Предполагаем один под и локальный диск (как в kind).
| Ситуация | Что делает Kubernetes | Данные |
|---|---|---|
Упал процесс postgres в контейнере |
kubelet перезапускает контейнер в том же поде | целы, том тот же |
| Удалили под | StatefulSet создаёт postgres-0 заново, подключает тот же PVC |
целы |
| Обновили образ | под пересоздаётся с тем же PVC | целы (при совместимых версиях) |
| Удалили StatefulSet | поды удаляются, PVC остаются | целы |
| Удалили PVC | том удаляется (при Delete) |
потеряны |
| Удалили namespace | удаляется всё внутри, включая PVC | потеряны |
| Умер узел с локальным диском | под не может стартовать на другом узле, том остался на мёртвом | недоступны, пока узел не вернётся |
| Умер узел, диск сетевой | том отвязывается и подключается на другом узле | целы, короткий простой |
Обрати внимание на строку про обновление образа: между большими версиями PostgreSQL (например 17 на 18) формат данных меняется, и просто заменить образ нельзя. Нужна миграция (pg_upgrade или дамп и восстановление). Это ещё одна причина, по которой операторы БД полезны.
Прикинь сам: кто-то выполнил
kubectl delete namespace notes. Что осталось от базы?
Ничего: namespace удаляет всё внутри, включая PVC, а при Delete и PV. Вернуть данные можно только из бэкапа.
Осторожно: обновление между большими версиями PostgreSQL заменой образа не работает: формат данных меняется, нужна миграция.
Проверь понимание: кто-то выполнил
kubectl delete namespace notes. Что осталось от базы?
Ответ
Ничего. Namespace удаляет все объекты внутри, включая StatefulSet, Service, Secret и PVC. Вместе с PVC удаляется и PV (при reclaimPolicy: Delete), а с ним и данные. Восстановиться можно только из бэкапа.
Главное: данные переживают удаление пода, StatefulSet и обновление образа, но не удаление PVC или namespace.
Остаётся решить, стоит ли вообще держать базу в кластере.
Когда база в Kubernetes оправдана
Последний вопрос теории: стоит ли вообще держать базу в кластере. Ответ зависит от цены ошибки.
- StatefulSet с одним подом (как у нас) подходит для учебы, dev и тестовых стендов: данные не жалко, поднимается за минуту.
- Оператор БД (CloudNativePG, Zalando и другие) это программа, которая умеет то, чего не умеет StatefulSet: настраивать реплики, переключать primary при сбое, делать бэкапы по расписанию и восстанавливать на момент времени. Подходит для продовой базы, если команда готова с ним работать.
- Managed-сервис (облачная база, например Managed PostgreSQL в Yandex Cloud) снимает всю эксплуатацию с команды: бэкапы, обновления, репликацию делает провайдер.
Бэкап нужен при любом варианте. Простейший способ: логический бэкап, то есть дамп в виде SQL-команд, из которых базу можно воссоздать (программа pg_dump). Ты сделаешь его в практике. Правило: бэкап, лежащий на том же диске и в том же кластере, что и база, не спасёт при потере кластера. Копию нужно хранить снаружи. Регулярные бэкапы по расписанию разберём в уроке 5.8.
Прикинь сам: ты скопировал каталог тома работающей базы как «бэкап». Чем это хуже
pg_dump?
Копия при работающей базе может оказаться несогласованной и привязана к версии и узлу. pg_dump делает согласованный снимок через саму базу.
Осторожно: бэкап на том же диске и в том же кластере не спасёт при потере кластера.
Проверь понимание: ты нашёл на диске узла каталог тома базы и скопировал его как «бэкап». Чем это хуже
pg_dump?
Ответ
Копия каталога, снятая при работающей базе, может оказаться несогласованной: в момент копирования часть файлов уже изменилась, часть ещё нет. Также такая копия привязана к той же версии PostgreSQL и к тому же узлу. pg_dump делает согласованный снимок через саму базу и даёт файл, который можно восстановить на любой версии не ниже.
Главное: один под StatefulSet подходит для учёбы и dev, для прода берут оператор БД или managed-сервис; бэкап нужен всегда и хранится снаружи.
Теперь пора развернуть базу руками.
Практика
Кластер kind notes из урока 5.1 должен быть запущен, namespace notes создан. Проверь:
kubectl config current-context
kubectl -n notes get deploy,svc
kind-notes
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/notes 3/3 3 3 2d
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/notes ClusterIP 10.96.41.207 <none> 8080/TCP 2d
Как читать вывод: kind-notes это имя текущего контекста (какой кластер сейчас управляется). Колонка READY 3/3 у Deployment значит «готово 3 пода из 3 нужных», AGE это возраст объекта. У Service notes есть CLUSTER-IP: обычный виртуальный адрес, в отличие от того, что мы сделаем для базы.
Работать будем из ~/notes (каталог k8s/base/ уже есть).
Задание 1. Secret и StatefulSet с PostgreSQL
Цель: запустить PostgreSQL 18 в кластере с постоянным томом и паролем из Secret.
Сначала создай Secret. Разберём команду по частям:
kubectl -n notesзначит «в namespacenotes»;create secret generic notes-dbсоздаёт Secret типаgeneric(произвольные пары «ключ: значение») с именемnotes-db;--from-literal=POSTGRES_PASSWORD="..."задаёт одну пару прямо в команде: ключPOSTGRES_PASSWORDи значение;$(openssl rand -hex 24)это подстановка: shell сначала выполняет команду внутри скобок, а её вывод вставляет в строку.openssl rand -hex 24печатает 24 случайных байта в шестнадцатеричном виде: строка из 48 символов0-9a-f.
Пароль генерируется и нигде не сохраняется в файлах:
kubectl -n notes create secret generic notes-db \
--from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 24)"
kubectl -n notes get secret notes-db
secret/notes-db created
NAME TYPE DATA AGE
notes-db Opaque 1 1s
Используем -hex, а не -base64: в шестнадцатеричной строке нет символов /, +, =, которые в уроке 5.6 пришлось бы экранировать в DATABASE_URL.
Как читать вывод: TYPE Opaque значит «произвольные данные» (обычный Secret), DATA 1 это число ключей внутри. Значения get не показывает.
Теперь манифест. Каждая ключевая строка объяснена комментарием, а после блока идёт разбор.
Создай файл k8s/base/40-postgres.yaml:
# Headless Service: даёт стабильные DNS-имена подам StatefulSet
apiVersion: v1
kind: Service
metadata:
name: db
namespace: notes
labels:
app.kubernetes.io/name: postgres
spec:
clusterIP: None
selector:
app.kubernetes.io/name: postgres
ports:
- name: postgres
port: 5432
targetPort: 5432
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: notes
labels:
app.kubernetes.io/name: postgres
spec:
serviceName: db
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: postgres
template:
metadata:
labels:
app.kubernetes.io/name: postgres
spec:
containers:
- name: postgres
image: postgres:18
ports:
- name: postgres
containerPort: 5432
env:
- name: POSTGRES_DB
value: notes
- name: POSTGRES_USER
value: notes
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: notes-db
key: POSTGRES_PASSWORD
volumeMounts:
# В postgres:18 данные лежат в /var/lib/postgresql/18/docker, PGDATA не нужен
- name: data
mountPath: /var/lib/postgresql
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 1Gi
Разбор манифеста:
- Первый документ (до
---) это headless Servicedb.clusterIP: Noneделает его headless.selectorговорит, каким подам принадлежат адреса: с меткойapp.kubernetes.io/name: postgres.portиtargetPortоба 5432: клиенты стучатся в этот порт, он же открыт в контейнере. - Второй документ это StatefulSet.
serviceName: dbсвязывает его с сервисом выше (мы разбирали это в теории).replicas: 1одна база.selector.matchLabelsиtemplate.metadata.labelsдолжны совпадать: так StatefulSet находит свои поды. - В
envтри переменных.POSTGRES_DBиPOSTGRES_USERзаданы прямо (это не секреты).POSTGRES_PASSWORDберётся черезvalueFrom.secretKeyRefиз Secretnotes-db, ключPOSTGRES_PASSWORD. volumeMountsмонтирует том с именемdataв/var/lib/postgresql(не в.../data, помним про версию 18).volumeClaimTemplatesописывает PVC: имя шаблонаdata(совпадает с именем вvolumeMounts), режимReadWriteOnce, размер1Gi(гибибайт, 1024 в третьей степени байт).storageClassNameне указан, значит, берётся класс по умолчаниюstandard.
Предскажи: как будет называться PVC, который создаст StatefulSet, и в каком статусе он окажется сразу после apply, пока под не запущен?
Ответ
PVC data-postgres-0 (шаблон data + имя пода), сначала Pending (режим WaitForFirstConsumer), после запуска пода Bound.
Шаги:
- Проверь и примени.
--dry-run=serverотправляет манифест на проверку в кластер, но ничего не создаёт.kubectl wait --for=condition=Ready pod/postgres-0 --timeout=180sждёт до трёх минут, пока под не станет готов.-l app.kubernetes.io/name=postgresпоказывает только объекты с этой меткой:
kubectl apply -f k8s/base/40-postgres.yaml --dry-run=server
kubectl apply -f k8s/base/40-postgres.yaml
kubectl -n notes wait --for=condition=Ready pod/postgres-0 --timeout=180s
kubectl -n notes get statefulset,pod,pvc,svc -l app.kubernetes.io/name=postgres
kubectl -n notes get pvc
- Убедись, что база отвечает.
kubectl exec postgres-0 -- командазапускает команду внутри контейнера пода (два дефиса отделяют команду от флаговkubectl),pg_isreadyспрашивает у сервера, готов ли он принимать соединения,logs ... | tail -3берёт три последних строки журнала:
kubectl -n notes exec postgres-0 -- pg_isready -U notes -d notes
kubectl -n notes logs postgres-0 | tail -3
Что должно получиться (после --dry-run=server вывод service/db created (server dry run) и statefulset.apps/postgres created (server dry run), затем service/db created и statefulset.apps/postgres created; возраст и имя тома у тебя будут другими):
NAME READY AGE
statefulset.apps/postgres 1/1 40s
NAME READY STATUS RESTARTS AGE
pod/postgres-0 1/1 Running 0 40s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/db ClusterIP None <none> 5432/TCP 40s
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
data-postgres-0 Bound pvc-8b1a0e0d-42a3-4a7c-b6b1-5a0c3c9e2f41 1Gi RWO standard <unset> 40s
/var/run/postgresql:5432 - accepting connections
...
LOG: database system is ready to accept connections
Как читать вывод: READY 1/1 у StatefulSet и пода значит, что готова единственная реплика (реплика - одна копия пода). CLUSTER-IP None подтверждает, что сервис headless. У PVC STATUS Bound значит «заявка связана с томом», VOLUME это имя созданного PV, RWO это ReadWriteOnce, STORAGECLASS standard это класс, по которому создан том, а VOLUMEATTRIBUTESCLASS <unset> значит, что дополнительный класс атрибутов не задан (колонка есть у свежих kubectl, у старых её нет). Фраза accepting connections от pg_isready и строка database system is ready to accept connections в логе значат, что база запущена.
Объясни себе:
- Чем
CLUSTER-IP: Noneу сервисаdbотличается от адреса у сервисаnotes? - Откуда взялся PVC, если мы его нигде не описывали отдельным объектом?
- Почему пароль не лежит в YAML и не попадёт в git?
Типичные ошибки:
Error: secret "notes-db" not found(статус подаCreateContainerConfigError): Secret не создан в namespacenotes; создай командой выше и подожди, под перезапустится сам.Error: Database is uninitialized and superuser password is not specified: переменнаяPOSTGRES_PASSWORDпуста; проверь ключ в Secret командойkubectl -n notes get secret notes-db -o jsonpath='{.data}'.initdb: error: directory "/var/lib/postgresql/data" exists but is not empty: тома с чужими данными; смонтируй том в/var/lib/postgresql, как в манифесте.
Задание 2. Подключиться к базе по DNS
Цель: проверить, что имена db.notes.svc и postgres-0.db резолвятся, и создать таблицу notes по контракту курса.
Предскажи: какой IP вернёт DNS для имени db.notes.svc: виртуальный ClusterIP или адрес пода?
Ответ
Адрес пода из диапазона podCIDR, например 10.244.2.7. У headless Service виртуального адреса нет, DNS отдаёт адреса endpoints.
Шаги:
- Резолвинг (превращение имени в адрес) из временного пода. Разбор:
kubectl run dnscheckсоздаёт под с именемdnscheck;--rmудалит его после завершения;-itподключает терминал;--restart=Neverзначит «запусти один раз, не перезапускай»;--image=busybox:1.37это минимальный образ с набором простых утилит; после--идёт командаnslookup <имя>, которая спрашивает DNS. Вторая команда показывает под с колонкойIP:
kubectl -n notes run dnscheck --rm -it --restart=Never --image=busybox:1.37 -- \
nslookup db.notes.svc.cluster.local
kubectl -n notes get pod postgres-0 -o wide
- Создай таблицу и запиши строку через
psqlвнутри пода.exec -iпередаёт в команду то, что ты пишешь дальше на вход (-iбез-t, потому что терминал здесь не нужен),<<'SQL' ... SQLэто блок текста, который shell отдаёт команде как ввод (кавычки вокругSQLотключают подстановки),-U notes -d notesэто пользователь и база.CREATE TABLE IF NOT EXISTSсоздаёт таблицу, только если её нет,serialэто автоматически растущий номер,INSERTдобавляет строку,SELECTчитает. Парольpsqlне спрашивает, потому что подключается по локальному сокету внутри контейнера (это доверенное соединение):
kubectl -n notes exec -i postgres-0 -- psql -U notes -d notes <<'SQL'
CREATE TABLE IF NOT EXISTS notes (
id serial PRIMARY KEY,
text text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO notes (text) VALUES ('первая заметка в кластере');
SELECT id, text FROM notes;
SQL
Что должно получиться:
Name: db.notes.svc.cluster.local
Address: 10.244.2.7
NAME READY STATUS RESTARTS AGE IP NODE
postgres-0 1/1 Running 0 3m 10.244.2.7 notes-worker2
...
CREATE TABLE
INSERT 0 1
id | text
----+---------------------------
1 | первая заметка в кластере
(1 row)
Как читать вывод: в nslookup строка Address это ответ DNS, его нужно сравнить с колонкой IP в get pod -o wide (-o wide добавляет колонки IP и узел). CREATE TABLE и INSERT 0 1 (ноль это служебное поле, один число вставленных строк) сообщают об успехе, таблица в конце это результат SELECT. Строка (1 row) подтверждает, что найдена одна строка.
Объясни себе:
- Совпал ли адрес из DNS с IP пода
postgres-0? Почему? - Почему
psqlвнутри пода не спросил пароль? - Что произойдёт с таблицей при удалении пода?
Типичные ошибки:
psql: error: connection to server on socket "/var/run/postgresql/.s.PGSQL.5432" failed: No such file or directory: сервер ещё стартует или упал; смотриkubectl -n notes logs postgres-0.psql: error: connection to server ... failed: FATAL: role "postgres" does not exist: забыт ключ-U notes, суперпользователь в нашей базе называетсяnotes.nslookup: can't resolve 'db.notes.svc.cluster.local': неверный namespace или имя сервиса, проверьkubectl -n notes get svc db.
Задание 3. Удалить под, потом попробовать удалить всё
Цель: отличить, что данные защищены (под, StatefulSet), а что нет (PVC, namespace).
Предскажи: после delete pod, после delete statefulset и после delete pvc в каких случаях строка про «первую заметку» останется?
Ответ
После delete pod останется (тот же PVC). После delete statefulset тоже (PVC остаётся, при новом apply подхватится). После delete pvc пропадёт: политика класса Delete удаляет PV и каталог.
Шаги:
- Удали под и проверь данные.
kubectl delete podудаляет под,waitждёт, пока StatefulSet создаст новый и он станет готов,psql -c '...'выполняет одну SQL-команду:
kubectl -n notes delete pod postgres-0
kubectl -n notes wait --for=condition=Ready pod/postgres-0 --timeout=180s
kubectl -n notes exec postgres-0 -- psql -U notes -d notes -c 'SELECT id, text FROM notes;'
- Удали StatefulSet, посмотри на PVC, верни обратно:
kubectl -n notes delete statefulset postgres
kubectl -n notes get pvc
kubectl apply -f k8s/base/40-postgres.yaml
kubectl -n notes wait --for=condition=Ready pod/postgres-0 --timeout=180s
kubectl -n notes exec postgres-0 -- psql -U notes -d notes -c 'SELECT count(*) FROM notes;'
- Сделай логический бэкап на свой компьютер.
pg_dump -U notes -d notesпечатает SQL-команды, которыми можно воссоздать базу,> notes-backup.sql(перенаправление) записывает этот вывод в файл на твоей машине,wc -lсчитает строки файла,grep -cсчитает строки с заданным текстом. Бэкапы по расписанию будут в уроке 5.8:
kubectl -n notes exec postgres-0 -- pg_dump -U notes -d notes > notes-backup.sql
wc -l notes-backup.sql
grep -c 'первая заметка' notes-backup.sql
rm notes-backup.sql
Что должно получиться:
pod "postgres-0" deleted
...
id | text
----+---------------------------
1 | первая заметка в кластере
(1 row)
statefulset.apps "postgres" deleted
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS VOLUMEATTRIBUTESCLASS AGE
data-postgres-0 Bound pvc-8b1a0e0d-42a3-4a7c-b6b1-5a0c3c9e2f41 1Gi RWO standard <unset> 9m
...
count
-------
1
(1 row)
97 notes-backup.sql
1
Как читать вывод: колонки те же, что в задании 1 (в том числе VOLUMEATTRIBUTESCLASS со значением <unset>). После удаления StatefulSet PVC остаётся в статусе Bound, и его AGE больше возраста нового пода: это тот же диск. count 1 значит «строка на месте». Число строк в дампе у тебя может немного отличаться (97 в проверенном прогоне), важно, что grep -c вернул 1: заметка есть в дампе.
Объясни себе:
- Почему PVC пережил удаление StatefulSet и это осознанное решение разработчиков?
- Чем
pg_dumpв файл отличается от копии каталога тома и почему для бэкапа он надёжнее? - Что теряется, если бэкап лежит в том же кластере и на том же узле, что и база?
Типичные ошибки:
Error from server (NotFound): pods "postgres-0" not found:waitвызван до того, как контроллер создал под; повтори через пару секунд.pg_dump: error: connection to server on socket ... failed: FATAL: role "postgres" does not exist: не указан-U notes.- В
notes-backup.sqlпервая строкаerror: ...: бэкап пишут в stdout, поэтому не добавляй-it(tty портит вывод).
Задание 4. Шаг проекта: Postgres в k8s/base/
Цель: зафиксировать состояние проекта: манифест в git, Secret вне git, адрес db.notes.svc:5432 проверен снаружи пода notes.
Предскажи: сможет ли под notes из Deployment достучаться до db.notes.svc:5432, если приложение пока не настроено на Postgres?
Ответ
Сеть да (Service работает), но приложение всё ещё в режиме STORE=file и базу не использует. Переключение делаем в уроке 5.6 через ConfigMap.
Шаги:
- Проверь манифест и то, что в репозитории нет пароля.
grep -rn -A1 'name: POSTGRES_PASSWORD' k8s/ищет строку в файлах каталога рекурсивно (-r), печатает номера строк (-n) и ещё одну строку после совпадения (-A1, After 1). Нам важно, что идёт после имени переменной:valueFrom(ссылка на Secret) илиvalue(пароль открытым текстом).git status --short k8s/показывает состояние файлов в коротком виде:
cd ~/notes
kubectl apply -f k8s/base/40-postgres.yaml --dry-run=server
grep -rn -A1 'name: POSTGRES_PASSWORD' k8s/
git status --short k8s/
- Проверь доступность порта из пода приложения (у образа
notesестьpython). Разбор:jsonpath='{.items[0].metadata.name}'достаёт из ответа имя первого пода,$(...)подставляет его в переменнуюPOD.python -c "..."запускает одну строку кода:socket.create_connection((хост, порт), 3)пытается открыть TCP-соединение за 3 секунды и падает с ошибкой, если не вышло:
POD=$(kubectl -n notes get pod -l app.kubernetes.io/name=notes -o jsonpath='{.items[0].metadata.name}')
kubectl -n notes exec "$POD" -- python -c \
"import socket; s=socket.create_connection(('db.notes.svc', 5432), 3); print('порт 5432 открыт')"
- Зафиксируй:
git add k8s/base/40-postgres.yaml
git commit -m "k8s: PostgreSQL StatefulSet, headless Service db и PVC 1Gi"
Что должно получиться:
service/db unchanged (server dry run)
statefulset.apps/postgres unchanged (server dry run)
k8s/base/40-postgres.yaml:47: - name: POSTGRES_PASSWORD
k8s/base/40-postgres.yaml-48- valueFrom:
?? k8s/base/40-postgres.yaml
Команда grep печатает две строки (номера строк у тебя совпадут, если файл набран как в задании 1): после name: POSTGRES_PASSWORD идёт valueFrom:, а не value:, значит, пароля в файле нет. git status показывает ?? k8s/base/40-postgres.yaml, а проверка порта печатает:
порт 5432 открыт
Состояние проекта после урока: StatefulSet postgres, PVC 1 ГиБ, Service db, Secret notes-db создан командой. Приложение по-прежнему в режиме STORE=file. Манифест лежит в твоём репозитории: ~/notes/k8s/base/40-postgres.yaml.
Как читать вывод: unchanged (server dry run) значит, что манифест корректен и совпадает с тем, что уже в кластере. ?? k8s/base/40-postgres.yaml в git status значит, что файл новый и ещё не добавлен в git. Строка порт 5432 открыт появляется только при успешном соединении.
Объясни себе:
- Почему Secret не в git и что из этого следует для нового кластера (долг: закроется в уроке 9.2)?
- Какой адрес будет в
DATABASE_URLв следующем уроке? - Что нужно сделать перед
kind delete cluster, если данные из базы нужны?
Типичные ошибки:
error: unable to upgrade connection: container not found ("notes"): под только что пересоздан, выбери другой командойkubectl -n notes get pods.socket.gaierror: [Errno -2] Name or service not known: сервисdbне создан или в другом namespace.fatal: not a git repository: команда запущена не из~/notes.
Нейросеть уверенно советует «удалить PVC и создать заново». Для базы это потеря данных, поэтому сначала прочитай события пода и PVC и проверь, не
Pendingли это из-заWaitForFirstConsumer.
Сломай и почини
В этом разделе ты тренируешь главный навык дежурного: по симптому найти причину. Скрипт сам ломает базу в кластере, не заглядывай в него: диагностика и есть упражнение. Скрипт работает только в namespace notes и стирает данные учебной базы в сценариях 1 и 2, поэтому запускай его только на своём kind-кластере. Если нужны данные, сделай pg_dump из задания 3.
Скачай скрипт. У curl флаг -f означает «при ошибке сервера не сохраняй страницу с ошибкой», -L разрешает переходить по перенаправлениям, -o задаёт имя файла:
curl -fsSL -o /tmp/break-5.5.sh https://raw.githubusercontent.com/distinguished-sre/learning/main/devops/project/notes/break/5.5/break.sh
bash /tmp/break-5.5.sh 1
Сценарии: 1, 2 и 3. Запуск без sudo. Проходи по одному: запусти, найди причину, почини сам или командой bash /tmp/break-5.5.sh fix, потом бери следующий. Пока сценарий не исправлен, следующий скрипт не запустит. В конце выполни fix и удали скрипт: rm /tmp/break-5.5.sh.
Симптом
- Сценарий 1. Под
postgres-0не запускается,kubectl get podпоказываетPendingуже пять минут. - Сценарий 2. Под
postgres-0живой и здоровый, ноSELECTиз таблицыnotesотвечает, что такой таблицы нет: данные пропали. Кто-то «почистил лишнее». - Сценарий 3. Под
postgres-0в статусеRunning, но подключение к базе по сети с паролем из Secret заканчивается ошибкойpassword authentication failed.
Гипотезы
Прежде чем проверять, выпиши хотя бы по две возможные причины для каждого симптома. Для этих симптомов подходят:
- Для
Pending: у PVC нет подходящего StorageClass; на узле нет места; PVC привязан к другому узлу. - Для пропавших данных: пересоздали PVC; пересоздали namespace; под смотрит на том другого пода.
- Для ошибки пароля: пароль в Secret не совпадает с паролем внутри базы; неверный ключ в Secret; старый том с другим паролем.
Проверки
Разбор команд: describe показывает подробности объекта, sed -n '/Events/,$p' печатает текст от строки со словом Events до конца (события внизу это то, что чаще всего объясняет причину), get sc,pv показывает классы хранилища и тома, logs --tail=20 последние 20 строк журнала, base64 -d расшифровывает base64 (Secret хранит значения в этом виде), wc -c считает символы.
kubectl -n notes describe pod postgres-0 | sed -n '/Events/,$p'
kubectl -n notes describe pvc data-postgres-0 | sed -n '/Events/,$p'
kubectl get sc,pv
kubectl -n notes logs postgres-0 --tail=20
kubectl -n notes get pvc -o wide
kubectl -n notes get secret notes-db -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d | wc -c
Чтобы проверить пароль по сети, запусти временный под с клиентом. --env передаёт переменную, PGPASSWORD psql читает как пароль, -h db это адрес сервера по DNS-имени сервиса. Пароль подставляется из Secret командой в $(...):
kubectl -n notes run pgcheck --rm -it --restart=Never --image=postgres:18 \
--env PGPASSWORD="$(kubectl -n notes get secret notes-db -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d)" \
-- psql -h db -U notes -d notes -c 'select 1'
Как читать вывод: в Events ищи строки Warning: они называют причину. Число из wc -c для нашего пароля должно быть 48; если оно другое, Secret пересоздавали. Ошибка password authentication failed при ответе сервера значит, что до базы ты дошёл (сеть и DNS в порядке), а не совпал именно пароль.
Исправление
Разбор сценариев
Сценарий 1: PVC Pending. В describe pvc событие вида:
Warning ProvisioningFailed persistentvolumeclaim/data-postgres-0 storageclass.storage.k8s.io "fast-ssd" not found
Причина: в volumeClaimTemplates указан несуществующий storageClassName. Важно: это поле у StatefulSet менять нельзя, API ответит Forbidden: updates to statefulset spec for fields other than ... are forbidden. Правильный путь: удалить StatefulSet (данных ещё нет), удалить зависший PVC, поправить манифест и применить заново:
kubectl -n notes delete statefulset postgres
kubectl -n notes delete pvc data-postgres-0
kubectl apply -f ~/notes/k8s/base/40-postgres.yaml
Сценарий 2: потеря данных. Таблицы нет, потому что PVC удалили, и StatefulSet создал новый пустой том с новой, пустой базой. Признак: у PVC свежий AGE, а у Deployment и Service старый. Вернуть данные из тома нельзя: класс standard имеет reclaimPolicy: Delete. Остаётся бэкап: psql < notes-backup.sql. Профилактика: бэкапы вне кластера (урок 5.8), для продовых классов Retain, права на delete pvc только у админов, защита namespace. Если есть дамп из задания 3:
kubectl -n notes exec -i postgres-0 -- psql -U notes -d notes < notes-backup.sql
Сценарий 3: password authentication failed. В ответе клиента:
psql: error: connection to server at "db" (10.244.2.7), port 5432 failed: FATAL: password authentication failed for user "notes"
Причина: Secret notes-db изменили, а том хранит базу, инициализированную со старым паролем. Пароль читается только при первом initdb. Лечение без потери данных: сменить пароль в базе изнутри пода, где локальное подключение по сокету доверенное:
NEWPASS=$(kubectl -n notes get secret notes-db -o jsonpath='{.data.POSTGRES_PASSWORD}' | base64 -d)
kubectl -n notes exec -i postgres-0 -- psql -U notes -d notes \
-c "ALTER USER notes PASSWORD '$NEWPASS';"
Если данных нет и не жалко, можно удалить StatefulSet и PVC: база проинициализируется заново с текущим паролем. В проде так делать нельзя. Заметь: внутри пода psql ошибки не показал бы, поэтому поломка видна только клиенту, который ходит по сети, как приложение.
ИИ в помощь
Нейросеть хорошо объясняет статусы томов и события, но не видит твои диски, а инструкции из интернета по PostgreSQL часто написаны для версии 15 или 16. Общие правила: ИИ-помощник.
Задача: разобрать, почему под или PVC в Pending.
Я учу Kubernetes. Под postgres-0 и PVC data-postgres-0 в статусе <вставь STATUS>.
Вот вывод kubectl describe pvc и блок Events пода: <вставь вывод>.
Скажи, на каком шаге цепочки от apply до данных на диске всё остановилось, и дай три проверки
от самой дешёвой.
Проверь ответ: сверь с цепочкой из семи шагов в теории и с событиями. Типичная ошибка: нейросеть советует «удалить PVC и создать заново», а так теряются данные.
Задача: разобраться с password authentication failed.
Приложение получает password authentication failed для postgres:18 в Kubernetes.
Я менял Secret <вставь, что менял> и перезапускал под. Объясни, почему так бывает,
и как сменить пароль, не теряя данные.
Проверь ответ: убедись, что ответ про ALTER USER, а не про удаление тома: пароль читается только при первом initdb. Типичная ошибка: совет «удалить StatefulSet и PVC», который стирает данные.
Задача: составить манифест StatefulSet для PostgreSQL 18.
Напиши манифест StatefulSet и headless Service для PostgreSQL 18 в namespace notes: имя postgres,
1 реплика, PVC 1Gi через volumeClaimTemplates, пароль из Secret notes-db. Объясни, куда монтировать том.
Проверь ответ: проверь, что том смонтирован в /var/lib/postgresql, а PGDATA не задан, и что serviceName совпадает с именем сервиса. Типичная ошибка: путь /var/lib/postgresql/data из инструкций для старых версий.
Словарик урока
| Термин | Простыми словами |
|---|---|
| Том (volume) | Каталог, который живёт отдельно от контейнера и подключается в него по нужному пути |
| Монтирование (mount) | Подключение тома к каталогу внутри контейнера |
emptyDir |
Временный каталог, который создаётся и удаляется вместе с подом |
| PV (PersistentVolume) | Конкретный кусок хранилища в кластере, без namespace |
| PVC (PersistentVolumeClaim) | Заявка на том: размер и режим доступа, живёт в namespace |
| StorageClass | Рецепт, как создавать тома по заявкам: поставщик, политика удаления, режим привязки |
| Provisioner | Программа, которая по заявке создаёт том (в kind rancher.io/local-path) |
reclaimPolicy |
Что делать с томом после удаления заявки: Delete (удалить с данными) или Retain (оставить) |
WaitForFirstConsumer |
Создавать том только когда появится под, который его использует |
| accessModes, RWO | Режимы доступа: ReadWriteOnce одним узлом на запись, ReadWriteMany несколькими |
| CSI | Единый интерфейс, через который кластер подключает хранилища разных производителей |
| StatefulSet | Контроллер подов со стабильными именами, своим диском у каждого и порядком запуска |
volumeClaimTemplates |
Шаблон, по которому StatefulSet создаёт отдельный PVC каждому поду |
VolumeAttributesClass (VOLUMEATTRIBUTESCLASS) |
Необязательный класс атрибутов тома (например, скорость диска); у нас не задан, в выводе <unset> |
| Headless Service | Service с clusterIP: None: без виртуального адреса, DNS отдаёт адреса подов |
| Ordinal | Порядковый номер пода в StatefulSet: postgres-0, postgres-1 |
| Secret | Объект для паролей и токенов, значения хранятся в base64 (подробно в уроке 5.6) |
initdb |
Создание нового кластера баз PostgreSQL в пустом каталоге, только тогда читается POSTGRES_PASSWORD |
PGDATA |
Путь к каталогу данных PostgreSQL; в образе 18 задавать не нужно |
pg_dump |
Программа, которая печатает SQL-команды для воссоздания базы (логический бэкап) |
| Crash recovery | Восстановление PostgreSQL по журналу после аварийной остановки |
| Оператор БД | Программа в кластере, которая умеет реплики, переключение и бэкапы базы |
Вопросы с собеседований
Раздел для повторения: ответь вслух, потом открой ответ. Короткие вопросы с пометкой [на скорость] тренируй на время: ответ за 30 секунд.
1. [junior] [часто] В чём разница между Deployment и StatefulSet?
Ответ
Deployment создаёт взаимозаменяемые поды со случайными именами и общим шаблоном. StatefulSet даёт подам стабильные имена name-0, name-1, свой PVC на каждую реплику из volumeClaimTemplates, упорядоченный запуск и обновление. Для БД, очередей и всего, где важна идентичность и диск.
Что хотят услышать: стабильное имя и DNS, персональный PVC, порядок, headless Service; что Deployment для stateless.
Красный флаг: «StatefulSet просто для баз данных» без объяснения, что именно он гарантирует.
2. [junior] [часто] Что такое PV, PVC и StorageClass?
Ответ
PV это конкретный том, PVC заявка на него из namespace, StorageClass рецепт динамического создания PV. Разработчик пишет PVC, провайдер создаёт PV, kubelet монтирует том в под.
Что хотят услышать: динамический провайдер, режим привязки, reclaimPolicy, accessModes.
Красный флаг: путает PV и PVC или думает, что PVC это сам диск.
3. [middle] [часто] Удалили StatefulSet с базой. Что случилось с данными?
Ответ
По умолчанию ничего: PVC остаются (persistentVolumeClaimRetentionPolicy: Retain), при повторном apply под получит тот же диск. Данные теряются при delete pvc, при удалении namespace и при reclaimPolicy: Delete, если PVC удалён. Я проверю kubectl get pvc и kubectl get pv, и если нужно, верну бэкап.
Что хотят услышать: различие политик, что namespace удаляет PVC, бэкап как единственная страховка.
Красный флаг: «StatefulSet защищает данные от любого удаления».
4. [middle] Под postgres-0 висит Pending, PVC тоже Pending. Твои действия?
Ответ
Смотрю kubectl describe pvc и события пода. Проверяю: есть ли StorageClass и правильно ли указан storageClassName, есть ли провайдер (под local-path-provisioner), режим WaitForFirstConsumer (тогда PVC ждёт пода), нет ли ограничений по узлам и ресурсам, хватает ли места. Если ошибка в volumeClaimTemplates, StatefulSet придётся пересоздавать, править это поле на лету нельзя.
Что хотят услышать: describe и события, StorageClass, provisioner, topology; неизменяемость шаблона.
Красный флаг: «пересоздам кластер» вместо диагностики.
5. [junior] [на скорость] Зачем StatefulSet нужен headless Service?
Ответ
Чтобы каждая реплика имела собственный DNS-адрес pod-N.svc.ns.svc.cluster.local, а клиент мог обратиться к конкретной реплике (primary), а не к случайной. Обычный ClusterIP скрывает, какой под ответил.
Что хотят услышать: clusterIP: None, serviceName, адрес пода в DNS.
Красный флаг: «headless это Service без портов».
6. [middle] Ты поменял пароль в Secret и перезапустил под Postgres, а приложение получает password authentication failed. Почему?
Ответ
Образ применяет POSTGRES_PASSWORD только при инициализации пустого каталога. В существующей базе пароль остался старым. Я либо верну старое значение в Secret, либо выполню ALTER USER внутри базы, и только потом обновлю приложение.
Что хотят услышать: initdb, данные на PVC, порядок смены пароля, ротация без простоя.
Красный флаг: «перезапущу поды побольше раз» или «удалю PVC», не подумав о данных.
7. [middle] Диск PVC заполнился на 100%, база перешла в аварийный режим. Что делаешь?
Ответ
Сначала смотрю df -h внутри пода и kubectl get pvc, чтобы понять, растут данные или WAL (журнал предзаписи: файлы, в которые PostgreSQL сначала записывает каждое изменение, прежде чем менять сами таблицы). Если StorageClass поддерживает расширение (allowVolumeExpansion: true), увеличиваю spec.resources.requests.storage у PVC, для StatefulSet правлю сам PVC, а не volumeClaimTemplates. Параллельно ищу причину роста: раздутые таблицы, слот репликации (запись на основном сервере: «не удаляй журнал, пока реплика его не прочитала»), которая остановилась и копит журнал, и архив WAL.
Что хотят услышать: расширение тома онлайн, ограничение шаблона, поиск причины, а не только «дай больше места», мониторинг заполнения.
Красный флаг: «удалю файлы в каталоге данных руками».
8. [middle] Как сделать бэкап PostgreSQL в Kubernetes и как убедиться, что он рабочий?
Ответ
Логический: kubectl exec ... pg_dump или CronJob, сохраняющий дамп на другой PVC или в объектное хранилище вне кластера. Физический бэкап (копия файлов базы вместе с архивом WAL, журнала изменений) позволяет восстановиться на любой момент времени (PITR, point-in-time recovery), его обычно делает оператор БД. Рабочим бэкап считается только после проверки восстановления в отдельную базу, поэтому раз в период делаю restore drill и слежу за возрастом последнего дампа.
Что хотят услышать: бэкап вне кластера и вне узла, restore drill (учебное восстановление), RPO (сколько данных допустимо потерять по времени) и RTO (за какое время нужно восстановиться), PITR как отдельная возможность.
Красный флаг: «PVC уже сам является бэкапом» или «снапшот диска и достаточно, не проверяя».
9. [middle] Узел с postgres-0 вышел из строя. Что будет с подом и данными?
Ответ
Под перейдёт в Unknown/Terminating, StatefulSet сознательно не создаёт замену, пока не убедится, что старый под действительно остановлен (гарантия «не более одного» (at-most-one: никогда не работают два пода с одним именем)). Если том локальный, как в kind, данные привязаны к узлу и недоступны до его возврата. Сетевой диск в облаке отвяжется и подключится на другом узле, после чего под стартует там.
Что хотят услышать: at-most-one, разница локального и сетевого тома, ручное вмешательство (--force) с пониманием риска split-brain (два экземпляра базы одновременно считают себя главными и пишут в один том).
Красный флаг: «просто сделаю delete pod --force» без проверки, что узел выключен.
10. [junior] Приложение получило ошибку connection refused к db:5432 сразу после развёртывания. Что проверишь?
Ответ
Готов ли под (kubectl get pod, logs), есть ли у Service endpoints (kubectl get endpoints db), совпадает ли selector с метками пода, порт и namespace в адресе. Postgres при первом старте инициализирует базу и перезапускается, так что несколько секунд недоступен: клиент должен уметь ждать (retry) или ждать по готовности.
Что хотят услышать: порядок проверки от пода к Service и DNS, endpoints, ожидание готовности (пробы в уроке 5.7).
Красный флаг: «попробую поднять ещё реплик» или «перезапущу всё».
11. [junior] [на скорость] Что такое accessModes у PVC: ReadWriteOnce, ReadWriteMany, ReadWriteOncePod?
Ответ
Это режимы, в которых том можно примонтировать. ReadWriteOnce (RWO): чтение и запись с одного узла, но на этом узле может быть несколько подов. ReadWriteOncePod (RWOP): только один под во всём кластере. ReadWriteMany (RWX): запись сразу с многих узлов, для этого нужно хранилище вроде NFS или распределённой ФС. Для PostgreSQL на блочном диске мне хватает RWO. Режим смотрю в kubectl get pvc, колонка ACCESS MODES.
Что хотят услышать: RWO это про узел, а не про под, RWOP про один под, RWX требует подходящего хранилища, блочные диски обычно RWO.
Красный флаг: «RWO значит один под» и «RWX есть у любого диска».
12. [middle] [на скорость] Что такое reclaimPolicy у PV: Delete и Retain? Что будет с данными, когда удалят PVC?
Ответ
reclaimPolicy определяет судьбу PV после удаления PVC. При Delete вместе с PV удаляется и реальный диск в хранилище, при Retain PV переходит в Released, а данные остаются, и админ разбирается с ними вручную. Политику PV, созданных динамически, задаёт StorageClass (по умолчанию обычно Delete). Для важной БД смотрю kubectl get pv и при необходимости ставлю Retain, плюс держу бэкапы.
Что хотят услышать: Delete удаляет диск, Retain оставляет, политика берётся из StorageClass, Released-том нужно чистить руками, бэкап всё равно нужен.
Красный флаг: Считать, что данные всегда переживут удаление PVC.
13. [middle] Как увеличить размер диска PVC, когда место заканчивается?
Ответ
Сначала проверяю, что у StorageClass стоит allowVolumeExpansion: true, и драйвер умеет расширять том. Затем правлю spec.resources.requests.storage у PVC на большее значение, например через kubectl edit pvc. Уменьшить размер нельзя. Файловая система обычно расширяется сама, иногда после перезапуска пода; состояние вижу в kubectl describe pvc (conditions). Для StatefulSet шаблон volumeClaimTemplates у существующего объекта обычно не поменять, поэтому правлю сами PVC.
Что хотят услышать: allowVolumeExpansion, правка requests PVC, только увеличение, зависит от драйвера, проверка через describe.
Красный флаг: «Удалю PVC и создам побольше» без бэкапа.
Проверено на версиях
Проверено командами на этом курсе (Docker на Mac, без кластера Kubernetes):
- PostgreSQL 18 (образ
postgres:18): каталог данных/var/lib/postgresql/18/docker,pg_isready, создание таблицы и вставка строки черезpsqlс<<'SQL',pg_dump(97 строк дампа для одной строки),password authentication failedпри неверном пароле по сети,role "postgres" does not existпри неверном-U,relation ... does not existдля отсутствующей таблицы; - манифест
40-postgres.yaml:kubeconform -strictбез замечаний (схема Kubernetes 1.3x); скриптbreak/5.5/break.sh:shellcheckбез замечаний, логика проверена чтением.
Не прогонялось в кластере kind, вывод команд kubectl взят из предыдущей редакции урока и сверен по знанию формата (значения AGE, имя PV и IP пода у тебя будут другими):
- kind: v0.33.0, Kubernetes: 1.37.1 (kubectl 1.37.1), StorageClass
standard(rancher.io/local-path); - busybox: 1.37.
Итог урока: ты умеешь
- объяснить, чем PV, PVC и StorageClass отличаются и что значит
WaitForFirstConsumer - написать StatefulSet с
volumeClaimTemplatesи headless Service - создать Secret командой и подключить его пароль в контейнер PostgreSQL 18
- проверить, что данные пережили удаление пода и StatefulSet, и назвать, что их убивает
- сделать
pg_dumpиз пода и восстановить из дампа - диагностировать
Pendingу PVC иpassword authentication failedпо событиям и логам - решить, когда БД в кластере оправдана, а когда нужен оператор или Managed-сервис
Дальше: Урок 5.6: ConfigMap и Secret
Проверь себя
Короткий тест по уроку: 5 вопросов из банка в 30. Засчитывается только полностью правильный ответ, порог 60%. Каждая новая попытка даёт другие вопросы, пока банк не закончится. Ответы видны после проверки.
Тест работает с включённым JavaScript.