---
title: VMware Tanzu for Postgres 18.2をPatroniによるHA構成でRocky Linuxにインストールするメモ
summary: この記事ではOrbStack上のRocky Linux 9にVMware Tanzu for Postgres 18.2をPatroni HA構成でインストールし、Etcd・HAProxy設定とフェイルオーバー検証まで手順を紹介します。
tags: ["Tanzu", "PostgreSQL", "Rocky", "Patroni", "Etcd", "HAProxy"]
categories: ["Middleware", "RDBMS", "PostgreSQL", "Patroni"]
date: 2026-02-26T11:37:26.067Z
updated: 2026-02-26T11:37:26.066Z
---

[VMware Tanzu for Postgres](https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-postgres/18-2/tnz-postgres/index.html) 18.2をRocky Linux 9上にPatroniによるHA構成でインストールするメモです。

本記事では[OrbStack](https://orbstack.dev/)のLinux Machineを使用して、以下の構成を構築します。HAProxy以外は[公式ドキュメント](https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-postgres/18-2/tnz-postgres/bp-patroni-setup.html)の推奨構成です。

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

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

**目次**
<!-- toc -->

### VMware Tanzu for Postgresのダウンロード

[Broadcom Support](https://support.broadcom.com)にログインして、[VMware Tanzu for Postgresのダウンロードページ](https://support.broadcom.com/group/ecx/productdownloads?subfamily=VMware%20Tanzu%20for%20Postgres)にアクセスします。

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

![image](https://s3.ik.am/ikam/_/1771999989178_pasted-image.png)


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

![image](https://s3.ik.am/ikam/_/1769080281957_pasted-image.png)

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

![image](https://s3.ik.am/ikam/_/1772000093245_pasted-image.png)


```bash
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を作成してシェルに入ります。

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

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

```bash
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`がマウントされているため、ホスト側でダウンロードしたファイルにそのままアクセスできます。

```bash
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サービスとして管理するためのユニットファイルを作成します。

```bash
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台目の設定が完了したので、シェルを抜けます。

```bash
exit
```

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

```bash
orb clone etcd-1 etcd-2
orb clone etcd-1 etcd-3
```

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

```bash
orb start etcd-2
orb start etcd-3
```

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

```bash
$ 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設定ファイルを自動生成します。

```bash
#!/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
```

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

```bash
chmod +x generate_etcd_configs.sh
./generate_etcd_configs.sh
```

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

```bash
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サービスを起動します。

```bash
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サービスが正常に起動していることを確認します。

```bash
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つにログインして、クラスターの状態を確認します。

```bash
orb shell -m etcd-1
```

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

```bash
/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 サイズ、リーダー情報など）を確認します。

```bash
/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を作成してシェルに入ります。

```bash
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）リポジトリを有効化する必要があります。

```bash
sudo dnf install epel-release -y
```

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

```bash
sudo dnf clean all
sudo dnf makecache
```

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

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


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

```bash
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`をインストールします。

```bash
sudo dnf install -y python3 python3-pip
```

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

```bash
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ライブラリをインストールします。

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

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

```bash
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`)の作成と必要な権限付与を行います。パスワードは適宜変更してください。

```bash
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サービスとして管理するためのユニットファイルを作成します。

```bash
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台目の設定が完了したので、シェルを抜けます。

```bash
exit
```

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

```bash
orb clone postgres-1 postgres-2
orb clone postgres-1 postgres-3
```

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

```bash
orb start postgres-2
orb start postgres-3
```

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

```bash
$ 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`で設定したものと合わせてください。

```bash
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
```

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

```bash
chmod +x generate_patroni_configs.sh
./generate_patroni_configs.sh
```

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

```bash
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`が実行されます。

```bash
orb shell -m postgres-1
```

```bash
sudo systemctl start patroni
```

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

`postgres-2`と`postgres-3`のPatroniサービスも同様に起動してください。
```bash
orb shell -m postgres-2
sudo systemctl start patroni
```

```bash
orb shell -m postgres-3
sudo systemctl start patroni
```


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

```bash
$ 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を使ってヘルスチェックを行います。

```bash
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を作成してシェルに入ります。

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

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

```bash
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ヘルスチェックを行い、役割に応じたノードにのみトラフィックを振り分けます。

```bash
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サービスを有効化して起動します。

```bash
sudo systemctl enable haproxy
sudo systemctl start haproxy
```

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

```bash
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`)がクライアントからの接続先となります。

```bash
$ 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](https://s3.ik.am/ikam/_/1772101442648_pasted-image.png)

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

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

```bash
$ 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）を返し、読み取り専用であることがわかります。

```bash
$ 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問わず全ノードにラウンドロビンで接続されます。

```bash
$ 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にテーブルを作成してデータを挿入します。

```bash
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）経由で書き込みを試みると、読み取り専用トランザクションのため正しくエラーになります。

```bash
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`を実行すると、全ノードにラウンドロビンで接続され、どのノードからもレプリケーションされたデータが読み取れることを確認できます。

```bash
$ 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を切り替えてフェイルオーバーの動作を確認します。

```bash
orb shell -m postgres-1
```

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

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

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

```bash
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`に進んでいます。

```bash
$ 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](https://s3.ik.am/ikam/_/1772104269742_pasted-image.png)

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

```bash
$ 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を検出し、クライアントからの接続先が透過的に切り替わることを確認しました。
