PostgreSQL StatefulSet в Kubernetes на одном хосте
Опубликовано:
Используемые термины: PostgreSQL, Kubernetes.
В этой инструкции разберем PostgreSQL Kubernetes deployment без стороннего PostgreSQL Kubernetes operator — это тот случай, когда StatefulSet справляется сам, а оператор избыточен. Подход годится для теста, локальной разработки или одноузлового окружения — там, где не нужна автоматическая репликация и failover.
Преднастройка
Пример файла для StatefulSet
Проверка запуска и работы пода
Создание пользователя и базы SQL
Типовые проблемы
Резервное копирование
Подготовка к работе
Предварительно, сделаем несколько настроек и создадим вспомогательные манифесты.
Каталог манифестов и namespace
Создадим каталог для манифестов и перейдем в него, затем отдельный namespace для базы данных:
mkdir -p /opt/pg-stateful && cd /opt/pg-stateful
kubectl create namespace database
* все манифесты — в /opt/pg-stateful, ресурсы — в namespace database.
Проверим, какие StorageClass доступны в кластере — понадобится имя класса для тома:
kubectl get storageclass
* запомните имя класса (часто local-path, standard, hostpath) — оно понадобится в volumeClaimTemplates.
Secret с паролем
Пароль суперпользователя postgres храним в Secret, а не в открытом виде в StatefulSet. Создадим файл манифеста:
vi pg-secret.yml
apiVersion: v1
kind: Secret
metadata:
name: pg-stateful-creds
namespace: database
type: Opaque
stringData:
POSTGRES_PASSWORD: ChangeMe_StrongPass
* где:
- stringData — kubectl сам закодирует в base64; пароль не попадает в историю shell (в отличие от --from-literal).
- POSTGRES_PASSWORD — пароль суперпользователя postgres; официальный образ требует его при старте (для сетевых подключений). БД и роль приложения на этом шаге не создаем.
- pg-secret.yml — после apply удаляем файл с диска.
Применим манифест и сразу удалим файл с диска, чтобы пароль не оставался в открытом виде:
kubectl apply -f pg-secret.yml
rm -f pg-secret.yml
* альтернатива — создать Secret одной командой, без файла на диске: kubectl create secret generic pg-stateful-creds --from-literal=POSTGRES_PASSWORD='...' -n database. Чтобы пароль не попал в историю shell, перед командой ставим пробел (если в bash/zsh включен HISTCONTROL=ignorespace).
ConfigMap параметров PostgreSQL
Вынесем postgresql.conf и pg_hba.conf в ConfigMap — так конфигурация редактируется отдельно от образа и без пересборки. Создадим манифест:
vi pg-config.yml
apiVersion: v1
kind: ConfigMap
metadata:
name: pg-stateful-config
namespace: database
data:
postgresql.conf: |
listen_addresses = '*'
max_connections = 100
shared_buffers = 128MB
wal_level = replica
log_timezone = 'Europe/Moscow'
timezone = 'Europe/Moscow'
pg_hba.conf: |
local all all trust
host all all 127.0.0.1/32 scram-sha-256
host all all ::1/128 scram-sha-256
host all all 10.0.0.0/8 scram-sha-256
host all all 172.16.0.0/12 scram-sha-256
host all all 192.168.0.0/16 scram-sha-256
* где:
- shared_buffers = 128MB — ~25% от limits.memory 512Mi в StatefulSet ниже; при смене memory меняйте и этот параметр.
- pg_hba.conf — доступ из типичных pod CIDR; если у кластера другой диапазон — добавьте свою сеть.
- local ... trust — только для peer-подключений внутри контейнера (пробы pg_isready).
Применим ConfigMap:
kubectl apply -f pg-config.yml
Headless Service и ClusterIP
Заведем два Service: headless для стабильного DNS-имени пода (нужен StatefulSet-у как serviceName) и обычный ClusterIP для подключения приложений. Создадим манифест:
vi pg-service.yml
apiVersion: v1
kind: Service
metadata:
name: pg-stateful-hl
namespace: database
labels:
app.kubernetes.io/name: pg-stateful
spec:
clusterIP: None
publishNotReadyAddresses: true
selector:
app.kubernetes.io/name: pg-stateful
ports:
- name: postgres
port: 5432
targetPort: postgres
---
apiVersion: v1
kind: Service
metadata:
name: pg-stateful
namespace: database
labels:
app.kubernetes.io/name: pg-stateful
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: pg-stateful
ports:
- name: postgres
port: 5432
targetPort: postgres
* где:
- pg-stateful-hl — headless Service для StatefulSet (serviceName); DNS пода: pg-stateful-0.pg-stateful-hl.database.svc.
- pg-stateful — обычный ClusterIP для приложений: pg-stateful.database.svc:5432.
- publishNotReadyAddresses: true — DNS headless доступен до Ready (удобно при инициализации).
Применим оба сервиса:
kubectl apply -f pg-service.yml
Пример файла для StatefulSet
Дальше — сам манифест Postgres StatefulSet:
vi pg-statefulset.yml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: pg-stateful
namespace: database
labels:
app.kubernetes.io/name: pg-stateful
spec:
serviceName: pg-stateful-hl
replicas: 1
podManagementPolicy: OrderedReady
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
selector:
matchLabels:
app.kubernetes.io/name: pg-stateful
template:
metadata:
labels:
app.kubernetes.io/name: pg-stateful
spec:
terminationGracePeriodSeconds: 60
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: postgres
image: postgres:17
imagePullPolicy: IfNotPresent
ports:
- name: postgres
containerPort: 5432
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: pg-stateful-creds
key: POSTGRES_PASSWORD
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
args:
- -c
- config_file=/etc/postgresql/postgresql.conf
- -c
- hba_file=/etc/postgresql/pg_hba.conf
resources:
requests:
memory: 512Mi
cpu: "250m"
limits:
memory: 512Mi
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "pg_ctl stop -D \"$PGDATA\" -m fast || true; sleep 10"]
startupProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
periodSeconds: 5
failureThreshold: 30
timeoutSeconds: 3
livenessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
privileged: false
runAsNonRoot: true
capabilities:
drop:
- ALL
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: config
mountPath: /etc/postgresql
readOnly: true
volumes:
- name: config
configMap:
name: pg-stateful-config
items:
- key: postgresql.conf
path: postgresql.conf
- key: pg_hba.conf
path: pg_hba.conf
volumeClaimTemplates:
- metadata:
name: data
labels:
app.kubernetes.io/name: pg-stateful
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
resources:
requests:
storage: 10Gi
* где:
- replicas: 1 — однохостовый инстанс; StatefulSet не делает streaming-репликацию сам по себе. Anti-affinity при одной реплике не нужен.
- serviceName: pg-stateful-hl — обязан совпадать с headless Service.
- updateStrategy.rollingUpdate.maxUnavailable: 1 — при replicas: 1 значение 0 блокирует обновление пода; для Deployment с несколькими репликами обычно maxUnavailable: 0 и maxSurge.
- terminationGracePeriodSeconds: 60 — запас под preStop (остановка Postgres + sleep 10) до SIGKILL.
- preStop — fast stop через pg_ctl, затем sleep 10, чтобы endpoint успел уйти из Service.
- POSTGRES_PASSWORD — единственная переменная инициализации образа; без POSTGRES_USER/POSTGRES_DB поднимаются суперпользователь postgres и системная БД postgres.
- startupProbe / livenessProbe / readinessProbe — pg_isready -U postgres; долгий initdb не убивает liveness; не Ready — нет трафика в ClusterIP.
- resources — memory requests = limits (Guaranteed QoS). CPU limits не задаем — только requests.
- runAsUser/fsGroup: 999 — UID пользователя postgres в официальном образе.
- PGDATA .../pgdata — подкаталог на томе: образ postgres:17 монтирует данные в /var/lib/postgresql/data, а initdb пишет в подпапку.
- readOnlyRootFilesystem не включаем: entrypoint и runtime Postgres пишут во временные пути образа.
- storageClassName — класс вашей одноузловой среды.
- volumeClaimTemplates — PVC вида data-pg-stateful-0.
* при kubectl delete StatefulSet PVC не удаляется автоматически — данные остаются на диске. Если нужно поднять чистую БД заново, PVC удаляем отдельной командой: kubectl delete pvc -n database data-pg-stateful-0.
Применим StatefulSet:
kubectl apply -f pg-statefulset.yml
Проверка запуска и работы пода
Дождемся раскатки и проверим статус пода:
kubectl -n database rollout status statefulset/pg-stateful
kubectl get pods -n database -l app.kubernetes.io/name=pg-stateful -o wide
* под pg-stateful-0 в Running / Ready 1/1.
Проверим, что PVC создался и привязался к тому:
kubectl get pvc -n database
* data-pg-stateful-0 в статусе Bound.
Проверим оба Service:
kubectl get svc -n database
Подключимся к базе изнутри пода и проверим, что сервер отвечает:
kubectl exec -n database -it pg-stateful-0 -- psql -U postgres -c 'SELECT current_user, current_database(), inet_server_addr();'
* внутри пода по local ... trust вход под postgres без пароля. Пока есть только системная БД postgres.
Создание пользователя и базы SQL
Роль и базу для приложения создадим отдельно от суперпользователя postgres. Подготовим SQL-скрипт:
vi init-app.sql
CREATE USER app WITH PASSWORD 'AppPass_ChangeMe';
CREATE DATABASE app OWNER app;
GRANT ALL PRIVILEGES ON DATABASE app TO app;
\c app
GRANT ALL ON SCHEMA public TO app;
ALTER SCHEMA public OWNER TO app;
* где:
- CREATE USER / CREATE DATABASE — роль и БД приложения после старта сервера, не через env образа.
- \c app — переключение на новую БД в том же psql-скрипте.
- GRANT / ALTER SCHEMA — права на public в PostgreSQL 15+.
Выполним скрипт внутри пода и сразу удалим файл с диска:
cat init-app.sql | kubectl exec -i -n database pg-stateful-0 -- psql -U postgres
rm -f init-app.sql
Проверим, что роль и база появились:
kubectl exec -n database -it pg-stateful-0 -- psql -U postgres -c '\du'
kubectl exec -n database -it pg-stateful-0 -- psql -U postgres -c '\l'
* в списке ролей должен быть app, в списке БД — app.
** инициализация через psql вручную подходит для теста. Для CI/CD и повторяемых деплоев тот же SQL-скрипт обычно оформляют как initContainer или разовый Job, который ждет готовности пода и выполняет команды сам.
Типовые проблемы
Под Pending
Pod не стартует и зависает в состоянии Pending.
Причина: под не запускается, потому что PVC не может привязаться к тому — нет подходящего StorageClass или не хватает ресурсов на узле.
Решение: Смотрим события пода и состояние тома:
kubectl describe pod -n database pg-stateful-0 | sed -n '/Events:/,$p'
kubectl get pvc -n database
kubectl get storageclass
* если нет StorageClass или PVC Pending — поправьте storageClassName. Insufficient memory — уменьшите resources.memory и shared_buffers.
CrashLoop
Запуск пода выполняется с ошибкой CrashLoop.
Причина: под запускается и падает по кругу — чаще всего из-за прав доступа на смонтированный том.
Решение: смотрим логи контейнера:
kubectl logs -n database pg-stateful-0 --tail=100
* ошибки permission denied на PGDATA — проверьте fsGroup: 999 и что том смонтирован в /var/lib/postgresql/data, а PGDATA указывает на подкаталог pgdata.
Конфиг не применяется
Причина: после правки ConfigMap параметры на лету не подхватываются — под их читает только при старте.
Решение: проверим текущее значение внутри пода:
kubectl exec -n database pg-stateful-0 -- psql -U postgres -c "SHOW shared_buffers; SHOW config_file;"
* после правки ConfigMap перезапустите под: kubectl delete pod -n database pg-stateful-0 (данные на PVC сохранятся). Для автоматического рестарта при изменении ConfigMap без ручной команды используют stakater/Reloader или хэш конфигурации в аннотациях пода, добавляемый в CI/CD.
Резервное копирование
StatefulSet сам по себе не делает бэкапы — данные лежат на PVC, но снимок состояния БД нужно снимать отдельно. Простой вариант — pg_dump прямо из пода:
kubectl exec -n database pg-stateful-0 -- pg_dump -U app app > app-backup.sql
* дамп сохраняется на хосте, откуда запущена команда, а не внутри пода. Для регулярных бэкапов такую команду оформляют в CronJob.