Files
compose-files/postgresql/cutover/test/pgha-dryrun.yaml
T

246 lines
8.9 KiB
YAML

# ─────────────────────────────────────────────────────────────────────────
# postgresql/cutover/test/pgha-dryrun.yaml — DISPOSABLE dry-run stack
#
# Validates the ADR-0001 cutover mechanics WITHOUT touching production:
# - own overlay network (pgha-test_db-backend), own stack name
# - throwaway "legacy" postgres:17 seeded with marker data, carrying the
# in-network alias "postgresql" (mirrors prod CLONE_HOST)
# - same etcd/patroni/haproxy topology, images, and env wiring as the
# real staging file — including clone-from-live via pg_basebackup
# - reuses the real Docker secrets (read-only mounts; harmless)
# - disposable data dirs under /volume1/docker/PostgreSQL/dryrun/
#
# Deploy: docker stack deploy -c <this file> pgha-test
# Teardown: docker stack rm pgha-test && rm -rf /volume1/docker/PostgreSQL/dryrun
#
# NOTE: command: blocks use $$(...) not $(...) — Compose's own variable
# interpolation parses $( as an attempted ${VAR} reference and fails with
# "invalid interpolation format" / "you may need to escape any $ with
# another $". $$ escapes to a literal $ for the shell at runtime. Same
# reasoning applies to $$PGDATA below.
#
# NOTE: ETCD3_HOSTS not ETCD_HOSTS — confirmed against zalando/spilo
# configure_spilo.py: PATRONI_DCS includes both "etcd" (legacy v2 API,
# python-etcd client) and "etcd3" (v3 API, python-etcd3 client) as
# distinct DCS backends selected by env var prefix. Our etcd containers
# (v3.5.9) have the v2 API disabled by default, so ETCD_HOSTS causes
# Patroni to hit /v2 endpoints that 404. ETCD3_HOSTS selects the correct
# v3-API client.
#
# NOTE: "legacy" needs a pg_hba.conf rule permitting REPLICATION-type
# connections, which is a distinct connection class from normal client
# connections in Postgres. The vanilla postgres:17 image's default
# pg_hba.conf allows normal client connections (pg_dumpall worked earlier)
# but NOT replication connections, which pg_basebackup (used by Patroni's
# CLONE_WITH_BASEBACKUP) requires. Fixed via a /docker-entrypoint-initdb.d/
# hook script (the officially supported extension point, runs once right
# after initdb, before the server's final restart) that appends a
# permissive "trust" rule for replication connections. "trust" is
# acceptable ONLY because this container is fully disposable and exists
# solely to validate cutover mechanics, not auth. See the note
# "ADR-0001 Addendum — pg_hba.conf replication prerequisite discovered in
# dry run" for the real production prerequisite this exposes.
# ─────────────────────────────────────────────────────────────────────────
version: "3.6"
services:
# Stand-in for the production single-instance postgres (clone source)
legacy:
image: public.ecr.aws/docker/library/postgres:17
hostname: db
entrypoint: ["/bin/sh", "-c"]
command:
- |
mkdir -p /docker-entrypoint-initdb.d
cat > /docker-entrypoint-initdb.d/zz-enable-replication.sh <<'EOF'
#!/bin/sh
echo "host replication all all trust" >> "$$PGDATA/pg_hba.conf"
EOF
chmod +x /docker-entrypoint-initdb.d/zz-enable-replication.sh
exec docker-entrypoint.sh postgres
environment:
POSTGRES_USER: PGadmin
POSTGRES_PASSWORD_FILE: /run/secrets/postgresql_password
secrets:
- postgresql_password
volumes:
- /volume1/docker/PostgreSQL/dryrun/legacy:/var/lib/postgresql/data
networks:
db-backend:
aliases:
- postgresql
- db
deploy:
placement:
constraints:
- node.hostname == docker-2
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=pgha-test-etcd
volumes:
- /volume1/docker/PostgreSQL/dryrun/etcd-1:/etcd-data
networks:
- 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=pgha-test-etcd
volumes:
- /volume1/docker/PostgreSQL/dryrun/etcd-2:/etcd-data
networks:
- 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=pgha-test-etcd
volumes:
- /volume1/docker/PostgreSQL/dryrun/etcd-3:/etcd-data
networks:
- 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: pgha-test
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
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/dryrun/patroni-0:/home/postgres/pgdata
networks:
- 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: pgha-test
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
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/dryrun/patroni-1:/home/postgres/pgdata
networks:
- db-backend
deploy:
placement:
constraints:
- node.labels.pg-role == replica
haproxy:
image: haproxy:2.9-alpine
hostname: haproxy
volumes:
- /volume1/docker/compose-files/postgresql/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
networks:
- db-backend
deploy:
mode: global
networks:
db-backend:
attachable: true
driver: overlay
secrets:
postgresql_password:
external: true
postgresql_replication_password:
external: true
postgresql_patroni_password:
external: true