В данной инструкции описан процесс развёртывания высокодоступного кластера PostgreSQL 18 в Kubernetes с использованием оператора CloudNativePG.
Кластер состоит из трёх нод: одной главной (primary) и двух реплик (replica). При отказе главной ноды одна из реплик автоматически становится главной, обеспечивая непрерывность работы.
Kubernetes-кластер с минимум 3 worker-нодами
Дополнительный диск на каждой worker-ноде (минимум 10 ГБ)
Установленные инструменты на управляющем хосте:
kubectlhelmДля работы с локальными дисками используется Local Static Provisioner (LSP) — официальный провайсер Kubernetes (kubernetes-sigs). LSP отслеживает блочные устройства на узлах и автоматически создаёт PersistentVolume для каждого найденного диска с привязкой к конкретной ноде (node affinity). Ручная подготовка дисков на worker-нодах не требуется: провизионер находит устройство, а форматирование в ext4 (согласно fsType) выполняется автоматически при первом монтировании тома в под — диск монтируется только внутри подов.
Примечание: Детальное описание работы с PV/PVC и Local Storage вынесено в отдельные статьи. Здесь приведён минимально необходимый набор для PostgreSQL.
Файл values-block.yaml:
# Mount the host's `/dev/` by default so that block device symlinks can be
# resolved by the containers
mountDevVolume: false
# Configuration for classes of static volumes.
classes:
- name: local-storage # Defines name of storage classes.
hostDir: /dev
namePattern: "vdb"
volumeMode: Filesystem
accessMode: ReadWriteOnce
fsType: ext4
blockCleanerCommand:
- "/scripts/shred.sh"
- "2"
storageClass:
name: local-storage
reclaimPolicy: Retain # Available reclaim policies: Delete/Retain, defaults: Delete.
isDefaultClass: true # set as default class
provisioner: kubernetes.io/no-provisioner
Важно:
namePatternдолжен совпадать с именем дополнительного диска на ваших нодах (обычноvdb, имя устройства можно посмотреть на ноде:lsblk). Подробнее работа с дисками описана в отдельной статье
Сгенерируйте манифест через Helm и примените его:
helm repo add sig-storage-local-static-provisioner \
https://kubernetes-sigs.github.io/sig-storage-local-static-provisioner
helm repo update
helm template sig-storage-local-static-provisioner/local-static-provisioner \
--version 2.9.0 \
--namespace sig-storage \
-f values-block.yaml > lsp-block.yaml
kubectl create ns sig-storage
kubectl apply -f lsp-block.yaml
Провизионер разворачивается как DaemonSet. Каждая нода, где найден диск, подходящий под namePattern, получает автоматически созданный PV с node affinity — вручную перечислять ноды не нужно.
Через короткое время провизионер отследит наличие /dev/vdb на узлах и создаст PV. Проверьте статус:
kubectl get pv
Ожидаемый результат — по одному PV на ноде:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS
local-pv-xxxxxx 35Gi RWO Retain Available
local-pv-yyyyyy 35Gi RWO Retain Available
local-pv-zzzzzz 35Gi RWO Retain Available
Имена PV генерируются автоматически (хеш от имени устройства, ноды и класса).
Провизионер автоматически создаёт StorageClass local-storage (указан в values-файле):
kubectl get sc local-storage
Далее в этой статье используется именно этот StorageClass (local-storage).
CloudNativePG — это Kubernetes-оператор, который управляет кластером PostgreSQL: создаёт инстансы, настраивает репликацию, автоматически выполняет failover.
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo update cnpg
helm install cnpg cnpg/cloudnative-pg --namespace cnpg-system --create-namespace
kubectl get pods -n cnpg-system
Ожидаемый результат:
NAME READY STATUS
cnpg-cloudnative-pg-xxxxxxxxx-xxxxx 1/1 Running
kubectl create namespace postgresql
Придумайте надёжный пароль и замените ВАШ_ПАРОЛЬ на него:
kubectl create secret generic postgres-credentials \
--namespace=postgresql \
--from-literal=username=app \
--from-literal=password='ВАШ_ПАРОЛЬ'
Важно: В секрете ключ
usernameдолжен быть имя пользователя, который будет владельцем базы данных. В даннмом примере указан пользовательapp
pg-cluster.yaml---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cluster-postgres
namespace: postgresql
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:18.1
postgresql:
parameters:
max_connections: "200"
storage:
size: 35Gi
storageClass: local-storage
resources:
requests:
cpu: "1"
memory: 1Gi
limits:
cpu: "2"
memory: 1800Mi
primaryUpdateStrategy: unsupervised
bootstrap:
initdb:
database: appdb
owner: app
secret:
name: postgres-credentials
postInitApplicationSQL:
- "CREATE EXTENSION IF NOT EXISTS pg_stat_statements;"
affinity:
enablePodAntiAffinity: true
---
apiVersion: v1
kind: Service
metadata:
name: postgres-external
namespace: postgresql
spec:
type: LoadBalancer
selector:
cnpg.io/cluster: cluster-postgres
cnpg.io/instanceRole: primary
ports:
- name: postgresql
port: 5432
targetPort: 5432
protocol: TCP
Настройка ресурсов: значения
resourcesдолжны соответствовать доступным ресурсам ваших нод. Проверьте доступные ресурсы командойkubectl describe nodes. Если на вашей ноде меньше ресурсов, уменьшите значенияrequestsиlimits.StorageClass:
local-storage— StorageClass, автоматически создаётся chart’ом LSP (см. раздел «Настройка Local Storage с LSP»).
kubectl apply -f pg-cluster.yaml
kubectl get cluster -n postgresql -w
Наблюдайте за состоянием кластера. Процесс может занять 2–5 минут. Ожидаемый результат:
NAME AGE INSTANCES READY STATUS PRIMARY
cluster-postgres 5m 3 3 Cluster in healthy state cluster-postgres-1
Кластер готов, когда STATUS показывает Cluster in healthy state и READY равен 3.
kubectl get pods -n postgresql -l cnpg.io/podRole=instance -o wide
Каждый под должен находиться на отдельной ноде благодаря enablePodAntiAffinity:
NAME READY STATUS NODE
cluster-postgres-1 1/1 Running worker-1
cluster-postgres-2 1/1 Running worker-2
cluster-postgres-3 1/1 Running worker-3
После создания кластера настройте пароли для пользователей PostgreSQL.
Важно: Пароль суперпользователя
postgresгенерируется CNPG случайно и неизвестен. Поэтому первичную настройку паролей выполняем через локальный сокет внутри пода (peer-аутентификация, пароль не нужен).
PRIMARY_POD=$(kubectl get pods -n postgresql -l cnpg.io/instanceRole=primary -o jsonpath='{.items[0].metadata.name}')
echo "$PRIMARY_POD"
Соединение через Unix-сокет внутри пода — без пароля (peer-аутентификация):
kubectl exec -it "$PRIMARY_POD" -n postgresql -c postgres -- psql -U postgres -d appdb
ALTER USER postgres WITH PASSWORD 'ВАШ_ПАРОЛЬ_СУПЕРП';
ALTER USER app WITH PASSWORD 'ВАШ_ПАРОЛЬ_APP';
\q
kubectl delete secret postgres-credentials -n postgresql
kubectl create secret generic postgres-credentials \
--namespace=postgresql \
--from-literal=username=app \
--from-literal=password='ВАШ_ПАРОЛЬ_APP'
Сервис postgres-external (LoadBalancer) предоставляет доступ к primary инстансу PostgreSQL. После создания сервиса внешний IP будет назначен балансировщиком:
kubectl get svc postgres-external -n postgresql
Ожидаемый результат:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
postgres-external LoadBalancer 10.96.x.x <EXTERNAL-IP> 5432:xxxxx/TCP
psql -h <EXTERNAL-IP> -p 5432 -U app -d appdb
Если вы подключаетесь изнутри кластера (из другого пода), используйте встроенные сервисы CloudNativePG:
| Сервис | Назначение | Адрес |
|---|---|---|
cluster-postgres-rw |
Запись/чтение (только primary) | cluster-postgres-rw.postgresql.svc.cluster.local:5432 |
cluster-postgres-r |
Все ноды (round-robin) | cluster-postgres-r.postgresql.svc.cluster.local:5432 |
cluster-postgres-ro |
Только чтение (только replica) | cluster-postgres-ro.postgresql.svc.cluster.local:5432 |
Важно: Сервисы типа NodePort не используются — на worker-нодах отсутствуют белые IP-адреса. Для внешнего доступа применяется LoadBalancer, а для внутреннего — ClusterIP-сервисы CloudNativePG.
kubectl get cluster -n postgresql
kubectl get pods -n postgresql \
-l cnpg.io/instanceRole=primary
kubectl get pods -n postgresql -l cnpg.io/podRole=instance
kubectl logs <имя-pod> -n postgresql -f
Через локальный сокет (без пароля, только с primary):
PRIMARY_POD=$(kubectl get pods -n postgresql -l cnpg.io/instanceRole=primary -o jsonpath='{.items[0].metadata.name}')
kubectl exec -it "$PRIMARY_POD" -n postgresql -c postgres -- psql -U postgres -d appdb
Подключение внутри кластера:
PGPASSWORD='ВАШ_ПАРОЛЬ_СУПЕРП' psql -h cluster-postgres-rw.postgresql.svc.cluster.local -U postgres -d appdb
kubectl patch cluster cluster-postgres -n postgresql --type merge --patch '{"spec":{"instances":3}}'