From 21e1f61369d39eb5e89a2cbb4a2c879e175751cf Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Fri, 29 Mar 2024 14:26:06 +0200 Subject: [PATCH 1/3] Add fundamental development configuration --- .example.env | 3 ++ .gitignore | 1 + Dockerfile | 16 ++++++ README.md | 52 ++++++++++++++++++- compose.yml | 45 ++++++++++++++++ dev/secrets/postgres-password | 1 + dev/secrets/replicator-password | 1 + .../10-setup-replication-user.sh | 4 ++ .../20-setup-replication-auth.sh | 6 +++ postgres/secondary/bin/10-base-backup.sh | 6 +++ .../secondary/bin/20-enable-standby-mode.sh | 3 ++ .../secondary/bin/30-configure-primary.sh | 6 +++ .../bin/docker-entrypoint-override.sh | 16 ++++++ 13 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 .example.env create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 compose.yml create mode 100644 dev/secrets/postgres-password create mode 100644 dev/secrets/replicator-password create mode 100644 postgres/primary/docker-entrypoint-initdb.d/10-setup-replication-user.sh create mode 100644 postgres/primary/docker-entrypoint-initdb.d/20-setup-replication-auth.sh create mode 100755 postgres/secondary/bin/10-base-backup.sh create mode 100755 postgres/secondary/bin/20-enable-standby-mode.sh create mode 100755 postgres/secondary/bin/30-configure-primary.sh create mode 100755 postgres/secondary/bin/docker-entrypoint-override.sh diff --git a/.example.env b/.example.env new file mode 100644 index 0000000..d9602eb --- /dev/null +++ b/.example.env @@ -0,0 +1,3 @@ +POSTGRES_CIDR=172.55.32.0/24 +POSTGRES_SUBNET=172.55.0.0/16 +POSTGRES_GATEWAY=172.55.32.254 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2eea525 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..01c6a91 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM postgres:16.2 as base + + +FROM base as primary + +COPY ./postgres/primary/docker-entrypoint-initdb.d/ docker-entrypoint-initdb.d/ + + +FROM base as secondary + +COPY ./postgres/secondary/bin/ /usr/local/bin/ +ENTRYPOINT [ "docker-entrypoint-override.sh" ] + +CMD ["postgres"] + +FROM primary diff --git a/README.md b/README.md index 9c8026f..9013b26 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,55 @@ # Postgres with LOGIC -Production-ready Postgres with Docker that does not suck, with [LOGIC](https://withlogic.co). +This repository contains the configuration for production-grade Postgres with Docker, with [LOGIC](https://withlogic.co). It was first presented on 24 January 2024, at Docker Athens during the presentation [Production grade Postgres with Docker](https://www.youtube.com/watch?v=tJegTc-oLtk)[^1]. + +The configuration in this repository sets up a primary and secondary Postgres server with streaming replication. This means that the primary server accepts all write queries, which are in turn replicated to the secondary server. Therefore, read queries can be load balanced and performed on both servers. + +## Requirements + +- Docker Engine 25.0.0 or newer +- Docker Compose 2.24.9 or newer, for development + +## Configuration + +The setup of Postgres with LOGIC is configured with environment variables and Docker Secrets for sensitive data. + +### Environment variables + +- `POSTGRES_CIDR`: The CIDR block from which to allocate IPs in the Docker network and also allow replication from (default: `172.54.32.0/24`) +- `POSTGRES_GATEWAY`: The gateway to use in the Docker network (default: `172.54.32.254`) +- `POSTGRES_SUBNET`: The subnet to allocate for the Docker network (default: `172.54.0.0/16`) + +For convenience, in development these environment variables can be set in a `.env` environment file. Example file available in [`.example.env`](./.example.env) + +## Development + +To kick off and evaluate the setup locally, all you have to do is run + +```console +docker compose up +``` + +After all containers start, you can validate the setup with the following steps: + +1. Create a table on the primary server + ```console + docker compose exec primary psql -U postgres -c "CREATE TABLE people (name varchar(40));" + ``` +2. Insert a couple of rows in the primary server + ```console + docker compose exec primary psql -U postgres -c "INSERT INTO people VALUES ('grace');" + docker compose exec primary psql -U postgres -c "INSERT INTO people VALUES ('alan');" + ``` +3. Validate that data can be read from both servers + ```console + docker compose exec primary psql -U postgres -c "SELECT * FROM people;" + docker compose exec secondary psql -U postgres -c "SELECT * FROM people;" + ``` + +---

- + 🦄 Built with LOGIC. 🦄

+ +[^1]: Presentation on YouTube: https://www.youtube.com/watch?v=tJegTc-oLtk \ No newline at end of file diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..148606b --- /dev/null +++ b/compose.yml @@ -0,0 +1,45 @@ +x-base: + &base + secrets: + - postgres-password + - replicator-password + environment: + POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password + POSTGRES_CIDR: ${POSTGRES_CIDR:-172.54.32.0/24} + command: ["postgres", "-c", "log_statement=all"] + +services: + primary: + <<: *base + build: + context: . + target: primary + volumes: + - primary_data:/var/lib/postgresql/data + + secondary: + <<: *base + build: + context: . + target: secondary + restart: on-failure:3 + volumes: + - secondary_data:/var/lib/postgresql/data + +secrets: + postgres-password: + file: dev/secrets/postgres-password + replicator-password: + file: dev/secrets/replicator-password + +networks: + default: + ipam: + config: + - subnet: ${POSTGRES_SUBNET:-172.54.0.0/16} + ip_range: ${POSTGRES_CIDR:-172.54.32.0/24} + gateway: ${POSTGRES_GATEWAY:-172.54.32.254} + +volumes: + primary_data: + secondary_data: diff --git a/dev/secrets/postgres-password b/dev/secrets/postgres-password new file mode 100644 index 0000000..c0e753f --- /dev/null +++ b/dev/secrets/postgres-password @@ -0,0 +1 @@ +development_password \ No newline at end of file diff --git a/dev/secrets/replicator-password b/dev/secrets/replicator-password new file mode 100644 index 0000000..1f31f49 --- /dev/null +++ b/dev/secrets/replicator-password @@ -0,0 +1 @@ +development_replicator_password \ No newline at end of file diff --git a/postgres/primary/docker-entrypoint-initdb.d/10-setup-replication-user.sh b/postgres/primary/docker-entrypoint-initdb.d/10-setup-replication-user.sh new file mode 100644 index 0000000..e7092f3 --- /dev/null +++ b/postgres/primary/docker-entrypoint-initdb.d/10-setup-replication-user.sh @@ -0,0 +1,4 @@ +set -ex + +REPLICATOR_PASSWORD=$(cat /run/secrets/replicator-password) +psql -c "CREATE ROLE replicator REPLICATION LOGIN PASSWORD '$REPLICATOR_PASSWORD'" \ No newline at end of file diff --git a/postgres/primary/docker-entrypoint-initdb.d/20-setup-replication-auth.sh b/postgres/primary/docker-entrypoint-initdb.d/20-setup-replication-auth.sh new file mode 100644 index 0000000..861ef3d --- /dev/null +++ b/postgres/primary/docker-entrypoint-initdb.d/20-setup-replication-auth.sh @@ -0,0 +1,6 @@ +set -ex + +POSTGRES_CIDR=${POSTGRES_CIDR:-172.54.32.0/24} +PGDATA=${PGDATA:-/var/lib/postgresql/data} + +echo "host replication replicator $POSTGRES_CIDR scram-sha-256" >> $PGDATA/pg_hba.conf diff --git a/postgres/secondary/bin/10-base-backup.sh b/postgres/secondary/bin/10-base-backup.sh new file mode 100755 index 0000000..10a8513 --- /dev/null +++ b/postgres/secondary/bin/10-base-backup.sh @@ -0,0 +1,6 @@ +set -ex + +export PGPASSWORD=$(cat /run/secrets/replicator-password) + +pg_basebackup -w -h primary -D $PGDATA -U replicator -P -v -X stream +chmod -R 0750 $PGDATA diff --git a/postgres/secondary/bin/20-enable-standby-mode.sh b/postgres/secondary/bin/20-enable-standby-mode.sh new file mode 100755 index 0000000..64fc817 --- /dev/null +++ b/postgres/secondary/bin/20-enable-standby-mode.sh @@ -0,0 +1,3 @@ +set -ex + +touch $PGDATA/standby.signal diff --git a/postgres/secondary/bin/30-configure-primary.sh b/postgres/secondary/bin/30-configure-primary.sh new file mode 100755 index 0000000..8ba48e7 --- /dev/null +++ b/postgres/secondary/bin/30-configure-primary.sh @@ -0,0 +1,6 @@ +set -ex + +pg_ctl -w start +REPLICATOR_PASSWORD=$(cat /run/secrets/replicator-password) +psql -c "ALTER SYSTEM SET primary_conninfo = 'host=primary port=5432 user=replicator password=$REPLICATOR_PASSWORD application_name=secondary';" +pg_ctl -w stop diff --git a/postgres/secondary/bin/docker-entrypoint-override.sh b/postgres/secondary/bin/docker-entrypoint-override.sh new file mode 100755 index 0000000..589da8e --- /dev/null +++ b/postgres/secondary/bin/docker-entrypoint-override.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -ex + +if [ ! -f $PGDATA/PG_VERSION ] +then + echo "Secondary server is not initialized. Starting initialization procedure." + su postgres -c '10-base-backup.sh' + su postgres -c '20-enable-standby-mode.sh' + su postgres -c '30-configure-primary.sh' + echo "Initialization procedure completed successfully." +else + echo "Secondary server is already initialized." +fi + +exec docker-entrypoint.sh "$@" From 2b13f73f52517f58e3a63f388534f50c97c11540 Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Fri, 29 Mar 2024 16:41:30 +0200 Subject: [PATCH 2/3] Add documentation about secrets --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 9013b26..b3ac390 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,13 @@ The setup of Postgres with LOGIC is configured with environment variables and Do For convenience, in development these environment variables can be set in a `.env` environment file. Example file available in [`.example.env`](./.example.env) +### Secrets + +- `postgres-password`: The password for the `postgres` user of the database used for most database operations +- `replicator-password`: The password for the `replicator` role, used from the secondary server to replicate data + +For convenience, in development all secrets are hardcoded as files and are available in the [`dev/secrets`](./dev/secrets) directory and do not need to be set. + ## Development To kick off and evaluate the setup locally, all you have to do is run From 65a996df248b6a0a6ebf6644279c10e44ef36a5a Mon Sep 17 00:00:00 2001 From: Paris Kasidiaris Date: Fri, 29 Mar 2024 17:37:32 +0200 Subject: [PATCH 3/3] Add pgpool2 configuration --- compose.yml | 24 ++++--- pgpool2/Dockerfile | 19 ++++++ pgpool2/bin/docker-entrypoint.sh | 14 +++++ pgpool2/pgpool2/pcp.conf.tpl | 4 ++ pgpool2/pgpool2/pgpool.conf.tpl | 101 ++++++++++++++++++++++++++++++ pgpool2/pgpool2/pool_hba.conf | 9 +++ pgpool2/pgpool2/pool_passwd.tpl | 1 + Dockerfile => postgres/Dockerfile | 0 8 files changed, 163 insertions(+), 9 deletions(-) create mode 100644 pgpool2/Dockerfile create mode 100755 pgpool2/bin/docker-entrypoint.sh create mode 100644 pgpool2/pgpool2/pcp.conf.tpl create mode 100644 pgpool2/pgpool2/pgpool.conf.tpl create mode 100644 pgpool2/pgpool2/pool_hba.conf create mode 100644 pgpool2/pgpool2/pool_passwd.tpl rename Dockerfile => postgres/Dockerfile (100%) diff --git a/compose.yml b/compose.yml index 148606b..05dad22 100644 --- a/compose.yml +++ b/compose.yml @@ -1,18 +1,18 @@ x-base: &base - secrets: - - postgres-password - - replicator-password - environment: - POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password - POSTGRES_CIDR: ${POSTGRES_CIDR:-172.54.32.0/24} - command: ["postgres", "-c", "log_statement=all"] + secrets: + - postgres-password + - replicator-password + environment: + POSTGRES_PASSWORD_FILE: /run/secrets/postgres-password + POSTGRES_CIDR: ${POSTGRES_CIDR:-172.54.32.0/24} + command: ["postgres", "-c", "log_statement=all"] services: primary: <<: *base build: - context: . + context: postgres target: primary volumes: - primary_data:/var/lib/postgresql/data @@ -20,12 +20,18 @@ services: secondary: <<: *base build: - context: . + context: postgres target: secondary restart: on-failure:3 volumes: - secondary_data:/var/lib/postgresql/data + pgpool2: + build: + context: pgpool2 + secrets: + - postgres-password + secrets: postgres-password: file: dev/secrets/postgres-password diff --git a/pgpool2/Dockerfile b/pgpool2/Dockerfile new file mode 100644 index 0000000..f47c5ad --- /dev/null +++ b/pgpool2/Dockerfile @@ -0,0 +1,19 @@ +FROM ubuntu:22.04 as pgpool + +# https://www.pgpool.net/mediawiki/index.php/Apt_Repository + +ENV DEBIAN_FRONTEND noninteractive + +RUN apt-get update +RUN apt-get install -y wget gnupg +RUN echo "deb http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list +RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - +RUN apt-get update +RUN apt-get install -y pgpool2 libpgpool2 postgresql-16-pgpool2 +RUN apt-get install -y gettext-base + +COPY pgpool2/ /etc/pgpool2/ +COPY bin/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh + +ENTRYPOINT [ "docker-entrypoint.sh" ] +CMD [ "pgpool", "-n" ] \ No newline at end of file diff --git a/pgpool2/bin/docker-entrypoint.sh b/pgpool2/bin/docker-entrypoint.sh new file mode 100755 index 0000000..cb59a67 --- /dev/null +++ b/pgpool2/bin/docker-entrypoint.sh @@ -0,0 +1,14 @@ +#! /bin/bash + +set -ex + +export POSTGRES_PASSWORD=$(cat /run/secrets/postgres-password) +cat /etc/pgpool2/pgpool.conf.tpl | envsubst > /etc/pgpool2/pgpool.conf +cat /etc/pgpool2/pool_passwd.tpl | envsubst > /etc/pgpool2/pool_passwd +unset POSTGRES_PASSWORD + +export POSTGRES_PASSWORD_MD5=$(md5sum /run/secrets/postgres-password | awk '{print $1}') +cat /etc/pgpool2/pcp.conf.tpl | envsubst > /etc/pgpool2/pcp.conf +unset POSTGRES_PASSWORD_MD5 + +exec "$@" diff --git a/pgpool2/pgpool2/pcp.conf.tpl b/pgpool2/pgpool2/pcp.conf.tpl new file mode 100644 index 0000000..c4c64cb --- /dev/null +++ b/pgpool2/pgpool2/pcp.conf.tpl @@ -0,0 +1,4 @@ +# PCP Client Authentication Configuration File +# ============================================ + +postgres:${POSTGRES_PASSWORD_MD5} diff --git a/pgpool2/pgpool2/pgpool.conf.tpl b/pgpool2/pgpool2/pgpool.conf.tpl new file mode 100644 index 0000000..728542f --- /dev/null +++ b/pgpool2/pgpool2/pgpool.conf.tpl @@ -0,0 +1,101 @@ +# ---------------------------- +# pgPool-II configuration file +# ---------------------------- +# +# Docs: https://www.pgpool.net/docs/latest/en/html/configuring-pgpool.html#:~:text=conf%20is%20the%20main%20configuration,%24prefix%2Fetc%2Fpgpool. + +#------------------------------------------------------------------------------ +# BACKEND CLUSTERING MODE +#------------------------------------------------------------------------------ + +backend_clustering_mode = 'streaming_replication' + + +#------------------------------------------------------------------------------ +# CONNECTIONS +#------------------------------------------------------------------------------ + +# - pgpool Connection Settings - + +listen_addresses = '*' +port = 5432 + +# - pgpool Communication Manager Connection Settings - + +pcp_listen_addresses = 'localhost' +pcp_port = 9898 + +# - Backend Connection Settings - + +backend_hostname0 = 'primary' +backend_port0 = 5432 +backend_weight0 = 1 +backend_data_directory0 = '/var/lib/postgresql/data' +backend_flag0 = 'ALLOW_TO_FAILOVER' +backend_application_name0 = 'primary' + +backend_hostname1 = 'secondary' +backend_port1 = 5432 +backend_weight1 = 1 +backend_data_directory1 = '/var/lib/postgresql/data' +backend_flag1 = 'ALLOW_TO_FAILOVER' +backend_application_name1 = 'secondary' + +# - Authentication - + +enable_pool_hba = on + +# - SSL Connections - + +#------------------------------------------------------------------------------ +# LOGS +#------------------------------------------------------------------------------ + +# - Where to log - +log_per_node_statement = on + + +#------------------------------------------------------------------------------ +# LOAD BALANCING MODE +#------------------------------------------------------------------------------ + +load_balance_mode = on + + +#------------------------------------------------------------------------------ +# STREAMING REPLICATION MODE +#------------------------------------------------------------------------------ + +# - Streaming - + +sr_check_period = 2 +sr_check_user = 'postgres' +sr_check_password = '${POSTGRES_PASSWORD}' +sr_check_database = 'postgres' + + +#------------------------------------------------------------------------------ +# HEALTH CHECK GLOBAL PARAMETERS +#------------------------------------------------------------------------------ + +health_check_period = 30 +health_check_timeout = 20 +health_check_user = 'postgres' +health_check_password = '${POSTGRES_PASSWORD}' +health_check_database = 'postgres' +health_check_max_retries = 3 +health_check_retry_delay = 10 + +#------------------------------------------------------------------------------ +# FAILOVER AND FAILBACK +#------------------------------------------------------------------------------ +failover_command = 'PGPASSWORD=${POSTGRES_PASSWORD} psql --host secondary -U postgres -c "SELECT pg_promote();"' +failover_on_backend_error = on +failover_on_backend_shutdown = on +detach_false_primary = on + +#------------------------------------------------------------------------------ +# ONLINE RECOVERY +#------------------------------------------------------------------------------ + +auto_failback = on diff --git a/pgpool2/pgpool2/pool_hba.conf b/pgpool2/pgpool2/pool_hba.conf new file mode 100644 index 0000000..6a5c547 --- /dev/null +++ b/pgpool2/pgpool2/pool_hba.conf @@ -0,0 +1,9 @@ +# https://www.pgpool.net/docs/latest/en/html/auth-pool-hba-conf.html + +# "local" is for Unix domain socket connections only +local all all trust +# IPv4 local connections: +host all all 127.0.0.1/32 trust +host all all ::1/128 trust + +host all all all scram-sha-256 diff --git a/pgpool2/pgpool2/pool_passwd.tpl b/pgpool2/pgpool2/pool_passwd.tpl new file mode 100644 index 0000000..4a9e930 --- /dev/null +++ b/pgpool2/pgpool2/pool_passwd.tpl @@ -0,0 +1 @@ +postgres:TEXT${POSTGRES_PASSWORD} diff --git a/Dockerfile b/postgres/Dockerfile similarity index 100% rename from Dockerfile rename to postgres/Dockerfile