diff --git a/postgresql/cutover/postgresql-ha-staging.yaml b/postgresql/cutover/postgresql-ha-staging.yaml new file mode 100644 index 0000000..c2b3ce1 --- /dev/null +++ b/postgresql/cutover/postgresql-ha-staging.yaml @@ -0,0 +1,200 @@ +# ───────────────────────────────────────────────────────────────────────── +# postgresql/cutover/postgresql-ha-staging.yaml — ADR-0001 Phase 3, Stage 2 +# +# Deployed by postgres-ha-cutover.sh as a SEPARATE stack: +# docker stack deploy -c postgresql/cutover/postgresql-ha-staging.yaml postgresqlha +# +# ⚠️ NEVER deploy this via stack-deploy.sh or under the "postgresql" stack +# name. It lives in cutover/ (a subdirectory) deliberately: +# stack-deploy.sh folder mode merges all *.yml/yaml at -maxdepth 1 of +# postgresql/, and merging this into the production stack would cause +# an outage. Subdirectory files are excluded from that merge. +# +# What this stage does: +# - Joins the EXISTING postgresql_db-backend overlay (external) — the +# live single-instance stack is untouched and keeps the "postgresql" +# and "db" aliases. +# - patroni-0/patroni-1 bootstrap a NEW cluster (scope: postgres-ha) by +# CLONING the live instance via pg_basebackup (CLONE_WITH_BASEBACKUP, +# CLONE_HOST=postgresql → resolves to the OLD instance during staging). +# CLONE_* is set on BOTH patroni nodes: whichever wins the initial +# leader race performs the clone; the other syncs from it. +# - HAProxy runs WITHOUT production aliases and WITHOUT published ports. +# It only gets those in postgresql-ha-final.yaml at promote time. +# - Superuser is PGadmin (matches the cloned data's existing superuser, +# same password secret), NOT Spilo's default "postgres". +# ───────────────────────────────────────────────────────────────────────── +version: "3.6" + +services: + + etcd-1: + image: quay.io/coreos/etcd:v3.5.9 + hostname: etcd-1 + command: + - etcd + - --name=etcd-1 + - --data-dir=/etcd-data + - --initial-advertise-peer-urls=http://etcd-1:2380 + - --listen-peer-urls=http://0.0.0.0:2380 + - --listen-client-urls=http://0.0.0.0:2379 + - --advertise-client-urls=http://etcd-1:2379 + - --initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380 + - --initial-cluster-state=new + - --initial-cluster-token=postgresql-ha-etcd + volumes: + - /volume1/docker/PostgreSQL/etcd-1-data:/etcd-data + networks: + - postgresql_db-backend + deploy: + placement: + constraints: + - node.hostname == docker-1 + + etcd-2: + image: quay.io/coreos/etcd:v3.5.9 + hostname: etcd-2 + command: + - etcd + - --name=etcd-2 + - --data-dir=/etcd-data + - --initial-advertise-peer-urls=http://etcd-2:2380 + - --listen-peer-urls=http://0.0.0.0:2380 + - --listen-client-urls=http://0.0.0.0:2379 + - --advertise-client-urls=http://etcd-2:2379 + - --initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380 + - --initial-cluster-state=new + - --initial-cluster-token=postgresql-ha-etcd + volumes: + - /volume1/docker/PostgreSQL/etcd-2-data:/etcd-data + networks: + - postgresql_db-backend + deploy: + placement: + constraints: + - node.hostname == docker-2 + + etcd-3: + image: quay.io/coreos/etcd:v3.5.9 + hostname: etcd-3 + command: + - etcd + - --name=etcd-3 + - --data-dir=/etcd-data + - --initial-advertise-peer-urls=http://etcd-3:2380 + - --listen-peer-urls=http://0.0.0.0:2380 + - --listen-client-urls=http://0.0.0.0:2379 + - --advertise-client-urls=http://etcd-3:2379 + - --initial-cluster=etcd-1=http://etcd-1:2380,etcd-2=http://etcd-2:2380,etcd-3=http://etcd-3:2380 + - --initial-cluster-state=new + - --initial-cluster-token=postgresql-ha-etcd + volumes: + - /volume1/docker/PostgreSQL/etcd-3-data:/etcd-data + networks: + - postgresql_db-backend + deploy: + placement: + constraints: + - node.hostname == docker-3 + + patroni-0: + image: ghcr.io/zalando/spilo-17:4.0-p3 + hostname: patroni-0 + command: + - /bin/sh + - -c + - | + export PGPASSWORD_SUPERUSER="$(cat /run/secrets/postgresql_password)" + export PGPASSWORD_STANDBY="$(cat /run/secrets/postgresql_replication_password)" + export PATRONI_RESTAPI_PASSWORD="$(cat /run/secrets/postgresql_patroni_password)" + export CLONE_PASSWORD="$(cat /run/secrets/postgresql_password)" + exec /bin/sh /launch.sh init + environment: + SCOPE: postgres-ha + PATRONI_NAME: patroni-0 + ETCD_HOSTS: '"etcd-1:2379","etcd-2:2379","etcd-3:2379"' + PGUSER_SUPERUSER: PGadmin + PGUSER_STANDBY: standby + PATRONI_RESTAPI_USERNAME: patroni + PGROOT: /home/postgres/pgdata/pgroot + # Clone-from-live: only runs when PGDATA is empty (first bootstrap). + # "postgresql" resolves to the OLD single instance during staging. + CLONE_METHOD: CLONE_WITH_BASEBACKUP + CLONE_SCOPE: legacy-single + CLONE_HOST: postgresql + CLONE_PORT: "5432" + CLONE_USER: PGadmin + secrets: + - postgresql_password + - postgresql_replication_password + - postgresql_patroni_password + volumes: + - /volume1/docker/PostgreSQL/patroni-0-data:/home/postgres/pgdata + networks: + - postgresql_db-backend + deploy: + placement: + constraints: + - node.labels.pg-role == primary + + patroni-1: + image: ghcr.io/zalando/spilo-17:4.0-p3 + hostname: patroni-1 + command: + - /bin/sh + - -c + - | + export PGPASSWORD_SUPERUSER="$(cat /run/secrets/postgresql_password)" + export PGPASSWORD_STANDBY="$(cat /run/secrets/postgresql_replication_password)" + export PATRONI_RESTAPI_PASSWORD="$(cat /run/secrets/postgresql_patroni_password)" + export CLONE_PASSWORD="$(cat /run/secrets/postgresql_password)" + exec /bin/sh /launch.sh init + environment: + SCOPE: postgres-ha + PATRONI_NAME: patroni-1 + ETCD_HOSTS: '"etcd-1:2379","etcd-2:2379","etcd-3:2379"' + PGUSER_SUPERUSER: PGadmin + PGUSER_STANDBY: standby + PATRONI_RESTAPI_USERNAME: patroni + PGROOT: /home/postgres/pgdata/pgroot + CLONE_METHOD: CLONE_WITH_BASEBACKUP + CLONE_SCOPE: legacy-single + CLONE_HOST: postgresql + CLONE_PORT: "5432" + CLONE_USER: PGadmin + secrets: + - postgresql_password + - postgresql_replication_password + - postgresql_patroni_password + volumes: + - /volume1/docker/PostgreSQL/patroni-1-data:/home/postgres/pgdata + networks: + - postgresql_db-backend + deploy: + placement: + constraints: + - node.labels.pg-role == replica + + # Staging HAProxy: NO production aliases, NO published ports. Reachable + # in-network as "haproxy" for pre-promote testing only. + haproxy: + image: haproxy:2.9-alpine + hostname: haproxy + volumes: + - /volume1/docker/compose-files/postgresql/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro + networks: + - postgresql_db-backend + deploy: + mode: global + +networks: + postgresql_db-backend: + external: true + +secrets: + postgresql_password: + external: true + postgresql_replication_password: + external: true + postgresql_patroni_password: + external: true