Кадр из фильма «Рембо IV»
Кадр из фильма «Рембо IV»

Вступление

Если вы работаете с crucial data, то рано или поздно задумаетесь о том, что неплохо бы поднять кластер отказоустойчивости. Даже если основной сервер с базой улетит в глухой нокаут, show must go on, не так ли? При этом мы подразумеваем две вещи:

  • база данных со всей ее структурой нам по-прежнему доступна;  

  • все данные находятся в состоянии максимально близком к тому, которое было на героически павшем лидере.

Patroni лишь одно из решений проблемы. До него я попробовал несколько других сервисов и на Github одного из них (не буду показывать курсором) до сих пор висит без ответа открытый мной баг репорт, хотя прошло уже несколько месяцев. У другого была не самая полная документация. Какие-то мне показались недостаточно надежными.

По Patroni же действительно много инфы, и он качественно поддерживается разработчиками. Также он использует DCS для синхронизации нод, что должно предотвращать ситуации со «split brain».

Из минусов совершенно точно нужно назвать то, что это не «out of the box solution. Как сказано в доке: 

“Patroni is a template for you to create your own customized, high-availability solution using Python...”

И ключевое здесь слово «template». То есть все придется собирать самому. Но в каком-то смысле это плюс — по крайней мере, мы детально будем знать, что именно идёт в прод.

По специализации, кстати, я не DevOps инженер. Когда появилась необходимость поднять отказоустойчивый кластер мне пришлось собрать все ямы и столбы какие только были на этом пути. Надеюсь, этот туториал поможет вам достичь результата, испытав в разы меньше страданий и боли, чем довелось мне.  

Если вы устали уже от вступлений и рветесь в бой, смело переходите к следующей главе.

Если нет, то предлагаю под спойлером прочесть короткие заметки по конфигурации кластера, которую я выбрал для деплоя.

Что, ещё один туториал о Patroni?

Зачем читать именно этот туториал?

Есть уже немало туториалов, которые рассказывают, как поднять кластер Patroni. Этот затрагивает вопросы деплоя в среде docker swarm и использования Zookeeper в качестве DCS.

Почему Zookeeper?

Я считаю, это один из моментов, которые стоит рассмотреть всерьез, прежде чем выбрать конечный сетап для продакшена. Дело в том, что Patroni использует сторонние сервисы чтобы установить и обслуживать коммуникацию между своими нодами. Их общее название — DCS (Dynamic Configuration Storage).

Если вы уже смотрели какие-то из туториалов о Patroni, то, должно быть, заметили, что самый частый кейс — это когда в качестве DCS используют Etcd кластер.

Интересный момент в работе Etcd кластера заключается в том, что:

Since etcd writes data to disk, its performance strongly depends on disk performance. For this reason, SSD is highly recommended.

(из документации Etcd)

Словом, если у вас нет по SSD диску на каждой машине, где будут работать ноды Etcd, то вы в зоне опасности. Конечно, пока нагрузка небольшая, то ничего критичного происходить не будет, но если это рабочий, нагруженный прод, то очень возможно (и даже вероятно), что вы просто перегрузите Etcd кластер. А это приведет к IO ошибкам при доступе к базе. Звучит скверно? На самом деле так и есть. Ловить такие ошибки на проде очень неприятно.

Здесь нам на помощь и может прийти Zookeeper, который ничего не пишет на диск и хранит все данные в памяти. Такой вариант оптимален в ситуации, когда не на всех серверах есть SSD, зато RAM хватает.

Почему Docker Swarm?

У меня не было выбора, так как одним из ключевых требований было, чтобы кластер был развернут в Swarm’е. Так что, если это и ваш кейс тоже, то вы в правильном месте!

Для тех же, кто открыл пост с желанием потестировать технологию, выбор Docker Swarm’а тоже может быть вполне органичным. Хотя бы по той причине, что вам не придется устанавливать и настраивать никаких сторонних сервисов (ну за исключением самого Docker’а, разумеется) или тянуть на локальную машину ворох непонятных зависимостей. Полагаю, недалеко от истины утверждение, что Docker у нас у всех и так уже настроен везде где только можно, мы все знаем, как он работает, так что почему бы не использовать его.

Потребуется лишь одна команда, чтобы сделать тюнинг Docker’а, который позволит развернуть на локальной машине кластер Patroni на 3 ноды без виртуальных машин, Kubernetes или подобных вещей.

Если вы не хотите копать в сторону каких-то еще инструментов за пределами Docker’а и хотите сделать все чисто и аккуратно, то данный туториал вам более чем подойдет.

А в конце будет небольшой бонус

Во второй части туториала я покажу различные варианты проверки статуса кластера (целых 3), и в конце дам простой скрипт с инструкциями для быстрого теста кластера.

Окей, достаточно разговоров. Давайте перейдем к практике.

Docker Swarm

Для быстрого и приближенного к реальности теста, нам, на самом деле, достаточно одной ноды в Swarm кластере. Поскольку мы можем очень легко поднимать и ронять сервисы, запущенные в Swarm’е, мы сможем имитировать падение сервера с нодой, имея в распоряжении только одну локальную машину.

Я исхожу из предположения, что у вас уже установлен и настроен Docker Engine. В таком случае, нужно только выполнить следующую команду:

docker swarm init
//now check your single-node cluster
docker node ls
ID                            HOSTNAME       STATUS       AVAILABILITY          
a9ej2flnv11ka1hencoc1mer2 *   floitet        Ready          Active       

Одна из важных фич Swarm’а заключается в том, что теперь мы можем использовать не только обычные Docker контейнеры, но и так называемые сервисы. Сервисы — это по сути дела абстракция над контейнерами. Если отталкиваться от аналогии с ООП, то сервис — это класс, а контейнер конкретный объект класса. Параметры и правила сервиса задаются при деплое из yml-файла.

Рекомендую запомнить hostname ноды — потом мы используем его для указания ‘constraint’ в конфигурационном файле.

В целом, это все приготовления в части Docker Swarm’а, которые нужно сделать. Здесь никаких проблем быть не должно. Так что двинемся дальше.

Zookeeper

Прежде чем мы начнем деплой самого Patroni, нам нужно сначала развернуть кластер с DCS (в нашем случае, как мы помним, это Zookeeper). Я взял версию 3.4, и она работает вполне стабильно. Далее идет docker-compose конфиг и некоторые комментарии по моментам, которые, как мне кажется, имеет смысл отдельно упомянуть.

  • docker-compose-zookeeper.yml

docker-compose-zookeeper.yml
version: '3.7'

services:
  zoo1:
    image: zookeeper:3.4
    hostname: zoo1
    ports:
      - 2191:2181
    networks:
      - patroni
    environment:
      ZOO_MY_ID: 1
      ZOO_SERVERS: server.1=0.0.0.0:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.hostname == floitet
      restart_policy:
        condition: any

  zoo2:
    image: zookeeper:3.4
    hostname: zoo2
    networks:
      - patroni
    ports:
      - 2192:2181
    environment:
      ZOO_MY_ID: 2
      ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=0.0.0.0:2888:3888 server.3=zoo3:2888:3888
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.hostname == floitet
      restart_policy:
        condition: any

  zoo3:
    image: zookeeper:3.4
    hostname: zoo3
    networks:
      - patroni
    ports:
      - 2193:2181
    environment:
      ZOO_MY_ID: 3
      ZOO_SERVERS: server.1=zoo1:2888:3888 server.2=zoo2:2888:3888 server.3=0.0.0.0:2888:3888
    deploy:
      replicas: 1
      placement:
        constraints:
          - node.hostname == floitet
      restart_policy:
        condition: any

networks:
  patroni:
    driver: overlay
    attachable: true

Details

Конечно же, важно дать каждой ноде уникальное имя и внешний порт. Hostname лучше ставить одинаковое с именем сервиса.

zoo1:
    image: zookeeper:3.4
    hostname: zoo1
    ports:
      - 2191:2181

Стоит отметить и то, как мы перечисляем host’ы в строке ниже: для первого сервиса server.1 будет привязан к 0.0.0.0, а, например, для zoo2 это уже будет server.2 соответственно и т.д.

ZOO_SERVERS: server.1=0.0.0.0:2888:3888 server.2=zoo2:2888:3888 server.3=zoo3:2888:3888

А таким образом мы контролируем распределение сервисов по нодам. Поскольку нода у нас сейчас только одна, мы спокойно могли бы убрать эти строчки из конфига, но когда серверов будет несколько, можно будет менять node.hostname и тем самым определять на какую ноду пойдет сервис.

     placement:
        constraints:
          - node.hostname == floitet

И последний момент, который мы здесь обсудим, это network. Я намерен деплоить все сервисы Zookeeper’а и все сервисы Patroni в одну сеть с драйвером overlay, чтобы они были изолированы от других сервисов и могли общаться между собой по именам, а не по IP (как это выглядит, будет видно дальше).

networks:
  patroni:
    driver: overlay
// мы должны отметить сеть как attachable  
// чтобы потом можно было присоединять к ней остальные сервисы
    attachable: true

Итак, можно задеплоить Zookeeper:

sudo docker stack deploy --compose-file docker-compose-zookeeper.yml patroni

Теперь нужно проверить, что все работает. Первое что можно сделать это просто посмотреть список сервисов:

sudo docker service ls
gxfj9rs3po7z        patroni_zoo1        replicated          1/1                 zookeeper:3.4         *:2191->2181/tcp
ibp0mevmiflw        patroni_zoo2        replicated          1/1                 zookeeper:3.4         *:2192->2181/tcp
srucfm8jrt57        patroni_zoo3        replicated          1/1                 zookeeper:3.4         *:2193->2181/tcp

И следующим шагом можно сделать пинг сервисов с помощью специальной команды mntr:

echo mntr | nc localhost 2191
// with the output being smth like this
zk_version	3.4.14-4c25d480e66aadd371de8bd2fd8da255ac140bcf, built on 03/06/2019 16:18 GMT
zk_avg_latency	6
zk_max_latency	205
zk_min_latency	0
zk_packets_received	1745
zk_packets_sent	1755
zk_num_alive_connections	3
zk_outstanding_requests	0
zk_server_state	follower
zk_znode_count	16
zk_watch_count	9
zk_ephemerals_count	4
zk_approximate_data_size	1370
zk_open_file_descriptor_count	34
zk_max_file_descriptor_count	1048576
zk_fsync_threshold_exceed_count	0

Также можно проверить логи сервиса, если есть желание:

docker service logs $zookeeper-service-id 
// service-id comes from 'docker service ls' command. 
// in my case it could be 
docker service logs gxfj9rs3po7z

Отлично, вот мы и разобрались с Zookeeper’ом. Теперь можно переходить к самому Patroni.

Patroni

Мы наконец добрались до основной части туториала, где нам предстоит поднимать кластер Patroni. Первое что нужно сделать — это билд кастомного имейджа Patroni, чтобы нам было что деплоить. Мы включим в сборку только самые необходимые вещи, и я постараюсь объяснить все шаги как можно более подробно, чтобы в дальнейшем вам легко было работать с этим образом и апгрейдить его по своему желанию.

Сначала создадим отдельную директорию ’patroni-test’ и перейдем в неё. Для того чтобы успешно сбилдить рабочий имейдж нам понадобится пара дополнительных файлов, с них и начнем.

  • patroni.yml

Это основной конфигурационный файл. Одна из особенностей Patroni, что мы можем задавать параметры для кластера из разных мест и patroni.yml — одно из них. Этот файл мы будем копировать в кастомный имейдж, так что любые изменения, внесенные в него, требуют ребилда образа.

Я в итоге пришел к идее, что буду хранить здесь только те параметры, которые наверняка почти никогда не буду трогать, поэтому складываю здесь только самые необходимые настройки. Ниже я привожу базовый конфиг. Если захочется, то можно добавить в него каких-то параметров, допустим, для движка Posgtres’а (например, max_connections и т.п.). Но для тестового стенда этого вполне достаточно.

patroni.yml
scope: patroni
namespace: /service/

bootstrap:
    dcs:
        ttl: 30
        loop_wait: 10
        retry_timeout: 10
        maximum_lag_on_failover: 1048576
        postgresql:
            use_pg_rewind: true

    postgresql:
      use_pg_rewind: true

    initdb:
    - encoding: UTF8
    - data-checksums

    pg_hba:
    - host replication all all md5
    - host all all all md5

zookeeper:
  hosts: 
      - zoo1:2181
      - zoo2:2181
      - zoo3:2181

postgresql:
    data_dir: /data/patroni
    bin_dir: /usr/lib/postgresql/11/bin
    pgpass: /tmp/pgpass
    parameters:
        unix_socket_directories: '.'

tags:
    nofailover: false
    noloadbalance: false
    clonefrom: false
    nosync: false

Details

Важно указать Patroni путь к бинарным файлам Postgres’а. В моем случае, так как я использую Postgres 11, директория выглядит так: ’/usr/lib/postgresql/11/bin’.

В директории, внутри уже созданного контейнера, Patroni будет искать файлы Postgres’а. Без этой настройки скорее всего ничего не взлетит (по крайней мере у меня не взлетело). И также еще есть ’data_dir’ — это место в контейнере, где будут храниться данные. Позже мы сделаем mount этой директории к месту на локальном жестком диске, чтобы не потерять все полимеры, если кластер все же упадет безнадежно. Это добавит нам работы по созданию этих папок локально, но, по-моему, оно того стоит.

postgresql:
    data_dir: /data/patroni
    bin_dir: /usr/lib/postgresql/11/bin

Также я перечисляю все сервера Zookeeper’а в этом конфиг файле, чтобы потом передать информацию о них утилите patronictl. Стоит отметить, что если не указать их в patroni.yml, то мы останемся в итоге с нерабочим patronictl. Как вы видите, перечисляя сервера, я не пишу никакие IP, а использую их имена. Это та самая фича Docker Swarm’а о которой я рассказывал выше.  

zookeeper:
  hosts: 
      - zoo1:2181
      - zoo2:2181
      - zoo3:2181

  • patroni-entrypoint.sh

Из следующего файла подтягивается большая часть настроек в моей конфигурации. Это небольшой скрипт, который будет выполнен, когда контейнер сервиса окажется создан.

patroni-entrypoint.sh
#!/bin/sh

readonly CONTAINER_IP=$(hostname --ip-address)
readonly CONTAINER_API_ADDR="${CONTAINER_IP}:${PATRONI_API_CONNECT_PORT}"
readonly CONTAINER_POSTGRE_ADDR="${CONTAINER_IP}:5432"

export PATRONI_NAME="${PATRONI_NAME:-$(hostname)}"
export PATRONI_RESTAPI_CONNECT_ADDRESS="$CONTAINER_API_ADDR"
export PATRONI_RESTAPI_LISTEN="$CONTAINER_API_ADDR"
export PATRONI_POSTGRESQL_CONNECT_ADDRESS="$CONTAINER_POSTGRE_ADDR"
export PATRONI_POSTGRESQL_LISTEN="$CONTAINER_POSTGRE_ADDR"
export PATRONI_REPLICATION_USERNAME="$REPLICATION_NAME"
export PATRONI_REPLICATION_PASSWORD="$REPLICATION_PASS"
export PATRONI_SUPERUSER_USERNAME="$SU_NAME"
export PATRONI_SUPERUSER_PASSWORD="$SU_PASS"
export PATRONI_approle_PASSWORD="$POSTGRES_APP_ROLE_PASS"
export PATRONI_approle_OPTIONS="${PATRONI_admin_OPTIONS:-createdb, createrole}"

exec /usr/local/bin/patroni /etc/patroni.yml

Details. Важно!

На самом деле, основной смысл вообще делать такой скрипт заключается в том, что мы просто не сможем стартануть сервис с Patroni, не зная IP адрес host’а. И в том случае, когда host’ом оказывается Docker-контейнер, нам как-то нужно сначала узнать какой же IP этот контейнер получил, и только потом мы можем запустить Patroni. Эта потребность закрывается вот здесь:

readonly CONTAINER_IP=$(hostname --ip-address)
readonly CONTAINER_API_ADDR="${CONTAINER_IP}:${PATRONI_API_CONNECT_PORT}"
readonly CONTAINER_POSTGRE_ADDR="${CONTAINER_IP}:5432"
...
export PATRONI_RESTAPI_CONNECT_ADDRESS="$CONTAINER_API_ADDR"
export PATRONI_RESTAPI_LISTEN="$CONTAINER_API_ADDR"
export PATRONI_POSTGRESQL_CONNECT_ADDRESS="$CONTAINER_POSTGRE_ADDR"

Как я уже говорил раньше, параметры конфига Patroni можно передавать разными способами. В этом скрипте мы пользуемся тем, что один из таких способов — это ’Environment configuration’. ’PATRONIRESTAPICONNECTADDRESS’, ’PATRONIRESTAPILISTEN’, ’PATRONIPOSTGRESQLCONNECTADDRESS’ — специальные переменные среды, о которых Patroni знает заранее и которые будут считаны. И кстати, они переписывают локальные настройки из patroni.yml, так что be aware!

И еще момент. Документация Patroni не рекомендует использовать superuser’а для подключения к базе приложений. Т.е. нужно создать отдельного юзера, который мы будем использовать непосредственно для коннекта, а superuser’а и replicator’а трогать не будем совсем. Создать такого юзера можно также через переменную среды. Если хотите, чтобы юзер назывался как-то иначе чем ‘approle’, просто замените в этой строке ‘approle’ на что-то другое.

export PATRONI_approle_PASSWORD="$POSTGRES_APP_ROLE_PASS"
export PATRONI_approle_OPTIONS="${PATRONI_admin_OPTIONS:-createdb, createrole}"

И в последней строчке, когда всё уже готово к старту, мы делаем запуск Patroni сервиса с указанием откуда брать основной конфиг файл:

exec /usr/local/bin/patroni /etc/patroni.yml

  • Dockerfile

Dockerfile я решил сделать настолько простым, насколько это только возможно. Но этого вполне достаточно, чтобы сделать билд рабочего Docker-образа. Давайте глянем, что в нем все-таки происходит.

Dockerfile
FROM postgres:11 

RUN apt-get update -y\ 
    && apt-get install python3 python3-pip -y    && pip3 install --upgrade setuptools    && pip3 install psycopg2-binary     && pip3 install patroni[zookeeper]     && mkdir /data/patroni -p     && chown postgres:postgres /data/patroni     && chmod 700 /data/patroni 

COPY patroni.yml /etc/patroni.yml
COPY patroni-entrypoint.sh ./entrypoint.sh
USER postgres

ENTRYPOINT ["bin/sh", "/entrypoint.sh"]

Details

Одна из главных деталей здесь это директория, которую мы указываем создать внутри контейнера, а также ее владелец и его права. Позже, когда будем деплоить Patroni, нужно будет подобным же образом создать папки на локальной машине, куда мы сможем сделать mount этой директории из контейнера.

// владелец должен быть 'postgres', а mode 700

    mkdir /data/patroni -p     chown postgres:postgres /data/patroni     chmod 700 /data/patroni 
    ...
// устанавливаем в кач-ве активного юзера внутри контейнера 
// юзера postgres
    
    USER postgres

Файлы, которые мы создали, ранее копируются в имейдж в этих строчках:

COPY patroni.yml /etc/patroni.yml
COPY patroni-entrypoint.sh ./entrypoint.sh

И, как я уже упомянул ранее, мы хотим запустить этот скрипт сразу после создания контейнера:

ENTRYPOINT ["bin/sh", "/entrypoint.sh"]

Вот, пожалуй, и вся основная подготовка. Теперь мы готовы создать наш кастомный Patroni имейдж.

docker build -t patroni-test .

Самое время обсудить последний по списку, но не по важности файл для Patroni — compose yml.

  • docker-compose-patroni.yml

Правильно написанный compose файл — важная часть общей картины. Разберем, что нам нужно иметь ввиду, настраивая конфигурацию.

docker-compose-patroni.yml
version: "3.4"

networks:
    patroni_patroni: 
        external: true
services:
    patroni1:
        image: patroni-test
        networks: [ patroni_patroni ]
        ports:
            - 5441:5432
            - 8091:8091
        hostname: patroni1
        volumes:
          - /patroni1:/data/patroni
        environment:
            PATRONI_API_CONNECT_PORT: 8091
            REPLICATION_NAME: replicator 
            REPLICATION_PASS: replpass
            SU_NAME: postgres
            SU_PASS: supass
            POSTGRES_APP_ROLE_PASS: appass
        deploy:
          replicas: 1
          placement:
            constraints: [node.hostname == floitet]

    patroni2:
        image: patroni-test
        networks: [ patroni_patroni ]
        ports:
            - 5442:5432
            - 8092:8091
        hostname: patroni2
        volumes:
          - /patroni2:/data/patroni
        environment:
            PATRONI_API_CONNECT_PORT: 8091
            REPLICATION_NAME: replicator 
            REPLICATION_PASS: replpass
            SU_NAME: postgres
            SU_PASS: supass
            POSTGRES_APP_ROLE_PASS: appass
        deploy:
          replicas: 1
          placement:
            constraints: [node.hostname == floitet]


    patroni3:
        image: patroni-test
        networks: [ patroni_patroni ]
        ports:
            - 5443:5432
            - 8093:8091
        hostname: patroni3
        volumes:
          - /patroni3:/data/patroni
        environment:
            PATRONI_API_CONNECT_PORT: 8091
            REPLICATION_NAME: replicator 
            REPLICATION_PASS: replpass
            SU_NAME: postgres
            SU_PASS: supass
            POSTGRES_APP_ROLE_PASS: appass
        deploy:
          replicas: 1
          placement:
            constraints: [node.hostname == floitet]

Details

Первое, о чем хочется сказать, это момент с external network, о котором говорилось ранее. Мы хотим разместить Patroni сервисы там же, где мы держим и сервисы Zookeeper. Таким образом мы сможем обращаться к сервисам по именам, и все имена: ’zoo1', ’zoo2', ’zoo3', — которые мы перечислили в patroni.yml, задавая сервера Zookeeper’а, будут работать, как надо.

networks:
    patroni_patroni: 
        external: true

Нужно отметить, что у нас будут два end point’а: сама база данных и API. И для того и для другого требуется открыть порты:

ports:
    - 5441:5432
    - 8091:8091
...

environment:
    PATRONI_API_CONNECT_PORT: 8091

// также нужно убедиться, что в PATRONI_API_CONNECT_PORT мы передаем
// тот же самый, который мы открываем для сервиса   

Также, нам, конечно, нужно передать все переменные среды, которые мы заявили в entrypoint скрипте. Но и это еще не всё. Есть вопрос с директорией для mount’а, который мы тоже здесь решаем:

volumes:
   - /patroni3:/data/patroni

Как видно из этой строки, ту директорию ’/data/patroni’, которая была создана в Dockerfile, мы монтируем к локальной директории. Так вот эту локальную директорию нам нужно создать. И не только создать, но и выставить правильного юзера и режим доступа, например так:

sudo mkdir /patroni3
sudo chown 999:999 /patroni3
sudo chmod 700 /patroni3

// 999 это дефолтный uid для юзера postgres  
// эти шаги нужно повторить для каждой ноды Patroni

Мы наконец готовы деплоить Patroni кластер:

sudo docker stack deploy --compose-file docker-compose-patroni.yml patroni

После деплоя в логах сервиса мы должны увидеть что-то в таком ключе:

INFO: Lock owner: patroni3; I am patroni1
INFO: does not have lock
INFO: no action.  i am a secondary and i am following a leader

Было бы печально, если бы мы могли проверить статус кластера и ноды только читая логи. Так что предлагаю коснуться немного способов проверки состояния кластера и начать с самого простого — patronictl. Для этого нужно сначала получить id любого контейнера Patroni:

sudo docker ps
CONTAINER ID        IMAGE                 COMMAND                  CREATED             STATUS              PORTS                          NAMES
a0090ce33a05        patroni-test:latest   "bin/sh /entrypoint.…"   3 hours ago         Up 3 hours          5432/tcp                       patroni_patroni1.1.tgjzpjyuip6ge8szz5lsf8kcq
...

И потом зайти в контейнер с помощью exec команды:

sudo docker exec -ti a0090ce33a05 /bin/bash
// при вызове команды мы должны передать имя кластера
// это параметр 'scope' в patroni.yml ('patroni' в нашем случае)
patronictl list patroni
// и тут ошибка...
Error: 'Can not find suitable configuration of distributed configuration store\nAvailable implementations: exhibitor, kubernetes, zookeeper'

Команда patronictl полагается на patroni.yml, чтобы получить информацию о серверах Zookeeper’а. Он не знает, где мы этот файл положили. Так что явно укажем ему путь:

patronictl -c /etc/patroni.yml list patroni
// and here is the nice output with the current states
+ Cluster: patroni (6893104757524385823) --+----+-----------+
| Member   | Host      | Role    | State   | TL | Lag in MB |
+----------+-----------+---------+---------+----+-----------+
| patroni1 | 10.0.1.93 | Replica | running |  8 |         0 |
| patroni2 | 10.0.1.91 | Replica | running |  8 |         0 |
| patroni3 | 10.0.1.92 | Leader  | running |  8 |           |
+----------+-----------+---------+---------+----+-----------+

PostgreSQL Connection

Готово! Теперь мы можем подключаться к Postgres и что-то туда писать. Но делать это мы сможем только из нашей сети «patroni_patroni». Так что сначала создаем контейнер с подходящим образом, и потом из него уже выполняем команды:

docker run --rm -ti --network=patroni_patroni postgres:11 /bin/bash
// доступ к конкретной ноде
psql --host patroni3 --port 5432 -U approle -d postgres
// доступ к лидеру через haproxy
// так что нужно указать какую-либо через флаг '-d' 

Вот мы и настроили сам кластер Patroni. Но наш сетап был бы неполным, если бы мы на этом и остановились. Есть ещё несколько важных моментов, которые мы должны закрыть, но об этом во второй части.

Все config файлы для первой части можно забрать отсюда.