В данной инструкции описан процесс развёртывания высокодоступного кластера PostgreSQL 18 в Kubernetes с использованием оператора CloudNativePG.
Кластер состоит из трёх нод: одной главной (primary) и двух реплик (replica). При отказе главной ноды одна из реплик автоматически становится главной, обеспечивая непрерывность работы.
Kubernetes-кластер с минимум 3 worker-нодами
Дополнительный диск на каждой worker-ноде (минимум 10 ГБ)
Установленные инструменты на управляющем хосте:
kubectlhelmКаждая worker-нода должна иметь смонтированный диск в /mnt/data. Диск форматируется в формате ext4 и права устанавливаются для пользователя postgres (UID 26).
disk-mount-daemonset.yamlapiVersion: apps/v1
kind: DaemonSet
metadata:
name: disk-mount-daemon
namespace: kube-system
labels:
app: disk-mount
spec:
selector:
matchLabels:
app: disk-mount
template:
metadata:
labels:
app: disk-mount
spec:
priorityClassName: system-node-critical
hostPID: true
containers:
- name: disk-mount
image: ubuntu:24.04
command:
- bash
- -c
- |
apt-get update -qq && apt-get install -y -qq e2fsprogs > /dev/null 2>&1
nsenter -t 1 -m -u -i -n -p mkdir -p /mnt/data
nsenter -t 1 -m -u -i -n -p mkfs.ext4 -F /dev/vdb 2>/dev/null || true
nsenter -t 1 -m -u -i -n -p mount /dev/vdb /mnt/data
nsenter -t 1 -m -u -i -n -p chown -R 26:26 /mnt/data
nsenter -t 1 -m -u -i -n -p chmod 700 /mnt/data
echo "Disk mounted on $(hostname)"
df -h /mnt/data
tail -f /dev/null
securityContext:
privileged: true
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
cpu: 100m
memory: 256Mi
tolerations:
- operator: Exists
kubectl apply -f disk-mount-daemonset.yaml
Убедитесь, что поды запущены на всех нодах:
kubectl get pods -n kube-system -l app=disk-mount
Ожидаемый результат:
NAME READY STATUS NODE
disk-mount-daemon-xxxxx 1/1 Running worker-1
disk-mount-daemon-xxxxx 1/1 Running worker-2
disk-mount-daemon-xxxxx 1/1 Running worker-3
Проверьте логи для подтверждения монтирования:
kubectl logs -n kube-system <имя-pod>
Лог должен содержать:
Filesystem Size Used Avail Use% Mounted on
/dev/vdb 10G 24K 9.8G 1% /mnt/data
Примечание: Если на вашей ноде дополнительный диск имеет другое имя (например,
/dev/sdb), замените/dev/vdbв YAML-файле на соответствующее значение.
pv-pvc.yaml---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-storage
provisioner: kubernetes.io/no-provisioner
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pg-data-worker-1
labels:
app: postgres
node: worker-1
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
storageClassName: local-storage
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /mnt/data
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-1
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pg-data-worker-2
labels:
app: postgres
node: worker-2
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
storageClassName: local-storage
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /mnt/data
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-2
---
apiVersion: v1
kind: PersistentVolume
metadata:
name: pg-data-worker-3
labels:
app: postgres
node: worker-3
spec:
capacity:
storage: 10Gi
accessModes:
- ReadWriteOnce
storageClassName: local-storage
persistentVolumeReclaimPolicy: Retain
hostPath:
path: /mnt/data
nodeAffinity:
required:
nodeSelectorTerms:
- matchExpressions:
- key: kubernetes.io/hostname
operator: In
values:
- worker-3
Примечание: Если ваши worker-ноды имеют другие имена, замените
worker-1,worker-2,worker-3на соответствующие значения. Узнать имена можно командойkubectl get nodes.
kubectl apply -f pv-pvc.yaml
kubectl get pv
kubectl get sc
Ожидаемый результат:
NAME CAPACITY ACCESS MODES RECLAIM POLICY STATUS
pg-data-worker-1 10Gi RWO Retain Available
pg-data-worker-2 10Gi RWO Retain Available
pg-data-worker-3 10Gi RWO Retain Available
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:16.4
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: NodePort
selector:
postgresql.cnpg.io/cluster: cluster-postgres
postgresql.cnpg.io/instance-role: primary
ports:
- name: postgresql
port: 5432
targetPort: 5432
nodePort: 30432
protocol: TCP
Настройка ресурсов: значения
resourcesдолжны соответствовать доступным ресурсам ваших нод. Проверьте доступные ресурсы командойkubectl describe nodes. Если на вашей ноде меньше ресурсов, уменьшите значенияrequestsиlimits.
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.
Узнайте имя primary пода:
kubectl get cluster -n postgresql
Подключитесь к консоли PostgreSQL:
kubectl exec -it <имя-primary-pod> -n postgresql --container 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 (NodePort) создан вместе с кластером и предоставляет доступ к primary ноде PostgreSQL с порта 30432.
kubectl get nodes -o wide
Запишите внутренние IP-адреса из колонки INTERNAL-IP.
psql -h <IP-НОДЫ> -p 30432 -U app -d appdb
Если вы подключаетесь изнутри кластера (из другого пода), используйте:
| Сервис | Назначение | Адрес |
|---|---|---|
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 |
kubectl get cluster -n postgresql
kubectl get pods -n postgresql -l cnpg.io/podRole=instance
kubectl logs <имя-pod> -n postgresql -f
kubectl exec <имя-primary-pod> -n postgresql --container postgres -- psql -U postgres -d appdb
kubectl patch cluster cluster-postgres -n postgresql --type merge --patch '{"spec":{"instances":3}}'