タケユー・ウェブ日報

Webシステム受託会社の業務の中での気づきや調べごとのメモ。

DockerCompose + PostgreSQL Replication

  • Railsの複数データベース機能を開発環境でも有効にしたかった
  • docker-compose up でセットアップから起動まで一発で動くようにしたい

Docker Compose

docker-compose.yml

version: "3"
volumes:
  pg_primary_data:
    driver: local
  pg_readonly_data:
    driver: local
services:
  pg_primary:
    build:
      context: ./docker/pg
      dockerfile: Dockerfile.primary
    command: postgres -c log_destination=stderr -c log_statement=all
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - pg_primary_data:/var/lib/postgresql/data
  pg_readonly:
    build:
      context: ./docker/pg
      dockerfile: Dockerfile.readonly
    command: postgres -c log_destination=stderr -c log_statement=all
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    volumes:
      - pg_readonly_data:/var/lib/postgresql/data
    depends_on:
      - pg_primary

docker/pg/Dockerfile.primary

FROM postgres:11-alpine

ENV LANG C.UTF-8

COPY ./setup-primary.sh /docker-entrypoint-initdb.d/setup-primary.sh
RUN chmod 0666 /docker-entrypoint-initdb.d/setup-primary.sh

docker/pg/setup-primary.sh

#!/bin/bash
set -e

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE ROLE replication_user LOGIN REPLICATION PASSWORD 'replicationpassword';
EOSQL

psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
SELECT * FROM pg_create_physical_replication_slot('node_a_slot');
EOSQL

mkdir $PGDATA/archive

cat >> "$PGDATA/postgresql.conf" <<EOF
wal_level = hot_standby
max_wal_senders = 10
max_replication_slots = 10
synchronous_commit = off
EOF

echo "host replication replication_user 0.0.0.0/0 md5" >> "$PGDATA/pg_hba.conf"

docker/pg/Dockerfile.readonly

FROM postgres:11-alpine

ENV LANG C.UTF-8
ENV ENTRYKIT_VERSION 0.4.0

RUN wget https://github.com/progrium/entrykit/releases/download/v${ENTRYKIT_VERSION}/entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && tar -xvzf entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && rm entrykit_${ENTRYKIT_VERSION}_Linux_x86_64.tgz \
  && mv entrykit /bin/entrykit \
  && chmod +x /bin/entrykit \
  && entrykit --symlink

# ログ転送にscpを使用する
RUN apk --update add openssh-client && rm -rf /var/cache/apk/*

COPY ./setup-readonly.sh /setup-readonly.sh
RUN chmod +x /setup-readonly.sh

# https://github.com/docker-library/postgres/blob/primary/11/Dockerfile
ENTRYPOINT [ \
    "prehook", \
        "/setup-readonly.sh", \
        "--", \
    "docker-entrypoint.sh" \
]
CMD ["postgres"]

docker/pg/setup-primary.sh

#!/bin/bash
set -e

if [ ! -s "$PGDATA/PG_VERSION" ]; then
    echo "*:*:*:replication_user:replicationpassword" > ~/.pgpass
    chmod 0600 ~/.pgpass
    until ping -c 1 -W 1 pg_primary
    do
        echo "Waiting for primary to ping..."
        sleep 1s
    done

    until pg_basebackup -h pg_primary -D ${PGDATA} -U replication_user -vP -W
    do
        echo "Waiting for primary to connect..."
        sleep 1s
    done

    sed -i 's/wal_level = hot_standby/wal_level = replica/g' ${PGDATA}/postgresql.conf

    cat > ${PGDATA}/recovery.conf <<EOF
standby_mode = on
primary_conninfo = 'host=pg_primary port=5432 user=replication_user password=replicationpassword application_name=pg_readonly'
primary_slot_name = 'node_a_slot'
EOF

    chown postgres:postgres ${PGDATA} -R
    chmod 700 ${PGDATA} -R
fi

Rails で使う

https://github.com/takeyuweb/rails6-multidb-sample

docker-compose.yml

  app:
    build:
      context: ./docker/app
      dockerfile: Dockerfile.dev
    environment:
      DATABASE_HOST_PRIMARY: pg_primary
      DATABASE_HOST_READONLY: pg_readonly
      DATABASE_USER: postgres
      DATABASE_PASSWORD: password
    user: ruby
    command: ["bundle", "exec", "rails", "s", "-p", "3000", "-b", "0.0.0.0"]
    volumes:
      - .:/src
    ports:
      - 3000:3000
    tty: true
    stdin_open: true
    depends_on:
      - pg_primary
      - pg_readonly

config/database.yml

default: &default
  adapter: postgresql
  encoding: unicode
  # For details on connection pooling, see Rails configuration guide
  # https://guides.rubyonrails.org/configuring.html#database-pooling
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  # 仮にこれを `hoge:` にすると `schema.rb` は `hoge_schema.rb` になる。
  primary:
    <<: *default
    database: MyApp_development
    host: <%= ENV.fetch('DATABASE_HOST_PRIMARY') { 'db' } %>
    username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
    password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
  primary_readonly:
    <<: *default
    database: MyApp_development
    host: <%= ENV.fetch('DATABASE_HOST_READONLY') { 'db' } %>
    username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
    password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
    replica: true

test:
  primary:
    <<: *default
    database: MyApp_test
    host: <%= ENV.fetch('DATABASE_HOST_PRIMARY') { 'db' } %>
    username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
    password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
  primary_readonly:
    <<: *default
    database: MyApp_test
    host: <%= ENV.fetch('DATABASE_HOST_READONLY') { 'db' } %>
    username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
    password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
    replica: true

app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
  connects_to database: { writing: :primary, reading: :primary_readonly }
end