Instalasi PostgreSQL + Patroni

Status: SUDAH DIIMPLEMENTASIKAN (8 Juli 2026)

Cluster PostgreSQL 3-node (dikelola Patroni) sudah berjalan sehat di namespace postgres-system, berdampingan dengan Galera (bukan pengganti) — sesuai keputusan Admin. Backup WAL-G ke S3 aktif, uji failover lulus.

1. Keputusan Teknis Awal

Item Keputusan
Operator Zalando postgres-operator — salah satu dari sedikit operator yang benar-benar memakai Patroni (beda dari CloudNativePG yang pakai instance manager sendiri)
Namespace postgres-system — terpisah dari galera-system, berjalan paralel
Jumlah node 3 (1 Leader + 1 Sync Standby + 1 Replica async, dikelola Patroni)
Sizing per node Request 0,5 vCPU / 1GB — Limit 1 vCPU / 2GB (konsisten dengan Galera)
Storage per node 50Gi, StorageClass longhorn-db-single (reuse dari setup Galera)
PostgreSQL version 16
synchronous_mode true (konsistensi ketat — sesuai keputusan Admin, trade-off sedikit latency demi zero data loss saat failover)

2. Install Operator

helm repo add postgres-operator-charts https://opensource.zalando.com/postgres-operator/charts/postgres-operator
helm install postgres-operator postgres-operator-charts/postgres-operator -n postgres-system --create-namespace

Berbeda dari mariadb-operator: CRD ikut otomatis

Tidak seperti mariadb-operator yang butuh chart CRD terpisah, chart ini langsung menyertakan CRD (postgresqls.acid.zalan.do, dll.) — operator langsung Running tanpa perlu langkah tambahan.

3. Deploy Cluster (Custom Resource postgresql)

apiVersion: acid.zalan.do/v1
kind: postgresql
metadata:
  name: postgres-cluster
  namespace: postgres-system
spec:
  teamId: "oneke"
  numberOfInstances: 3
  postgresql:
    version: "16"
  volume:
    size: 50Gi
    storageClass: longhorn-db-single
  resources:
    requests: { cpu: 500m, memory: 1Gi }
    limits: { cpu: 1000m, memory: 2Gi }
  users:
    appuser: ["createdb"]
  databases:
    appdb: appuser
  patroni:
    synchronous_mode: true
    synchronous_mode_strict: false

Pod anti-affinity otomatis aktif secara default (operator built-in) — terkonfirmasi 3 pod tersebar di 3 node fisik berbeda tanpa konfigurasi tambahan.

4. Verifikasi Kesehatan Cluster ✅

kubectl exec -n postgres-system postgres-cluster-0 -- patronictl list

Hasil:

Member Role State Lag
postgres-cluster-0 Leader running
postgres-cluster-1 Sync Standby streaming 0 MB
postgres-cluster-2 Replica streaming 0 MB

Status "Sync Standby" mengonfirmasi synchronous_mode: true aktif dan bekerja.

5. Database & User Aplikasi ✅

Dibuat otomatis oleh operator dari field users/databases di CR:

Item Nilai
Database appdb
User appuser (privilege createdb, owner appdb)
Password Auto-generated, tersimpan di Secret appuser.postgres-cluster.credentials.postgresql.acid.zalan.do

Nama sama dengan Galera — disengaja

Database/user di sini bernama sama (appdb/appuser) seperti di cluster Galera, sesuai instruksi Admin. Kedua cluster berjalan di namespace terpisah (postgres-system vs galera-system) jadi tidak ada konflik teknis — hanya perlu diperhatikan saat memilih cluster mana yang dituju dari sisi aplikasi/connection string.

6. Backup ke S3 via WAL-G ✅ (Native, Lebih Kaya dari Galera)

6.1 Konfigurasi

Reuse endpoint oneke-s3.ums.id dan kredensial oneke-svc yang sama seperti Galera, dengan prefix folder terpisah (postgres-dumps/) di bucket oneke-backups yang sama:

kubectl create secret generic postgres-pod-env -n postgres-system \
  --from-literal=WAL_S3_BUCKET='oneke-backups' \
  --from-literal=WALG_S3_PREFIX='s3://oneke-backups/postgres-dumps' \
  --from-literal=AWS_ENDPOINT='https://oneke-s3.ums.id' \
  --from-literal=AWS_ACCESS_KEY_ID='oneke-svc' \
  --from-literal=AWS_SECRET_ACCESS_KEY='<secret>' \
  --from-literal=AWS_S3_FORCE_PATH_STYLE='true' \
  --from-literal=USE_WALG_BACKUP='true' \
  --from-literal=USE_WALG_RESTORE='true'

kubectl patch operatorconfiguration postgres-operator -n postgres-system --type merge \
  -p '{"configuration":{"kubernetes":{"pod_environment_secret":"postgres-pod-env"}}}'

kubectl rollout restart deployment postgres-operator -n postgres-system

Setelah operator restart, ia otomatis melakukan rolling-update seluruh pod cluster untuk menyuntikkan env var baru — tidak perlu trigger manual per-pod.

Keuntungan dibanding Galera: tidak ada bug, plus dapat WAL archiving kontinu

Berbeda dari mariadb-operator yang Backup CRD bawaannya crash (lihat Instalasi MariaDB Galera §8.2), mekanisme WAL-G bawaan Spilo (image resmi operator ini) bekerja langsung tanpa masalah. Selain base backup terjadwal otomatis, WAL archiving kontinu juga aktif — memberi kemampuan Point-in-Time Recovery (PITR) yang tidak dimiliki setup Galera saat ini.

6.2 Verifikasi

kubectl exec -n postgres-system postgres-cluster-1 -- \
  su postgres -c 'bash /scripts/postgres_backup.sh /home/postgres/pgdata/pgroot/data'

Gotcha: jangan panggil wal-g langsung via kubectl exec

Memanggil wal-g backup-push langsung sebagai user root (default kubectl exec) gagal — Postgres auth peer/local butuh OS-user postgres, bukan root. Gunakan su postgres -c '...' atau jalankan lewat script resmi /scripts/postgres_backup.sh yang sudah menangani ini dengan benar.

Hasil verifikasi: backup + WAL archiving terkonfirmasi ada di oneke-backups/postgres-dumps/basebackups_005/ dan wal_005/ via mc ls --recursive.

7. Uji Failover ✅

kubectl delete pod postgres-cluster-1 -n postgres-system --force --grace-period=0

Hasil: Timeline (TL) naik dari 2 → 3 (konfirmasi promosi otomatis oleh Patroni benar-benar terjadi), cluster tetap tersedia sepanjang proses, seluruh node kembali streaming dengan 0 MB lag. Database appdb tetap utuh — tanpa intervensi manual.

8. Ringkasan Kredensial

Secret Namespace Isi
appuser.postgres-cluster.credentials.postgresql.acid.zalan.do postgres-system Password appuser (auto-generated)
postgres.postgres-cluster.credentials.postgresql.acid.zalan.do postgres-system Password superuser postgres (auto-generated)
postgres-pod-env postgres-system Kredensial S3 untuk WAL-G (reuse oneke-svc)
kubectl get secret appuser.postgres-cluster.credentials.postgresql.acid.zalan.do \
  -n postgres-system -o jsonpath='{.data.password}' | base64 -d

9. pgAdmin ✅

Dipasang untuk kebutuhan penelitian (pola sama seperti phpMyAdmin) — akses via https://oneke.ums.id/pqadmin/.

9.1 Deployment

apiVersion: apps/v1
kind: Deployment
metadata:
  name: pgadmin
  namespace: postgres-system
spec:
  replicas: 1
  selector:
    matchLabels: { app: pgadmin }
  template:
    metadata:
      labels: { app: pgadmin }
    spec:
      containers:
      - name: pgadmin
        image: dpage/pgadmin4:latest
        env:
        - name: PGADMIN_DEFAULT_EMAIL
          valueFrom: { secretKeyRef: { name: pgadmin-credentials, key: email } }
        - name: PGADMIN_DEFAULT_PASSWORD
          valueFrom: { secretKeyRef: { name: pgadmin-credentials, key: password } }
        - name: SCRIPT_NAME
          value: /pqadmin
        volumeMounts:
        - name: servers-config
          mountPath: /pgadmin4/servers.json
          subPath: servers.json
        resources:
          requests: { cpu: 100m, memory: 256Mi }
          limits: { cpu: 1500m, memory: 1Gi }   # lihat gotcha CPU di bawah
      volumes:
      - name: servers-config
        configMap: { name: pgadmin-servers }

Koneksi ke postgres-cluster.postgres-system.svc.cluster.local sudah pre-konfigurasi otomatis lewat servers.json (ConfigMap) — begitu login, server sudah muncul di daftar, tinggal masukkan password appuser.

9.2 Routing — Beda dari phpMyAdmin/Dashboard/Docs!

Gotcha: JANGAN pakai stripPrefix untuk pgAdmin

Berbeda dari semua path lain (/plan/, /phpmyadmin/, /oneke-dashboard/) yang memakai middleware stripPrefix, pgAdmin justru butuh path lengkap /pqadmin/... tetap utuh sampai ke container — ia strip prefix-nya sendiri secara internal berdasarkan env SCRIPT_NAME/header X-Script-Name. Memakai stripPrefix seperti biasa menghasilkan error 500: "Configuration problem: Request path '/' does not start with SCRIPT_NAME '/pqadmin'".

Middleware yang benar: hanya redirect trailing-slash + headers (menambahkan X-Script-Name: /pqadmin) — tanpa stripPrefix: yaml apiVersion: traefik.io/v1alpha1 kind: Middleware metadata: name: pqadmin-headers namespace: postgres-system spec: headers: customRequestHeaders: X-Script-Name: /pqadmin

9.3 Gotcha Lain: Validasi Email & CPU Throttling

Email .local ditolak

Percobaan awal pakai admin@oneke.local ditolak validator email pgAdmin (TLD .local tidak dianggap valid domain publik). Diganti ke admin@oneke.ums.id (domain asli) — beres.

CPU limit 500m terlalu kecil untuk startup

Limit CPU awal (500m, sama seperti phpMyAdmin) membuat pgAdmin sangat lambat saat inisialisasi pertama (build SQLite config db + load server config) — sempat terlihat seperti macet selama >1 menit. Dinaikkan ke 1500m (limit) — startup selesai dalam hitungan detik setelahnya. phpMyAdmin tidak mengalami ini karena lebih ringan (PHP, bukan Python+Flask+SQLite init).

9.4 Status Keamanan

Sama seperti phpMyAdmin — proteksi tambahan (IP whitelist/Basic Auth) sengaja ditunda untuk fase penelitian. Login form pgAdmin sendiri (email+password) jadi satu-satunya gerbang.

Kredensial login tersimpan di Secret pgadmin-credentials (namespace postgres-system, key email dan password) — tidak dituliskan plaintext di sini.

Ringkasan Eksekusi

Langkah Status
Namespace postgres-system
postgres-operator + CRDs (otomatis)
Cluster PostgreSQL 3 node, synchronous_mode ✅ Healthy, 1 Leader + 1 Sync Standby + 1 Replica
Anti-affinity ✅ Otomatis, 3 node fisik berbeda
Database appdb + user appuser ✅ Auto-created
Backup WAL-G ke S3 + WAL archiving kontinu (PITR) ✅ Native, tanpa workaround
Uji failover ✅ Lulus, timeline naik, 0 lag, 0 data loss
Berjalan paralel dengan Galera ✅ Namespace terpisah, tidak ada konflik
pgAdmin (/pqadmin/) ✅ Live, pre-configured connection, proteksi tambahan tertunda

Diimplementasikan 8 Juli 2026. Lihat Instalasi MariaDB Galera untuk perbandingan langsung kedua database yang kini berjalan berdampingan di cluster oneKE.