PostgreSQL StatefulSet в Kubernetes на одном хосте

Обновлено и опубликовано Опубликовано:

Используемые термины: PostgreSQLKubernetes.

В этой инструкции разберем PostgreSQL Kubernetes deployment без стороннего PostgreSQL Kubernetes operator — это тот случай, когда StatefulSet справляется сам, а оператор избыточен. Подход годится для теста, локальной разработки или одноузлового окружения — там, где не нужна автоматическая репликация и failover.

Подготовка к работе

Предварительно, сделаем несколько настроек и создадим вспомогательные манифесты.

Каталог манифестов и 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.

# DevOps # Kubernetes # Базы данных # Серверы
Дмитрий Моск — частный мастер
Был ли вам полезен этот скрипт?

Да            Нет

Дмитрий Моск
— IT-специалист.
Настройка серверов, услуги DevOps.

Заказать настройку контейнеризации

Нужна бесплатная консультация?

Скрипты

PostgreSQL StatefulSet в Kubernetes на одном хосте

GitLab CE в Docker Compose за Traefik с HTTPS

Пример манифеста для запуска Swagger в Kubernetes

Пример скрипта для миграции запущенной виртуальной машины Proxmox с ZFS репликацией

Пример манифеста для развертывания Hermes AI в Kubernetes

Пример файла docker-compose для развертывания брокера Kafka

Пример файла docker-compose для настройки и запуска Netbox

Другие скрипты

Все статьи

Нужен скрипт? Опишите его назначение:





Реклама