Feb 26, 2026
Feb 26, 2026
N/A Views
MD

VMware Tanzu for Postgres 18.2をRocky Linux 9上にPatroniによるHA構成でインストールするメモです。

本記事ではOrbStackのLinux Machineを使用して、以下の構成を構築します。HAProxy以外は公式ドキュメントの推奨構成です。

  • Etcdクラスター: 3ノード (etcd-1, etcd-2, etcd-3)
  • PostgreSQL + Patroniクラスター: 3ノード (postgres-1, postgres-2, postgres-3)
  • HAProxyロードバランサー: 1ノード (haproxy)

OrbStackの仕様上、Firewallは無効化されている前提で進めます。必要に応じて適宜ポートを開放してください。

目次

VMware Tanzu for Postgresのダウンロード

Broadcom Supportにログインして、VMware Tanzu for Postgresのダウンロードページにアクセスします。

最新バージョンを選択します。

image

"I agree to the Terms and Conditions"にチェックを入れて、

image

EL9用のインストーラーを~/Downloadsにダウンロードします。

image

cd ~/Downloads
mkdir -p vmware-postgres-18.2.0
unzip -d vmware-postgres-18.2.0 vmware-postgres-18.2.0.el9.x86_64.zip

Etcd クラスターの構築

PatroniはDCS(Distributed Configuration Store)としてEtcdを使用し、クラスターのリーダー選出や構成管理を行います。ここでは3ノードのEtcdクラスターを構築します。

まず、1台目のEtcd用のLinux Machineを作成してシェルに入ります。

orb create -a amd64 rocky:9 etcd-1
orb shell -m etcd-1

Etcd用のシステムユーザーとグループを作成し、データディレクトリを準備します。

sudo groupadd --system etcd
sudo useradd -s /sbin/nologin --system -g etcd etcd
sudo mkdir -p /var/lib/etcd /etc/etcd
sudo chown -R etcd:etcd /var/lib/etcd
sudo chmod 750 /var/lib/etcd

VMware Tanzu for Postgresに同梱されているEtcdのRPMパッケージをインストールします。OrbStackのLinux Machineではホストの/Usersがマウントされているため、ホスト側でダウンロードしたファイルにそのままアクセスできます。

cd /Users/$USER/Downloads/vmware-postgres-18.2.0
sudo dnf install -y vmware-postgres-etcd-3.6.7-2.el9.x86_64.rpm

Etcdをsystemdサービスとして管理するためのユニットファイルを作成します。

sudo tee /etc/systemd/system/etcd.service <<'EOF'
[Unit]
Description=etcd distributed reliable key-value store
After=network-online.target
Wants=network-online.target

[Service]
Type=notify
User=etcd
Group=etcd
ExecStart=/opt/vmware/etcd/bin/etcd --config-file /etc/etcd/etcd.yaml
Restart=on-failure
RestartSec=10s
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl daemon-reload

1台目の設定が完了したので、シェルを抜けます。

exit

orb cloneで1台目のマシンをクローンして、残り2台のEtcdノードを作成します。

orb clone etcd-1 etcd-2
orb clone etcd-1 etcd-3

クローンしたマシンを起動します。

orb start etcd-2
orb start etcd-3

各ノードのIPアドレスを確認します。このIPアドレスは後ほどEtcdの設定ファイルで使用します。

$ orb list        
NAME    STATE    DISTRO      VERSION  ARCH   SIZE  IP
----    -----    ------      -------  ----   ----  --
etcd-1  running  rockylinux  9        amd64        192.168.139.78
etcd-2  running  rockylinux  9        amd64        192.168.139.203
etcd-3  running  rockylinux  9        amd64        192.168.139.214

generate_etcd_configs.sh を作成して、各ノードのetcd設定ファイルを自動生成します。

#!/bin/bash

# ノード定義
hosts=("etcd-1" "etcd-2" "etcd-3")
addrs=("192.168.139.78" "192.168.139.203" "192.168.139.214")

count=${#hosts[@]}

for i in "${!hosts[@]}"; do
  instance_hostname="${hosts[$i]}"
  instance_address="${addrs[$i]}"

  # 残り2ノードをa, bとして取得
  others=()
  for j in "${!hosts[@]}"; do
    [[ $i -ne $j ]] && others+=($j)
  done

  etcd_hostname_a="${hosts[${others[0]}]}"
  etcd_address_a="${addrs[${others[0]}]}"
  etcd_hostname_b="${hosts[${others[1]}]}"
  etcd_address_b="${addrs[${others[1]}]}"

  cat > "${instance_hostname}.yaml" << EOF
name: '${instance_hostname}'
data-dir: /var/lib/etcd
listen-peer-urls: 'http://${instance_address}:2380'
listen-client-urls: 'http://${instance_address}:2379,http://127.0.0.1:2379'
initial-advertise-peer-urls: 'http://${instance_address}:2380'
advertise-client-urls: 'http://${instance_address}:2379'

initial-cluster: '${instance_hostname}=http://${instance_address}:2380,${etcd_hostname_a}=http://${etcd_address_a}:2380,${etcd_hostname_b}=http://${etcd_address_b}:2380'
initial-cluster-state: 'new'
initial-cluster-token: 'etcd-cluster'
EOF

  echo "Generated: ${instance_hostname}.yaml"
done

スクリプトを実行して設定ファイルを生成します。

chmod +x generate_etcd_configs.sh
./generate_etcd_configs.sh

生成された設定ファイルを各ノードに配布します。

for i in $(seq 1 3); do
  orb push etcd-${i}.yaml -m etcd-${i} /tmp/
  orb shell -m etcd-${i} sudo mv /tmp/etcd-${i}.yaml /etc/etcd/etcd.yaml
  orb shell -m etcd-${i} sudo chown etcd:etcd /etc/etcd/etcd.yaml
done

全ノードでEtcdサービスを起動します。

for i in $(seq 1 3); do
  orb shell -m etcd-${i} sudo systemctl enable etcd
  orb shell -m etcd-${i} sudo systemctl start etcd
done

各ノードでEtcdサービスが正常に起動していることを確認します。

for i in $(seq 1 3); do
  orb shell -m etcd-${i} sudo systemctl status etcd -n 0
done
● etcd.service - etcd distributed reliable key-value store
     Loaded: loaded (/etc/systemd/system/etcd.service; enabled; preset: disabled)
    Drop-In: /run/systemd/system/service.d
             └─zzz-lxc-service.conf
     Active: active (running) since Thu 2026-02-26 16:37:52 JST; 1min 40s ago
   Main PID: 777 (etcd)
      Tasks: 18 (limit: 617192)
     Memory: 83.9M (peak: 86.0M)
        CPU: 2.247s
     CGroup: /system.slice/etcd.service
             └─777 /opt/vmware/etcd/bin/etcd --config-file /etc/etcd/etcd.yaml
● etcd.service - etcd distributed reliable key-value store
     Loaded: loaded (/etc/systemd/system/etcd.service; enabled; preset: disabled)
    Drop-In: /run/systemd/system/service.d
             └─zzz-lxc-service.conf
     Active: active (running) since Thu 2026-02-26 16:37:52 JST; 1min 40s ago
   Main PID: 430 (etcd)
      Tasks: 19 (limit: 617192)
     Memory: 61.1M (peak: 62.2M)
        CPU: 2.081s
     CGroup: /system.slice/etcd.service
             └─430 /opt/vmware/etcd/bin/etcd --config-file /etc/etcd/etcd.yaml
● etcd.service - etcd distributed reliable key-value store
     Loaded: loaded (/etc/systemd/system/etcd.service; enabled; preset: disabled)
    Drop-In: /run/systemd/system/service.d
             └─zzz-lxc-service.conf
     Active: active (running) since Thu 2026-02-26 16:37:52 JST; 1min 40s ago
   Main PID: 432 (etcd)
      Tasks: 18 (limit: 617192)
     Memory: 64.4M (peak: 65.4M)
        CPU: 2.675s
     CGroup: /system.slice/etcd.service
             └─432 /opt/vmware/etcd/bin/etcd --config-file /etc/etcd/etcd.yaml

Etcdノードの1つにログインして、クラスターの状態を確認します。

orb shell -m etcd-1

etcdctlコマンドでクラスター内の全エンドポイントのヘルスチェックを行います。

/opt/vmware/etcd/bin/etcdctl endpoint health --cluster -w table
+-----------------------------+--------+-------------+-------+
|          ENDPOINT           | HEALTH |    TOOK     | ERROR |
+-----------------------------+--------+-------------+-------+
|  http://192.168.139.78:2379 |   true |  6.363441ms |       |
| http://192.168.139.214:2379 |   true | 26.989624ms |       |
| http://192.168.139.203:2379 |   true | 28.053309ms |       |
+-----------------------------+--------+-------------+-------+

各ノードのステータス(バージョン、DB サイズ、リーダー情報など)を確認します。

/opt/vmware/etcd/bin/etcdctl endpoint status --cluster -w table
+-----------------------------+------------------+---------+-----------------+---------+--------+-----------------------+--------+-----------+------------+-----------+------------+--------------------+--------+--------------------------+-------------------+
|          ENDPOINT           |        ID        | VERSION | STORAGE VERSION | DB SIZE | IN USE | PERCENTAGE NOT IN USE | QUOTA  | IS LEADER | IS LEARNER | RAFT TERM | RAFT INDEX | RAFT APPLIED INDEX | ERRORS | DOWNGRADE TARGET VERSION | DOWNGRADE ENABLED |
+-----------------------------+------------------+---------+-----------------+---------+--------+-----------------------+--------+-----------+------------+-----------+------------+--------------------+--------+--------------------------+-------------------+
| http://192.168.139.203:2379 | 19506a455bdfcf4f |   3.6.7 |           3.6.0 |   20 kB |  16 kB |                   20% | 2.1 GB |     false |      false |         2 |         11 |                 11 |        |                          |             false |
|  http://192.168.139.78:2379 | 9b774fa21f98412c |   3.6.7 |           3.6.0 |   20 kB |  16 kB |                   20% | 2.1 GB |     false |      false |         2 |         11 |                 11 |        |                          |             false |
| http://192.168.139.214:2379 | a4d6bd99f689a01b |   3.6.7 |           3.6.0 |   20 kB |  16 kB |                   20% | 2.1 GB |      true |      false |         2 |         11 |                 11 |        |                          |             false |
+-----------------------------+------------------+---------+-----------------+---------+--------+-----------------------+--------+-----------+------------+-----------+------------+--------------------+--------+--------------------------+-------------------+

3ノード全てがHEALTH: trueであれば、Etcdクラスターは正常に稼働しています。

Postgres / Patroni クラスターの構築

続いて、PostgreSQLとPatroniをインストールしてHA構成のPostgreSQLクラスターを構築します。

まず、1台目のPostgres用のLinux Machineを作成してシェルに入ります。

orb create -a amd64 rocky:9 postgres-1
orb shell -m postgres-1

ドキュメントには書いてありませんでしたが、VMware Tanzu for Postgresのインストールにはlibssh2パッケージが必要でした。
libssh2パッケージをインストールするにはEPEL(Extra Packages for Enterprise Linux)リポジトリを有効化する必要があります。

sudo dnf install epel-release -y

メタデータを更新します。

sudo dnf clean all
sudo dnf makecache

libssh2(その他、あると便利な)パッケージをインストールします。

sudo dnf install libssh2 which lsof vim bind-utils -y

VMware Tanzu for Postgres 18のRPMパッケージをインストールします。

cd /Users/$USER/Downloads/vmware-postgres-18.2.0
sudo dnf install ./vmware-postgres18-18.2-1.el9.x86_64.rpm ./vmware-postgres18-libs-18.2-1.el9.x86_64.rpm -y

PatroniはPythonで実装されているため、Python3とpipをインストールします。

sudo dnf install -y python3 python3-pip

Patroniが必要とするPythonライブラリの一覧をrequirements.txtに記載します。

cat <<EOF > /tmp/requirements.txt
PyYAML
click>=4.1
prettytable>=0.7
psutil>=2.0.0
python-dateutil
python-etcd>=0.4.3,<0.5
requests
six >= 1.7
urllib3>=1.19.1,!=1.21
ydiff>=1.2.0
cdiff>=1.0
EOF

postgresユーザーとしてPythonライブラリをインストールします。

sudo -u postgres pip3 install --user -r /tmp/requirements.txt

PatroniのRPMパッケージをインストールします。

cd /Users/$USER/Downloads/vmware-postgres-18.2.0
sudo dnf install ./vmware-postgres-patroni-4.1.0-4.el9.x86_64.rpm -y

Patroniがクラスターを初期化した直後に実行されるブートストラップスクリプトを作成します。このスクリプトでは、postgresユーザーのパスワード設定、レプリケーション用ユーザー(replicator)の作成、およびpg_rewind用ユーザー(rewind_user)の作成と必要な権限付与を行います。パスワードは適宜変更してください。

sudo -u postgres tee /etc/patroni/post_bootstrap.sh <<'EOF'
#!/bin/bash
/opt/vmware/postgres/18/bin/psql -U postgres <<SQL
ALTER USER postgres WITH PASSWORD 'postgres';
CREATE USER replicator WITH REPLICATION PASSWORD 'rep-pass';
CREATE USER rewind_user WITH PASSWORD 'rewind_password';
GRANT EXECUTE ON FUNCTION pg_catalog.pg_ls_dir(text, boolean, boolean) TO rewind_user;
GRANT EXECUTE ON FUNCTION pg_catalog.pg_stat_file(text, boolean) TO rewind_user;
GRANT EXECUTE ON FUNCTION pg_catalog.pg_read_binary_file(text) TO rewind_user;
GRANT EXECUTE ON FUNCTION pg_catalog.pg_read_binary_file(text, bigint, bigint, boolean) TO rewind_user;
SQL
EOF

sudo chmod +x /etc/patroni/post_bootstrap.sh

Patroniをsystemdサービスとして管理するためのユニットファイルを作成します。

sudo tee /etc/systemd/system/patroni.service <<'EOF'
[Unit]
Description=Patroni - HA solution for PostgreSQL
After=syslog.target network.target etcd.service

[Service]
Type=simple
User=postgres
Group=postgres
ExecStart=/opt/vmware/patroni-python3/bin/patroni /etc/patroni/patroni.yaml
ExecReload=/bin/kill -s HUP $MAINPID
KillMode=process
KillSignal=SIGINT
TimeoutSec=30
Restart=on-failure
RestartSec=30s
LimitNOFILE=65536

[Install]
WantedBy=multi-user.target
EOF

1台目の設定が完了したので、シェルを抜けます。

exit

Etcdと同様に、orb cloneで残り2台のPostgresノードを作成します。

orb clone postgres-1 postgres-2
orb clone postgres-1 postgres-3

クローンしたマシンを起動します。

orb start postgres-2
orb start postgres-3

各ノードのIPアドレスを確認します。このIPアドレスはPatroniおよびHAProxyの設定で使用します。

$ orb list
NAME        STATE    DISTRO      VERSION  ARCH   SIZE  IP
----        -----    ------      -------  ----   ----  --
etcd-1      running  rockylinux  9        amd64        192.168.139.78
etcd-2      running  rockylinux  9        amd64        192.168.139.203
etcd-3      running  rockylinux  9        amd64        192.168.139.214
postgres-1  running  rockylinux  9        amd64        192.168.139.134
postgres-2  running  rockylinux  9        amd64        192.168.139.155
postgres-3  running  rockylinux  9        amd64        192.168.139.141

Etcdの設定ファイルと同様に、generate_patroni_configs.shを作成して各ノードのPatroni設定ファイルを自動生成します。設定ファイルでは、Etcdクラスターのアドレス、PostgreSQLのデータディレクトリ、レプリケーションやpg_rewindの認証情報などを定義します。パスワードはpost_bootstrap.shで設定したものと合わせてください。

hosts=("postgres-1" "postgres-2" "postgres-3")
addrs=("192.168.139.134" "192.168.139.155" "192.168.139.141")
etcd_addrs=("192.168.139.78" "192.168.139.203" "192.168.139.214")
data_host_CIDR="192.168.139.0/24"

etcd_hosts=$(IFS=,; echo "${etcd_addrs[*]/%/:2379}")

for i in "${!hosts[@]}"; do
  instance_name="${hosts[$i]}"
  instance_address="${addrs[$i]}"

  cat > "patroni-${instance_name}.yaml" <<EOF
scope: patroni_cluster
name: $instance_name
restapi:
  listen: '$instance_address:8008'
  connect_address: '$instance_address:8008'
etcd3:
  hosts: '$etcd_hosts'
bootstrap:
  post_bootstrap: /etc/patroni/post_bootstrap.sh
  dcs:
    ttl: 30
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    postgresql:
      use_pg_rewind: true
      use_slots: true
      parameters:
        wal_level: replica
        hot_standby: 'on'
        wal_log_hints: 'on'
        wal_keep_size: 320
        max_wal_senders: 8
        max_replication_slots: 8
    slots:
      patroni_standby_leader:
        type: physical
  initdb:
  - encoding: UTF8
  - data-checksums
  pg_hba:
  - host replication replicator $data_host_CIDR md5
  - host all all 0.0.0.0/0 md5
postgresql:
  listen: '$instance_address:5432'
  connect_address: '$instance_address:5432'
  data_dir: /var/lib/pgsql/data
  bin_dir: /opt/vmware/postgres/18/bin
  pgpass: /tmp/pgpass0
  authentication:
    replication:
      username: replicator
      password: rep-pass
    superuser:
      username: postgres
      password: postgres
    rewind:
      username: rewind_user
      password: rewind_password
tags:
  nofailover: false
  noloadbalance: false
  clonefrom: false
  nosync: false
EOF

  echo "Generated: patroni-${instance_name}.yaml"
done

スクリプトを実行して設定ファイルを生成します。

chmod +x generate_patroni_configs.sh
./generate_patroni_configs.sh

生成された設定ファイルを各ノードに配布し、Patroniサービスを有効化します。

for i in $(seq 1 3); do
  orb push patroni-postgres-${i}.yaml -m postgres-${i} /tmp/
  orb shell -m postgres-${i} sudo mv /tmp/patroni-postgres-${i}.yaml /etc/patroni/patroni.yaml
  orb shell -m postgres-${i} sudo chown postgres:postgres /etc/patroni/patroni.yaml
done

for i in $(seq 1 3); do
  orb shell -m postgres-${i} sudo systemctl daemon-reload
  orb shell -m postgres-${i} sudo systemctl enable patroni
done

最初にLeaderとなるノード(postgres-1)でPatroniを起動します。最初のノードが起動するとPostgreSQLのデータベースが初期化され、post_bootstrap.shが実行されます。

orb shell -m postgres-1
sudo systemctl start patroni

残りの2台(postgres-2postgres-3)は別のターミナルからそれぞれ起動します。これらのノードはLeaderからベースバックアップを取得してReplicaとして参加します。

postgres-2postgres-3のPatroniサービスも同様に起動してください。

orb shell -m postgres-2
sudo systemctl start patroni
orb shell -m postgres-3
sudo systemctl start patroni

全ノードのPatroniが起動したら、patronictlコマンドでクラスターの状態を確認します。postgres-1がLeader、postgres-2postgres-3がReplicaとしてstreaming状態であれば正常です。

$ patronictl -c /etc/patroni/patroni.yaml list
+ Cluster: patroni_cluster (7611092906447976897) ----+----+-------------+-----+------------+-----+
| Member     | Host            | Role    | State     | TL | Receive LSN | Lag | Replay LSN | Lag |
+------------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
| postgres-1 | 192.168.139.134 | Leader  | running   |  1 |             |     |            |     |
| postgres-2 | 192.168.139.155 | Replica | streaming |  1 |   0/5031A78 |   0 |  0/5031A78 |   0 |
| postgres-3 | 192.168.139.141 | Replica | streaming |  1 |   0/5031A78 |   0 |  0/5031A78 |   0 |
+------------+-----------------+---------+-----------+----+-------------+-----+------------+-----+

PatroniはREST APIを提供しており、各ノードの役割を確認できます。/primaryエンドポイントはLeaderノードで200を返し、それ以外のノードでは503を返します。同様に/replicaエンドポイントはReplicaノードで200を返し、それ以外では503を返します。HAProxyはこのAPIを使ってヘルスチェックを行います。

for ip in 192.168.139.134 192.168.139.155 192.168.139.141;do
  echo "== $ip =="
  echo -n "Primary: "
  curl -s -o /dev/null -w "%{http_code}" http://${ip}:8008/primary
  echo
  echo -n "Replica: "
  curl -s -o /dev/null -w "%{http_code}" http://${ip}:8008/replica
  echo
done

HAProxy の構築

HAProxyをPostgreSQLクラスターの前段に配置し、クライアントからの接続を適切なノードに振り分けます。PatroniのREST APIを利用したヘルスチェックにより、以下のポートで接続を振り分けます。

  • ポート5000: Primary(書き込み可能)ノードへの接続
  • ポート5001: Replicaノードへの接続(読み取り専用、ラウンドロビン)
  • ポート5002: 全ノードへの接続(ラウンドロビン)

HAProxy用のLinux Machineを作成してシェルに入ります。

orb create -a amd64 rocky:9 haproxy
orb shell -m haproxy

VMware Tanzu for Postgresに同梱されているHAProxyのRPMパッケージをインストールします。

cd /Users/$USER/Downloads/vmware-postgres-18.2.0
sudo dnf install ./vmware-postgres-haproxy-3.3.0-2.el9.x86_64.rpm -y

HAProxyの設定ファイルを作成します。各listenセクションでPatroniのREST APIエンドポイントを使ったHTTPヘルスチェックを行い、役割に応じたノードにのみトラフィックを振り分けます。

addrs=("192.168.139.134" "192.168.139.155" "192.168.139.141")

cat << EOF | sudo tee /etc/haproxy/haproxy.cfg > /dev/null 
global
    log         /dev/log local0
    log         /dev/log local1 notice
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon

defaults
    mode                    tcp
    log                     global
    option                  tcplog
    option                  dontlognull
    option                  tcp-check
    retries                 3
    timeout connect         5s
    timeout client          30m
    timeout server          30m
    timeout check           5s
    maxconn                 3000

listen stats
    bind *:7000
    mode http
    stats enable
    stats uri /
    stats refresh 10s
    stats show-node
    stats auth admin:admin

listen primary
    bind *:5000
    option httpchk GET /primary
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
$(for i in "${!addrs[@]}"; do echo "    server postgres-$((i+1)) ${addrs[$i]}:5432 check port 8008"; done)

listen replicas
    bind *:5001
    option httpchk GET /replica
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
$(for i in "${!addrs[@]}"; do echo "    server postgres-$((i+1)) ${addrs[$i]}:5432 check port 8008"; done)

listen all_nodes
    bind *:5002
    balance roundrobin
    option httpchk GET /health
    http-check expect status 200
    default-server inter 3s fall 3 rise 2 on-marked-down shutdown-sessions
$(for i in "${!addrs[@]}"; do echo "    server postgres-$((i+1)) ${addrs[$i]}:5432 check port 8008"; done)
EOF

/opt/vmware/haproxy/sbin/haproxy -c -f /etc/haproxy/haproxy.cfg && echo "Config OK"

HAProxyサービスを有効化して起動します。

sudo systemctl enable haproxy
sudo systemctl start haproxy

ログを確認すると、HAProxyがPatroniのREST APIを使ってヘルスチェックを行い、各ノードの役割に応じて振り分けが正しく機能していることがわかります。Replicaノードはprimaryポートで503を返すためDOWNとなり、LeaderノードはreplicaポートでDOWNとなることが正常な動作です。

journalctl -u haproxy -f
Feb 26 19:20:55 haproxy haproxy[763]: Server primary/postgres-2 is DOWN, reason: Layer7 wrong status, code: 503, info: "Service Unavailable", check duration: 22ms. 2 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.
Feb 26 19:20:55 haproxy haproxy[763]: Server primary/postgres-2 is DOWN, reason: Layer7 wrong status, code: 503, info: "Service Unavailable", check duration: 22ms. 2 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.
Feb 26 19:20:56 haproxy haproxy[763]: Server primary/postgres-3 is DOWN, reason: Layer7 wrong status, code: 503, info: "Service Unavailable", check duration: 21ms. 1 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.
Feb 26 19:20:56 haproxy haproxy[763]: Server primary/postgres-3 is DOWN, reason: Layer7 wrong status, code: 503, info: "Service Unavailable", check duration: 21ms. 1 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.
Feb 26 19:20:56 haproxy haproxy[763]: Server replicas/postgres-1 is DOWN, reason: Layer7 wrong status, code: 503, info: "Service Unavailable", check duration: 6ms. 2 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.
Feb 26 19:20:56 haproxy haproxy[763]: Server replicas/postgres-1 is DOWN, reason: Layer7 wrong status, code: 503, info: "Service Unavailable", check duration: 6ms. 2 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.

全マシンの一覧を確認します。HAProxyのIPアドレス(192.168.139.240)がクライアントからの接続先となります。

$ orb list
NAME        STATE    DISTRO      VERSION  ARCH   SIZE  IP
----        -----    ------      -------  ----   ----  --
etcd-1      running  rockylinux  9        amd64        192.168.139.78
etcd-2      running  rockylinux  9        amd64        192.168.139.203
etcd-3      running  rockylinux  9        amd64        192.168.139.214
haproxy     running  rockylinux  9        amd64        192.168.139.240
postgres-1  running  rockylinux  9        amd64        192.168.139.134
postgres-2  running  rockylinux  9        amd64        192.168.139.155
postgres-3  running  rockylinux  9        amd64        192.168.139.141

HAProxyの統計画面(ポート7000)にブラウザからアクセスすると、各バックエンドの状態を視覚的に確認できます。

image

HAProxy経由でPostgreSQLに接続して動作を確認します。

ポート5000(primary)に接続すると、Leaderノード(192.168.139.134)に接続され、pg_is_in_recovery()f(false)を返します。

$ psql postgresql://postgres:postgres@192.168.139.240:5000 -c "SELECT inet_server_addr(), pg_is_in_recovery()"
 inet_server_addr | pg_is_in_recovery 
------------------+-------------------
 192.168.139.134  | f
(1 row)

ポート5001(replica)に接続すると、Replicaノードにラウンドロビンで接続されます。pg_is_in_recovery()t(true)を返し、読み取り専用であることがわかります。

$ psql postgresql://postgres:postgres@192.168.139.240:5001 -c "SELECT inet_server_addr(), pg_is_in_recovery()"
 inet_server_addr | pg_is_in_recovery 
------------------+-------------------
 192.168.139.155  | t
(1 row)

$ psql postgresql://postgres:postgres@192.168.139.240:5001 -c "SELECT inet_server_addr(), pg_is_in_recovery()"
 inet_server_addr | pg_is_in_recovery 
------------------+-------------------
 192.168.139.141  | t
(1 row)

ポート5002(all_nodes)に接続すると、Leader・Replica問わず全ノードにラウンドロビンで接続されます。

$ psql postgresql://postgres:postgres@192.168.139.240:5002 -c "SELECT inet_server_addr(), pg_is_in_recovery()"
 inet_server_addr | pg_is_in_recovery 
------------------+-------------------
 192.168.139.134  | f
(1 row)

$ psql postgresql://postgres:postgres@192.168.139.240:5002 -c "SELECT inet_server_addr(), pg_is_in_recovery()"
 inet_server_addr | pg_is_in_recovery 
------------------+-------------------
 192.168.139.155  | t
(1 row)

$ psql postgresql://postgres:postgres@192.168.139.240:5002 -c "SELECT inet_server_addr(), pg_is_in_recovery()"
 inet_server_addr | pg_is_in_recovery 
------------------+-------------------
 192.168.139.141  | t

レプリケーションが正しく動作していることを確認するため、Primaryにテーブルを作成してデータを挿入します。

psql postgresql://postgres:postgres@192.168.139.240:5000 -c "
CREATE TABLE IF NOT EXISTS organization
(
    organization_id   BIGINT PRIMARY KEY,
    organization_name VARCHAR(255) NOT NULL
);
INSERT INTO organization(organization_id, organization_name) VALUES(1, 'foo');
INSERT INTO organization(organization_id, organization_name) VALUES(2, 'bar');
"
CREATE TABLE
INSERT 0 1
INSERT 0 1

Replicaポート(5001)経由で書き込みを試みると、読み取り専用トランザクションのため正しくエラーになります。

psql postgresql://postgres:postgres@192.168.139.240:5001 -c "INSERT INTO organization(organization_id, organization_name) VALUES(3, 'baz')"
ERROR:  cannot execute INSERT in a read-only transaction

all_nodesポート(5002)経由でSELECTを実行すると、全ノードにラウンドロビンで接続され、どのノードからもレプリケーションされたデータが読み取れることを確認できます。

$ psql postgresql://postgres:postgres@192.168.139.240:5002 -c "SELECT inet_server_addr(), organization_id,organization_name from organization"      
 inet_server_addr | organization_id | organization_name 
------------------+-----------------+-------------------
 192.168.139.134  |               1 | foo
 192.168.139.134  |               2 | bar
(2 rows)

$ psql postgresql://postgres:postgres@192.168.139.240:5002 -c "SELECT inet_server_addr(), organization_id,organization_name from organization"
 inet_server_addr | organization_id | organization_name 
------------------+-----------------+-------------------
 192.168.139.155  |               1 | foo
 192.168.139.155  |               2 | bar
(2 rows)

$ psql postgresql://postgres:postgres@192.168.139.240:5002 -c "SELECT inet_server_addr(), organization_id,organization_name from organization"
 inet_server_addr | organization_id | organization_name 
------------------+-----------------+-------------------
 192.168.139.141  |               1 | foo
 192.168.139.141  |               2 | bar
(2 rows)

フェイルオーバーのテスト

Patroniのswitchoverコマンドを使って、手動でLeaderを切り替えてフェイルオーバーの動作を確認します。

orb shell -m postgres-1

patronictl switchoverを実行し、Leaderをpostgres-1からpostgres-2に切り替えます。

patronictl -c /etc/patroni/patroni.yaml switchover patroni_cluster

対話的にPrimaryとCandidateを選択して実行します。

Current cluster topology
+ Cluster: patroni_cluster (7611092906447976897) ----+----+-------------+-----+------------+-----+
| Member     | Host            | Role    | State     | TL | Receive LSN | Lag | Replay LSN | Lag |
+------------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
| postgres-1 | 192.168.139.134 | Leader  | running   |  1 |             |     |            |     |
| postgres-2 | 192.168.139.155 | Replica | streaming |  1 |   0/5053F10 |   0 |  0/5053F10 |   0 |
| postgres-3 | 192.168.139.141 | Replica | streaming |  1 |   0/5053F10 |   0 |  0/5053F10 |   0 |
+------------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
Primary [postgres-1]: postgres-1
Candidate ['postgres-2', 'postgres-3'] []: postgres-2
When should the switchover take place (e.g. 2026-02-26T21:09 )  [now]: now 
Are you sure you want to switchover cluster patroni_cluster, demoting current leader postgres-1? [y/N]: y
2026-02-26 20:09:59.02874 Successfully switched over to "postgres-2"
+ Cluster: patroni_cluster (7611092906447976897) --+----+-------------+-----+------------+-----+
| Member     | Host            | Role    | State   | TL | Receive LSN | Lag | Replay LSN | Lag |
+------------+-----------------+---------+---------+----+-------------+-----+------------+-----+
| postgres-1 | 192.168.139.134 | Replica | stopped |    |     unknown |     |    unknown |     |
| postgres-2 | 192.168.139.155 | Leader  | running |  1 |             |     |            |     |
| postgres-3 | 192.168.139.141 | Replica | running |  1 |   0/5054070 |   0 |  0/5054070 |   0 |
+------------+-----------------+---------+---------+----+-------------+-----+------------+-----+

しばらく待ってからクラスターの状態を確認すると、postgres-2がLeaderに昇格し、postgres-1がReplicaに降格していることがわかります。Timeline(TL)も2に進んでいます。

$ patronictl -c /etc/patroni/patroni.yaml list
+ Cluster: patroni_cluster (7611092906447976897) ----+----+-------------+-----+------------+-----+
| Member     | Host            | Role    | State     | TL | Receive LSN | Lag | Replay LSN | Lag |
+------------+-----------------+---------+-----------+----+-------------+-----+------------+-----+
| postgres-1 | 192.168.139.134 | Replica | streaming |  2 |   0/50541B0 |   0 |  0/50541B0 |   0 |
| postgres-2 | 192.168.139.155 | Leader  | running   |  2 |             |     |            |     |
| postgres-3 | 192.168.139.141 | Replica | streaming |  2 |   0/50541B0 |   0 |  0/50541B0 |   0 |
+------------+-----------------+---------+-----------+----+-------------+-----+------------+-----+

HAProxyの統計画面でも、postgres-2がprimaryバックエンドでUP、postgres-1がreplicasバックエンドでUPに変わっていることが確認できます。

image

HAProxy経由の接続もswitchoverに追従しています。ポート5000(primary)では新しいLeaderであるpostgres-2192.168.139.155)に接続され、ポート5001(replica)ではpostgres-1postgres-3に接続されるようになりました。

$ psql postgresql://postgres:postgres@192.168.139.240:5000 -c "SELECT inet_server_addr(), pg_is_in_recovery()"
 inet_server_addr | pg_is_in_recovery 
------------------+-------------------
 192.168.139.155  | f
(1 row)

$ psql postgresql://postgres:postgres@192.168.139.240:5001 -c "SELECT inet_server_addr(), pg_is_in_recovery()"
 inet_server_addr | pg_is_in_recovery 
------------------+-------------------
 192.168.139.141  | t
(1 row)

$ psql postgresql://postgres:postgres@192.168.139.240:5001 -c "SELECT inet_server_addr(), pg_is_in_recovery()"
 inet_server_addr | pg_is_in_recovery 
------------------+-------------------
 192.168.139.134  | t
(1 row)

OrbStackのLinux Machineを使って、VMware Tanzu for Postgres 18.2をPatroniによるHA構成でRocky Linux 9上に構築しました。

patronictl switchoverによるフェイルオーバーでは、Leaderの切り替え後にHAProxyが自動的に新しいLeaderを検出し、クライアントからの接続先が透過的に切り替わることを確認しました。

Found a mistake? Update the entry.
Share this article: