246 lines
9.5 KiB
YAML
246 lines
9.5 KiB
YAML
# ──────────────────────────────────────────────────────────────────────────
|
|
# postgresql/cutover/postgresql-ha-final.yaml — ADR-0001 Phase 3, Stage 3
|
|
#
|
|
# Deployed by cutover.sh (Phase 7) as an UPDATE to the SAME "postgresqlha"
|
|
# stack that postgresql-ha-staging.yaml created in Stage 2:
|
|
# docker stack deploy -c postgresql/cutover/postgresql-ha-final.yaml postgresqlha
|
|
#
|
|
# ⚠️ Do NOT deploy this before Patroni has been genuinely promoted
|
|
# (cutover.sh Phase 5) and confirmed healthy (Phase 6). Deploying it
|
|
# earlier adds the postgresql/db aliases to a backend HAProxy's own
|
|
# healthcheck (GET /primary) will correctly report as unhealthy (a
|
|
# standby_leader is read-only, not a real primary) — any client that
|
|
# Swarm's round-robin DNS routes to HAProxy during that window would
|
|
# hit zero healthy servers and fail outright. See RUNBOOK.md for the
|
|
# full rationale on phase ordering.
|
|
#
|
|
# What changed vs. postgresql-ha-staging.yaml (Stage 2):
|
|
# - etcd-1/2/3 and patroni-0/patroni-1 service specs are BYTE-IDENTICAL
|
|
# to the staging file, deliberately. `docker stack deploy` diffs each
|
|
# service's spec independently — since these five specs are unchanged,
|
|
# Swarm will NOT recreate/restart them. This matters enormously here:
|
|
# patroni-1 is the live, newly-promoted PRIMARY carrying real
|
|
# production traffic by the time this file is deployed, and
|
|
# patroni-0 is its live replica. Neither may be disrupted by this
|
|
# step. Only the haproxy service (below) has a spec change, so only
|
|
# it gets updated.
|
|
# - haproxy service: adds `postgresql` and `db` as network aliases on
|
|
# postgresql_db-backend — this is the actual DNS cutover mechanism.
|
|
# Still deliberately WITHOUT the external port publish (5430) or
|
|
# traefik_backend network/labels that the STEADY-STATE production
|
|
# postgresql.yaml's haproxy block eventually needs — taking over
|
|
# that external/Traefik-SNI path is a documented MANUAL, POST-cutover
|
|
# step (see RUNBOOK.md "Post-cutover follow-up"), not part of this
|
|
# automated procedure. Confirmed with the operator: no external
|
|
# consumer depends on port 5430 during the cutover window, so this
|
|
# is deferred deliberately, not an oversight.
|
|
# - Uses the same postgresql/haproxy.cfg already committed to git
|
|
# (Patroni-aware TCP router, GET /primary healthcheck, no changes
|
|
# needed for this stage).
|
|
#
|
|
# Once this is live and all consumers have verified (cutover.sh Phase 9),
|
|
# legacy is scaled to 0 (Phase 10) and this becomes the sole answer for
|
|
# the `postgresql`/`db` names on postgresql_db-backend.
|
|
# ──────────────────────────────────────────────────────────────────────────
|
|
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)"
|
|
mkdir -p /scripts
|
|
cat > /scripts/post_init_wrapper.sh <<'WRAP'
|
|
#!/bin/bash
|
|
set -e
|
|
psql -d "$$2" -v ON_ERROR_STOP=1 -c 'DO $$do$$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = $$x$$postgres$$x$$) THEN CREATE ROLE postgres WITH SUPERUSER NOLOGIN; END IF; END $$do$$;'
|
|
exec /scripts/post_init.sh "$$@"
|
|
WRAP
|
|
chmod +x /scripts/post_init_wrapper.sh
|
|
exec /bin/sh /launch.sh init
|
|
environment:
|
|
SCOPE: postgres-ha
|
|
PATRONI_NAME: patroni-0
|
|
ETCD3_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
|
|
SPILO_CONFIGURATION: |
|
|
bootstrap:
|
|
post_init: /scripts/post_init_wrapper.sh "zalandos"
|
|
dcs:
|
|
standby_cluster:
|
|
host: postgresql
|
|
port: 5432
|
|
create_replica_methods:
|
|
- basebackup_fast_xlog
|
|
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)"
|
|
mkdir -p /scripts
|
|
cat > /scripts/post_init_wrapper.sh <<'WRAP'
|
|
#!/bin/bash
|
|
set -e
|
|
psql -d "$$2" -v ON_ERROR_STOP=1 -c 'DO $$do$$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = $$x$$postgres$$x$$) THEN CREATE ROLE postgres WITH SUPERUSER NOLOGIN; END IF; END $$do$$;'
|
|
exec /scripts/post_init.sh "$$@"
|
|
WRAP
|
|
chmod +x /scripts/post_init_wrapper.sh
|
|
exec /bin/sh /launch.sh init
|
|
environment:
|
|
SCOPE: postgres-ha
|
|
PATRONI_NAME: patroni-1
|
|
ETCD3_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
|
|
SPILO_CONFIGURATION: |
|
|
bootstrap:
|
|
post_init: /scripts/post_init_wrapper.sh "zalandos"
|
|
dcs:
|
|
standby_cluster:
|
|
host: postgresql
|
|
port: 5432
|
|
create_replica_methods:
|
|
- basebackup_fast_xlog
|
|
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
|
|
|
|
# ── Final-stage HAProxy: takes over the postgresql/db aliases ──────────
|
|
# Internal Swarm-DNS only at this stage — no published port, no
|
|
# traefik_backend network/labels. External access (port 5430 /
|
|
# Traefik SNI) is a deliberate, documented MANUAL post-cutover step,
|
|
# not part of this automated procedure — confirmed no external
|
|
# consumer depends on it during the cutover window.
|
|
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:
|
|
aliases:
|
|
- postgresql
|
|
- db
|
|
deploy:
|
|
mode: global
|
|
|
|
networks:
|
|
postgresql_db-backend:
|
|
external: true
|
|
|
|
secrets:
|
|
postgresql_password:
|
|
external: true
|
|
postgresql_replication_password:
|
|
external: true
|
|
postgresql_patroni_password:
|
|
external: true
|