---
title: Tanzu for Postgres on Kubernetes 4.3をインストールしてSpring Bootアプリからアクセスするメモ
summary: この記事では、Tanzu for Postgres on Kubernetes 4.3のインストール、イメージリロケート、HA構成、サンプルアプリでのダウンタイム削減手順を紹介します。
tags: ["Kubernetes", "Tanzu", "PostgreSQL", "Spring Boot", "Helm", "Patroni"]
categories: ["Dev", "CaaS", "Kubernetes", "TanzuSQL", "PostgreSQL"]
date: 2025-11-29T12:44:31Z
updated: 2025-11-29T12:49:38Z
---

[以前の記事](/entries/844)でTanzu for Postgres on Kubernetes 4.0を試しました。

[Tanzu for Postgres on Kubernetes 4.3](https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-postgres-on-kubernetes/4-3/tnz-postgres-k8s/release-notes.html)がリリースされ、 インストールの方法が若干変わったため、ほぼ同じ内容で再度メモします。

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

### Tanzu for Postgres on Kubernetes Docker Registryのアクセストークン取得

まずはこのHelmチャートが配布されているDocker Registryにアクセスするためのアクセストークンを取得します。

[Broadcom Support](https://support.broadcom.com/)にログインして、"My Downloads"を選択し、"postgres"で検索します。

検索結果から"VMware Tanzu for Postgres on Kubernetes"をクリックします。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/ccf01528-c477-48d7-ae0b-cefb5ab518c1" />

バージョン"4.3.1"の"Token Download"アイコンをクリックします。

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/9636ad9c-5fb5-430c-9325-689d6e66a9d5.png)

次のようなダイアログが現れます。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/1cd02350-5aba-49d7-930e-1a1ac1b8b45c" />

下にスクロールするとRegistryの情報の記載が見えます。

<img width="1024" alt="image" src="https://github.com/user-attachments/assets/a8de247e-22d6-40dc-bcbb-0e640475366e" />

次のコマンドで`tanzu-sql-postgres.packages.broadcom.com`にログインします。認証情報を自分のものに置き換えてください。

```bash
BC_SUPPORT_USERNAME=toshiaki.maki@broadcom.com
BC_SUPPORT_PASSWORD=eyJ2ZXI*************************************
docker login tanzu-sql-postgres.packages.broadcom.com -u ${BC_SUPPORT_USERNAME} -p ${BC_SUPPORT_PASSWORD}
```

これで次のコマンドで`tanzu-sql-postgres.packages.broadcom.com`からHelm Chartをpullできます。

```bash
helm pull oci://tanzu-sql-postgres.packages.broadcom.com/vmware-sql-postgres-operator --version v4.3.1 --untar --untardir /tmp
```

ただし、このトークンは有効期限があり、期限が切れるたびにローテートするのが面倒くさいので、今回はこのRegistryから直接Pullするのではなく、内部ネットワーク内のPrivate Registryである`registry.example.com`にリロケートした上で使用することとします。`registry.example.com`は自分の環境に変えてください。

> [!WARNING]
> ライセンスの期限を超えて、このソフトウェアを使用することは禁止されています。リロケートした後も、ライセンス条件に従って使用してください。

### Helm ChartとコンテナイメージをPrivate Registryへリロケート

次のコマンドで`registry.example.com`にログインします。認証情報を自分のものに置き換えてください。`registry.example.com`以外のRegistryでも良いです。

```bash
REGISTRY_USERNAME=username
REGISTRY_PASSWORD=changeme
docker login registry.example.com -u ${REGISTRY_USERNAME} -p ${REGISTRY_PASSWORD}
```

リロケートには[imgpkg](https://carvel.dev/imgpkg/)を使用します。

```bash
imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/vmware-sql-postgres-operator:v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/vmware-sql-postgres-operator
```

リロケート後のHelm Chartにアクセスして設定可能なvaluesを確認します。

```yaml
$ helm show values oci://registry.example.com/tanzu-sql-postgres/vmware-sql-postgres-operator --version v4.3.1
Pulled: registry.example.com/tanzu-sql-postgres/vmware-sql-postgres-operator:v4.3.1
Digest: sha256:20cd8e22a7d0c903e608c3b21f9b344f81f19730047372e7bc499f5f0425b638
---
# specify the url for the docker image for the operator, e.g. gcr.io/<my_project>/postgres-operator
operatorImage: tanzu-sql-postgres.packages.broadcom.com/postgres-operator:v4.3.1

# specify the docker image repository for postgres instance, e.g. gcr.io/<my_project>/postgres-instance
instanceRegistryRepo: tanzu-sql-postgres.packages.broadcom.com/postgres-instance

# specify the docker image repository for ferretdb instances (optional - only needed if deploying FerretDB instances)
# e.g. gcr.io/<my_project>/ferret-db
ferretDBImageRepo: ""

# specify the name of the docker-registry secret to allow the cluster to authenticate with the container registry for pulling images
dockerRegistrySecretName: regsecret

# override the default self-signed cert-manager cluster issuer
certManagerClusterIssuerName: postgres-operator-ca-certificate-cluster-issuer

# override the namespace of the cert-manager installation namespace
certManagerNamespace: cert-manager

# set the resources for the postgres operator deployment
resources: { }
#  limits:
#    cpu: 500m
#    memory: 300Mi
#  requests:
#    cpu: 500m
#    memory: 300Mi

# enabled security context for the postgres-operator deployment and the managed instances, typically disabled on OpenShift clusters
enableSecurityContext: true
```

OperatorのImageとPostgres InstanceのImageも`tanzu-sql-postgres.packages.broadcom.com`から`registry.example.com`にリロケートします。
4.3から、バージョン毎にPostgresインスタンス用のコンテナイメージが用意されるようになったため、必要なバージョン分をリロケートします。各バージョン毎に、full版とlite版のイメージがあります。

> [!NOTE]
> full版とlite版ではインストールされているextensionに違いがあります。違いは[こちらのドキュメント](https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-postgres-on-kubernetes/4-3/tnz-postgres-k8s/supported_extension_v17.html)を参照してください。

次のコマンドでイメージをリロケートします。ここでは最新のPostgres 17.5のfull版とlite版をリロケートしています。

```bash
imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/postgres-operator:v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/postgres-operator
imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/postgres-instance:v17.5-v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/postgres-instance
imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/postgres-instance:v17.5-lite-v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/postgres-instance
```

他にサポートされているバージョンは[リリースノート](https://techdocs.broadcom.com/us/en/vmware-tanzu/data-solutions/tanzu-for-postgres-on-kubernetes/4-3/tnz-postgres-k8s/release-notes.html)に記載されています。
必要であれば、次のようにまとめてリロケートします。

```bash
for ver in 17.4 16.9 16.8 15.13 15.12 14.18 14.17 13.21 13.20; do
  imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/postgres-instance:v${ver}-v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/postgres-instance
  imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/postgres-instance:v${ver}-lite-v4.3.1 --to-repo registry.example.com/tanzu-sql-postgres/postgres-instance
done
```

本記事では扱いませんが、FerretDBも使えるようになったので、FerretDB用のコンテナイメージもリロケートしておきます。

```bash
imgpkg copy -i tanzu-sql-postgres.packages.broadcom.com/ferretdb:v2.3.1 --to-repo registry.example.com/tanzu-sql-postgres/ferretdb
```

### Postgres Operatorのインストール

ここでは`postgres-system` namespaceにPostgres Operatorをインストールします。
Docker Pull Secretの名前はデフォルトで`regsecret`です。

次のコマンドでnamespaceとsecretを作成します。

```bash
kubectl create namespace postgres-system
kubectl create secret docker-registry regsecret \
    --docker-server=registry.example.com \
    --docker-username=${REGISTRY_USERNAME} \
    --docker-password="${REGISTRY_PASSWORD}" \
    -n  postgres-system
```

Postgres Operatorの前提条件として[cert-manager](https://github.com/cert-manager)が必要なので、cert-managerが未インストールの場合は次のコマンドでインストールします。

```bash
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.19.1/cert-manager.yaml
```

次のコマンドでPostgres Operatorをインストールします。リロケート後のChartとコンテナイメージを指定します。

```bash
helm upgrade --install \
  -n postgres-system \
  postgres-operator \
  oci://registry.example.com/tanzu-sql-postgres/vmware-sql-postgres-operator \
  --version v4.3.1 \
  --set operatorImage=registry.example.com/tanzu-sql-postgres/postgres-operator:v4.3.1 \
  --set instanceRegistryRepo=registry.example.com/tanzu-sql-postgres/postgres-instance \
  --wait
```

しばらくするとインストールが完了します。`helm list`でインストール状態を確認できます。

```bash
$ helm list -n postgres-system
NAME             	NAMESPACE      	REVISION	UPDATED                             	STATUS  	CHART                              	APP VERSION
postgres-operator	postgres-system	1       	2025-11-28 11:25:22.277037 +0900 JST	deployed	vmware-sql-postgres-operator-v4.3.1	latest  
```

次のコマンドで、Postgres OperatorのPodの状態を確認します。

```bash
$ kubectl get pod -n postgres-system 
NAME                                 READY   STATUS    RESTARTS   AGE
postgres-operator-559d4b6478-qs6rz   1/1     Running   0          35s
```

次のコマンドで、このPostgres Operatorで利用可能なカスタムリソースを確認します。

```bash
$ kubectl api-resources --api-group=sql.tanzu.vmware.com
NAME                        SHORTNAMES   APIVERSION                NAMESPACED   KIND
postgres                    pg           sql.tanzu.vmware.com/v1   true         Postgres
postgresbackuplocations     pgbkpl       sql.tanzu.vmware.com/v1   true         PostgresBackupLocation
postgresbackups             pgbkp        sql.tanzu.vmware.com/v1   true         PostgresBackup
postgresbackupschedules                  sql.tanzu.vmware.com/v1   true         PostgresBackupSchedule
postgresferretdocumentdbs                sql.tanzu.vmware.com/v1   true         PostgresFerretDocumentDB
postgresmigrations          pgmigr       sql.tanzu.vmware.com/v1   true         PostgresMigration
postgresrestores            pgrstr       sql.tanzu.vmware.com/v1   true         PostgresRestore
postgresversions            pgver        sql.tanzu.vmware.com/v1   false        PostgresVersion
postgresversionupgrades                  sql.tanzu.vmware.com/v1   true         PostgresVersionUpgrade
```

次のコマンドで、利用可能なPostgresのバージョンを確認します。

```bash
$ kubectl get postgresversion
NAME                  DB VERSION
postgres-13.20        13.20
postgres-13.20-lite   13.20
postgres-13.21        13.21
postgres-13.21-lite   13.21
postgres-14.17        14.17
postgres-14.17-lite   14.17
postgres-14.18        14.18
postgres-14.18-lite   14.18
postgres-15.12        15.12
postgres-15.12-lite   15.12
postgres-15.13        15.13
postgres-15.13-lite   15.13
postgres-16.8         16.8
postgres-16.8-lite    16.8
postgres-16.9         16.9
postgres-16.9-lite    16.9
postgres-17.4         17.4
postgres-17.4-lite    17.4
postgres-17.5         17.5
postgres-17.5-lite    17.5
```

次のコマンドで、Postgres 17.5のPostgresVersionリソースの内容を確認します。`instanceImage`にリロケート後のコンテナイメージが指定されていることを確認してください。

```yaml
$ kubectl get postgresversion postgres-17.5 -oyaml
apiVersion: sql.tanzu.vmware.com/v1
kind: PostgresVersion
metadata:
  annotations:
    meta.helm.sh/release-name: postgres-operator
    meta.helm.sh/release-namespace: postgres-system
  creationTimestamp: "2025-11-28T02:25:25Z"
  generation: 2
  labels:
    app.kubernetes.io/managed-by: Helm
  name: postgres-17.5
  resourceVersion: "34248"
  uid: abd8b13b-63b3-4a16-af3e-4dd8999e0248
spec:
  dbVersion: "17.5"
  instanceImage: registry.example.com/tanzu-sql-postgres/postgres-instance:v17.5-v4.3.1
```


### Postgresインスタンスの作成

Postgres Operatorを使って`demo` namespaceにPostgresインスタンスの作成します。

次のコマンドでnamespaceとdocker pull secretを作成します。

```bash
kubectl create namespace demo
kubectl create secret docker-registry regsecret \
    --docker-server=registry.example.com \
    --docker-username=${REGISTRY_USERNAME} \
    --docker-password="${REGISTRY_PASSWORD}" \
    -n  demo
```

次のコマンドでPostgresインスタンスを作成します。Postgresバージョンは17.5を指定し、ストレージサイズは1Gi、HAモードを有効にしています。

```yaml
cat <<EOF > /tmp/demo-postgres.yaml
---
apiVersion: sql.tanzu.vmware.com/v1
kind: Postgres
metadata:
  name: demo
spec:
  postgresVersion:
    name: postgres-17.5
  storageSize: 1Gi
  highAvailability:
    enabled: true
---
EOF

kubectl apply -f /tmp/demo-postgres.yaml -n demo
```

しばらくすると次のリソースが作成されます。

```bash
$ kubectl get postgres,sts,pod,svc,secret,certificate,pvc -n demo -l app=postgres 
NAME                                 STATUS    POSTGRES VERSION   BACKUP LOCATION   AGE
postgres.sql.tanzu.vmware.com/demo   Running   17.5                                 11m

NAME                    READY   AGE
statefulset.apps/demo   2/2     11m

NAME         READY   STATUS    RESTARTS   AGE
pod/demo-0   4/4     Running   0          11m
pod/demo-1   4/4     Running   0          11m

NAME                     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/demo             ClusterIP   10.43.31.204   <none>        5432/TCP   11m
service/demo-agent       ClusterIP   None           <none>        <none>     11m
service/demo-read-only   ClusterIP   10.43.243.24   <none>        5432/TCP   11m

NAME                                         TYPE                           DATA   AGE
secret/demo-additional-db-creds              Opaque                         1      11m
secret/demo-app-user-db-secret               servicebinding.io/postgresql   8      11m
secret/demo-ca-bundle-secret                 Opaque                         3      11m
secret/demo-db-secret                        Opaque                         5      11m
secret/demo-empty-secret                     Opaque                         0      11m
secret/demo-internal-ssl-secret              kubernetes.io/tls              3      11m
secret/demo-metrics-secret                   Opaque                         4      11m
secret/demo-metrics-tls-secret               kubernetes.io/tls              3      11m
secret/demo-mounted-replication-ssl-secret   Opaque                         3      11m
secret/demo-pgbackrest-secret                Opaque                         3      11m
secret/demo-read-only-user-db-secret         servicebinding.io/postgresql   8      11m
secret/demo-read-write-user-db-secret        servicebinding.io/postgresql   8      11m
secret/demo-replication-ssl-secret           kubernetes.io/tls              3      11m

NAME                                                           READY   SECRET                        AGE
certificate.cert-manager.io/demo-internal-ssl-certificate      True    demo-internal-ssl-secret      11m
certificate.cert-manager.io/demo-metrics-tls-certificate       True    demo-metrics-tls-secret       11m
certificate.cert-manager.io/demo-replication-ssl-certificate   True    demo-replication-ssl-secret   11m

NAME                                       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS             VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/demo-pgdata-demo-0   Bound    pvc-89b7ef87-610c-4f5d-9abe-03c806c797e7   1Gi        RWO            synology-iscsi-storage   <unset>                 11m
persistentvolumeclaim/demo-pgdata-demo-1   Bound    pvc-e6ad273e-501c-4f3e-88ae-7ba3d920b134   1Gi        RWO            synology-iscsi-storage   <unset>                 11m
persistentvolumeclaim/demo-pgwal-demo-0    Bound    pvc-fee7725c-8943-4c8c-856f-8cb54298e680   1Gi        RWO            synology-iscsi-storage   <unset>                 11m
persistentvolumeclaim/demo-pgwal-demo-1    Bound    pvc-f18d19bd-15f0-40bf-9ca2-7309d331400e   1Gi        RWO            synology-iscsi-storage   <unset>                 11m
```

作成したPostgresインスタンスにコンテナの中から`psql`でアクセスします。

```bash
$ kubectl exec -it -n demo demo-0 -c pg-container -- psql demo
psql (17.5 (VMware Postgres 17.5.0))
Type "help" for help.

demo=# 
```

初期状態は次の通りです。

```
demo=# \l
                                                     List of databases
   Name    |  Owner   | Encoding | Locale Provider | Collate |  Ctype  | Locale | ICU Rules |      Access privileges       
-----------+----------+----------+-----------------+---------+---------+--------+-----------+------------------------------
 demo      | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8 |        |           | postgres=CTc/postgres       +
           |          |          |                 |         |         |        |           | postgres_exporter=c/postgres+
           |          |          |                 |         |         |        |           | pgappuser=CTc/postgres      +
           |          |          |                 |         |         |        |           | pgrouser=c/postgres         +
           |          |          |                 |         |         |        |           | pgrwuser=c/postgres
 postgres  | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8 |        |           | postgres=CTc/postgres       +
           |          |          |                 |         |         |        |           | replication=CTc/postgres    +
           |          |          |                 |         |         |        |           | postgres_exporter=c/postgres
 template0 | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8 |        |           | =c/postgres                 +
           |          |          |                 |         |         |        |           | postgres=CTc/postgres
 template1 | postgres | UTF8     | libc            | C.UTF-8 | C.UTF-8 |        |           | =c/postgres                 +
           |          |          |                 |         |         |        |           | postgres=CTc/postgres
(4 rows)

demo=# \d
Did not find any relations.
```

利用可能なExtensionは次の通りです。ベクトル検索のための[pgvector](https://github.com/pgvector/pgvector)やUberで使われている地理インデックスシステムである[h3](https://github.com/zachasme/h3-pg)などが利用可能です。
その他、時系列データベースの[TimescaleDB](https://github.com/timescale/timescaledb)やMongoDB互換機能を提供する[DocumentDB](https://github.com/documentdb/documentdb)も含まれています。

```
demo=# SELECT * FROM pg_available_extensions ORDER BY name;
             name             | default_version | installed_version |                                                                                                                          comment                                                                                                                          
------------------------------+-----------------+-------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 address_standardizer         | 3.5.0           |                   | Used to parse an address into constituent elements. Generally used to support geocoding address normalization step.
 address_standardizer_data_us | 3.5.0           |                   | Address Standardizer US dataset example
 advanced_password_check      | 1.4             |                   | Advanced Password Check
 amcheck                      | 1.4             |                   | functions for verifying relation integrity
 autoinc                      | 1.0             |                   | functions for autoincrementing fields
 bloom                        | 1.0             |                   | bloom access method - signature file based index
 bool_plperl                  | 1.0             |                   | transform between bool and plperl
 bool_plperlu                 | 1.0             |                   | transform between bool and plperlu
 btree_gin                    | 1.3             |                   | support for indexing common datatypes in GIN
 btree_gist                   | 1.7             |                   | support for indexing common datatypes in GiST
 citext                       | 1.6             |                   | data type for case-insensitive character strings
 cube                         | 1.5             |                   | data type for multidimensional cubes
 dblink                       | 1.2             |                   | connect to other PostgreSQL databases from within a database
 dict_int                     | 1.0             |                   | text search dictionary template for integers
 dict_xsyn                    | 1.0             |                   | text search dictionary template for extended synonym processing
 diskquota                    | 3.1             |                   | diskquota
 documentdb                   | 0.104-0         |                   | API surface for DocumentDB for PostgreSQL
 documentdb_core              | 0.104-0         |                   | Core API surface for DocumentDB on PostgreSQL
 earthdistance                | 1.2             |                   | calculate great-circle distances on the surface of the Earth
 file_fdw                     | 1.0             |                   | foreign-data wrapper for flat file access
 fuzzystrmatch                | 1.2             |                   | determine similarities and distance between strings
 h3                           | 4.2.2           |                   | H3 bindings for PostgreSQL
 h3_postgis                   | 4.2.2           |                   | H3 PostGIS integration
 hstore                       | 1.8             |                   | data type for storing sets of (key, value) pairs
 hstore_plperl                | 1.0             |                   | transform between hstore and plperl
 hstore_plperlu               | 1.0             |                   | transform between hstore and plperlu
 hstore_plpython3u            | 1.0             |                   | transform between hstore and plpython3u
 insert_username              | 1.0             |                   | functions for tracking who changed a table
 intagg                       | 1.1             |                   | integer aggregator and enumerator (obsolete)
 intarray                     | 1.5             |                   | functions, operators, and index support for 1-D arrays of integers
 isn                          | 1.2             |                   | data types for international product numbering standards
 jsonb_plperl                 | 1.0             |                   | transform between jsonb and plperl
 jsonb_plperlu                | 1.0             |                   | transform between jsonb and plperlu
 jsonb_plpython3u             | 1.0             |                   | transform between jsonb and plpython3u
 libversion                   | 2.0.0           |                   | libversion extension
 lo                           | 1.1             |                   | Large Object maintenance
 login_hook                   | 1.6             |                   | login_hook - hook to execute login_hook.login() at login time
 ltree                        | 1.3             |                   | data type for hierarchical tree-like structures
 ltree_plpython3u             | 1.0             |                   | transform between ltree and plpython3u
 moddatetime                  | 1.0             |                   | functions for tracking last modification time
 orafce                       | 4.10            |                   | Functions and operators that emulate a subset of functions and packages from the Oracle RDBMS
 pageinspect                  | 1.12            |                   | inspect the contents of database pages at a low level
 pg_buffercache               | 1.5             |                   | examine the shared buffer cache
 pg_cron                      | 1.6             |                   | Job scheduler for PostgreSQL
 pg_freespacemap              | 1.2             |                   | examine the free space map (FSM)
 pg_graphql                   | 1.5.8           |                   | pg_graphql: GraphQL support
 pg_hint_plan                 | 1.7.0           |                   | optimizer hints for PostgreSQL
 pg_partman                   | 5.1.0           |                   | Extension to manage partitioned tables by time or ID
 pg_prewarm                   | 1.2             |                   | prewarm relation data
 pg_stat_monitor              | 2.1             |                   | The pg_stat_monitor is a PostgreSQL Query Performance Monitoring tool, based on PostgreSQL contrib module pg_stat_statements. pg_stat_monitor provides aggregated statistics, client information, plan details including plan, and histogram information.
 pg_stat_statements           | 1.11            |                   | track planning and execution statistics of all SQL statements executed
 pg_surgery                   | 1.0             |                   | extension to perform surgery on a damaged relation
 pg_trgm                      | 1.6             |                   | text similarity measurement and index searching based on trigrams
 pg_visibility                | 1.2             |                   | examine the visibility map (VM) and page-level visibility info
 pg_wait_sampling             | 1.1             |                   | sampling based statistics of wait events
 pg_walinspect                | 1.1             |                   | functions to inspect contents of PostgreSQL Write-Ahead Log
 pgagent                      | 4.2             |                   | A PostgreSQL job scheduler
 pgaudit                      | 17.0            |                   | provides auditing functionality
 pgautofailover               | 2.2             |                   | pg_auto_failover
 pgcrypto                     | 1.3             |                   | cryptographic functions
 pgrowlocks                   | 1.2             |                   | show row-level locking information
 pgstattuple                  | 1.5             |                   | show tuple-level statistics
 plperl                       | 1.0             |                   | PL/Perl procedural language
 plperlu                      | 1.0             |                   | PL/PerlU untrusted procedural language
 plpgsql                      | 1.0             | 1.0               | PL/pgSQL procedural language
 plpython3u                   | 1.0             |                   | PL/Python3U untrusted procedural language
 plr                          | 8.4.7           |                   | load R interpreter and execute R script from within a database
 postgis                      | 3.5.0           |                   | PostGIS geometry and geography spatial types and functions
 postgis_raster               | 3.5.0           |                   | PostGIS raster types and functions
 postgis_tiger_geocoder       | 3.5.0           |                   | PostGIS tiger geocoder and reverse geocoder
 postgis_topology             | 3.5.0           |                   | PostGIS topology spatial types and functions
 postgres_fdw                 | 1.1             |                   | foreign-data wrapper for remote PostgreSQL servers
 refint                       | 1.0             |                   | functions for implementing referential integrity (obsolete)
 rum                          | 1.3             |                   | RUM index access method
 seg                          | 1.4             |                   | data type for representing line segments or floating-point intervals
 set_user                     | 4.1.0           |                   | similar to SET ROLE but with added logging
 sslinfo                      | 1.2             |                   | information about SSL certificates
 tablefunc                    | 1.0             |                   | functions that manipulate whole tables, including crosstab
 tcn                          | 1.0             |                   | Triggered change notifications
 timescaledb                  | 2.19.3          |                   | Enables scalable inserts and complex queries for time-series data
 tsm_system_rows              | 1.0             |                   | TABLESAMPLE method which accepts number of rows as a limit
 tsm_system_time              | 1.0             |                   | TABLESAMPLE method which accepts time in milliseconds as a limit
 unaccent                     | 1.1             |                   | text search dictionary that removes accents
 uuid-ossp                    | 1.1             |                   | generate universally unique identifiers (UUIDs)
 vector                       | 0.8.0           |                   | vector data type and ivfflat and hnsw access methods
 xml2                         | 1.1             |                   | XPath querying and XSLT
(86 rows)
```

一旦quitします。

```
demo=# \q
```

次にPatroniのクラスタ情報を確認します。`demo-0`がRead Replica、`demo-1`がPrimaryであることがわかります。

```bash
$ kubectl exec -it -n demo demo-0 -c pg-container -- patronictl list

+ Cluster: demo (7577598800927174712) --------------+--------------+-----------+----+-----------+--------------------+
| Member | Host                                     | Role         | State     | TL | Lag in MB | Tags               |
+--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+
| demo-0 | demo-0.demo-agent.demo.svc.cluster.local | Leader       | running   |  1 |           | cluster-name: demo |
| demo-1 | demo-1.demo-agent.demo.svc.cluster.local | Sync Standby | streaming |  1 |         0 | cluster-name: demo |
+--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+
```

次のコマンドでRead Replicaの数を2に増やします。

```yaml
cat <<EOF > /tmp/demo-postgres.yaml
---
apiVersion: sql.tanzu.vmware.com/v1
kind: Postgres
metadata:
  name: demo
spec:
  postgresVersion:
    name: postgres-17.5
  storageSize: 1Gi
  highAvailability:
    readReplicas: 2
    enabled: true
---
EOF

kubectl apply -f /tmp/demo-postgres.yaml -n demo
```

次のコマンドでリソースを確認します。まだReplicaを増やしている途中です。

```bash
$ kubectl get postgres,sts,pod,svc,secret,certificate,pvc -n demo -l app=postgres
NAME                                 STATUS    POSTGRES VERSION   BACKUP LOCATION   AGE
postgres.sql.tanzu.vmware.com/demo   Pending   17.5                                 163m

NAME                    READY   AGE
statefulset.apps/demo   2/3     163m

NAME         READY   STATUS     RESTARTS   AGE
pod/demo-0   4/4     Running    0          163m
pod/demo-1   4/4     Running    0          163m
pod/demo-2   0/4     Init:1/2   0          18s

NAME                     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/demo             ClusterIP   10.43.31.204   <none>        5432/TCP   163m
service/demo-agent       ClusterIP   None           <none>        <none>     163m
service/demo-read-only   ClusterIP   10.43.243.24   <none>        5432/TCP   163m

NAME                                         TYPE                           DATA   AGE
secret/demo-additional-db-creds              Opaque                         1      163m
secret/demo-app-user-db-secret               servicebinding.io/postgresql   8      163m
secret/demo-ca-bundle-secret                 Opaque                         3      162m
secret/demo-db-secret                        Opaque                         5      163m
secret/demo-empty-secret                     Opaque                         0      163m
secret/demo-internal-ssl-secret              kubernetes.io/tls              3      162m
secret/demo-metrics-secret                   Opaque                         4      163m
secret/demo-metrics-tls-secret               kubernetes.io/tls              3      162m
secret/demo-mounted-replication-ssl-secret   Opaque                         3      162m
secret/demo-pgbackrest-secret                Opaque                         3      163m
secret/demo-read-only-user-db-secret         servicebinding.io/postgresql   8      163m
secret/demo-read-write-user-db-secret        servicebinding.io/postgresql   8      163m
secret/demo-replication-ssl-secret           kubernetes.io/tls              3      162m

NAME                                                           READY   SECRET                        AGE
certificate.cert-manager.io/demo-internal-ssl-certificate      True    demo-internal-ssl-secret      163m
certificate.cert-manager.io/demo-metrics-tls-certificate       True    demo-metrics-tls-secret       163m
certificate.cert-manager.io/demo-replication-ssl-certificate   True    demo-replication-ssl-secret   163m

NAME                                       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS             VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/demo-pgdata-demo-0   Bound    pvc-89b7ef87-610c-4f5d-9abe-03c806c797e7   1Gi        RWO            synology-iscsi-storage   <unset>                 163m
persistentvolumeclaim/demo-pgdata-demo-1   Bound    pvc-e6ad273e-501c-4f3e-88ae-7ba3d920b134   1Gi        RWO            synology-iscsi-storage   <unset>                 163m
persistentvolumeclaim/demo-pgdata-demo-2   Bound    pvc-fbf5a41e-19ac-4b81-befa-3b1e215ba0c4   1Gi        RWO            synology-iscsi-storage   <unset>                 18s
persistentvolumeclaim/demo-pgwal-demo-0    Bound    pvc-fee7725c-8943-4c8c-856f-8cb54298e680   1Gi        RWO            synology-iscsi-storage   <unset>                 163m
persistentvolumeclaim/demo-pgwal-demo-1    Bound    pvc-f18d19bd-15f0-40bf-9ca2-7309d331400e   1Gi        RWO            synology-iscsi-storage   <unset>                 163m
persistentvolumeclaim/demo-pgwal-demo-2    Bound    pvc-7ab78448-701e-4de5-a343-812c098f5d08   1Gi        RWO            synology-iscsi-storage   <unset>                 18s
```

途中の状態ではPatroniは次の状態になっています。

```bash
$ kubectl exec -it -n demo demo-0 -c pg-container -- patronictl list             
+ Cluster: demo (7577598800927174712) --------------+--------------+-----------+----+-----------+--------------------+
| Member | Host                                     | Role         | State     | TL | Lag in MB | Tags               |
+--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+
| demo-0 | demo-0.demo-agent.demo.svc.cluster.local | Leader       | running   |  1 |           | cluster-name: demo |
+--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+
| demo-1 | demo-1.demo-agent.demo.svc.cluster.local | Sync Standby | streaming |  1 |         0 | cluster-name: demo |
+--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+
| demo-2 | demo-2.demo-agent.demo.svc.cluster.local | Replica      | stopped   |    |   unknown | cluster-name: demo |
|        |                                          |              |           |    |           | nosync: true       |
+--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+
```

しばらくするとクラスタの更新が完了します。

```bash
$ kubectl get postgres,sts,pod,svc,secret,certificate,pvc -n demo -l app=postgres
NAME                                 STATUS    POSTGRES VERSION   BACKUP LOCATION   AGE
postgres.sql.tanzu.vmware.com/demo   Running   17.5                                 163m

NAME                    READY   AGE
statefulset.apps/demo   3/3     163m

NAME         READY   STATUS    RESTARTS   AGE
pod/demo-0   4/4     Running   0          163m
pod/demo-1   4/4     Running   0          163m
pod/demo-2   4/4     Running   0          72s

NAME                     TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/demo             ClusterIP   10.43.31.204   <none>        5432/TCP   163m
service/demo-agent       ClusterIP   None           <none>        <none>     163m
service/demo-read-only   ClusterIP   10.43.243.24   <none>        5432/TCP   163m

NAME                                         TYPE                           DATA   AGE
secret/demo-additional-db-creds              Opaque                         1      163m
secret/demo-app-user-db-secret               servicebinding.io/postgresql   8      163m
secret/demo-ca-bundle-secret                 Opaque                         3      163m
secret/demo-db-secret                        Opaque                         5      163m
secret/demo-empty-secret                     Opaque                         0      163m
secret/demo-internal-ssl-secret              kubernetes.io/tls              3      163m
secret/demo-metrics-secret                   Opaque                         4      163m
secret/demo-metrics-tls-secret               kubernetes.io/tls              3      163m
secret/demo-mounted-replication-ssl-secret   Opaque                         3      163m
secret/demo-pgbackrest-secret                Opaque                         3      163m
secret/demo-read-only-user-db-secret         servicebinding.io/postgresql   8      163m
secret/demo-read-write-user-db-secret        servicebinding.io/postgresql   8      163m
secret/demo-replication-ssl-secret           kubernetes.io/tls              3      163m

NAME                                                           READY   SECRET                        AGE
certificate.cert-manager.io/demo-internal-ssl-certificate      True    demo-internal-ssl-secret      163m
certificate.cert-manager.io/demo-metrics-tls-certificate       True    demo-metrics-tls-secret       163m
certificate.cert-manager.io/demo-replication-ssl-certificate   True    demo-replication-ssl-secret   163m

NAME                                       STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS             VOLUMEATTRIBUTESCLASS   AGE
persistentvolumeclaim/demo-pgdata-demo-0   Bound    pvc-89b7ef87-610c-4f5d-9abe-03c806c797e7   1Gi        RWO            synology-iscsi-storage   <unset>                 163m
persistentvolumeclaim/demo-pgdata-demo-1   Bound    pvc-e6ad273e-501c-4f3e-88ae-7ba3d920b134   1Gi        RWO            synology-iscsi-storage   <unset>                 163m
persistentvolumeclaim/demo-pgdata-demo-2   Bound    pvc-fbf5a41e-19ac-4b81-befa-3b1e215ba0c4   1Gi        RWO            synology-iscsi-storage   <unset>                 72s
persistentvolumeclaim/demo-pgwal-demo-0    Bound    pvc-fee7725c-8943-4c8c-856f-8cb54298e680   1Gi        RWO            synology-iscsi-storage   <unset>                 163m
persistentvolumeclaim/demo-pgwal-demo-1    Bound    pvc-f18d19bd-15f0-40bf-9ca2-7309d331400e   1Gi        RWO            synology-iscsi-storage   <unset>                 163m
persistentvolumeclaim/demo-pgwal-demo-2    Bound    pvc-7ab78448-701e-4de5-a343-812c098f5d08   1Gi        RWO            synology-iscsi-storage   <unset>                 72s
```

Patroniクラスタは次の状態になっています。


```bash
$ kubectl exec -it -n demo demo-0 -c pg-container -- patronictl list 
+ Cluster: demo (7577598800927174712) --------------+--------------+-----------+----+-----------+--------------------+
| Member | Host                                     | Role         | State     | TL | Lag in MB | Tags               |
+--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+
| demo-0 | demo-0.demo-agent.demo.svc.cluster.local | Leader       | running   |  1 |           | cluster-name: demo |
| demo-1 | demo-1.demo-agent.demo.svc.cluster.local | Sync Standby | streaming |  1 |         0 | cluster-name: demo |
| demo-2 | demo-2.demo-agent.demo.svc.cluster.local | Replica      | streaming |  1 |         0 | cluster-name: demo |
+--------+------------------------------------------+--------------+-----------+----+-----------+--------------------+
```


次のコマンドで各PostgresインスタンスのIPを確認します。

```bash
$ kubectl get pod -n demo -l app=postgres -owide
NAME     READY   STATUS    RESTARTS   AGE    IP           NODE             NOMINATED NODE   READINESS GATES
demo-0   4/4     Running   0          165m   10.42.1.23   192.168.11.51    <none>           <none>
demo-1   4/4     Running   0          165m   10.42.2.10   192.168.11.52    <none>           <none>
demo-2   4/4     Running   0          3m8s   10.42.0.7    192.168.11.150   <none>           <none>
```

クラスタの外から、このクラスタにアクセスしてみます。次のコマンドで接続情報を確認します。

```bash
$ kubectl get secret -n demo demo-app-user-db-secret -ojson | jq '.data | map_values(@base64d)'
{
  "database": "demo",
  "host": "demo.demo",
  "password": "Y08CQIn2SOOpRQwhCQO892p9Muf692",
  "port": "5432",
  "provider": "vmware",
  "type": "postgresql",
  "uri": "postgresql://pgappuser:Y08CQIn2SOOpRQwhCQO892p9Muf692@demo.demo:5432/demo",
  "username": "pgappuser"
}
```

次のコマンドでクラスタの外から`psql`を使用する準備をします。

```bash
$ kubectl run pg-client --rm -it --image=postgres:alpine -- bash

If you don't see a command prompt, try pressing enter.
pg-client:/# 
```

取得した接続情報を使い、次のように`psql`でクラスタにアクセスします。まずはread-writeなエンドポイントにアクセスします。

```bash
psql postgresql://pgappuser:Y08CQIn2SOOpRQwhCQO892p9Muf692@demo.demo:5432/demo
```

次のコマンドで接続先のサーバー情報を表示します。`demo-0`のIPが返るので、Leaderに接続していることがわかります。

```
demo=> SELECT inet_server_addr() AS server_ip, inet_server_port() AS server_port;
 server_ip  | server_port 
------------+-------------
 10.42.1.23 |        5432
(1 row)
```

次のSQLを実行し、テストデータをinsertします。

```sql
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');
```

insertしたデータを検索します。

```
demo=> select organization_id,organization_name from organization;
 organization_id | organization_name 
-----------------+-------------------
               1 | foo
               2 | bar
(2 rows)
```

いったんquitします。

```
demo=> \q
```

次にread-onlyなエンドポイントに接続します。

```bash
psql postgresql://pgappuser:Y08CQIn2SOOpRQwhCQO892p9Muf692@demo-read-only.demo:5432/demo
```

次のコマンドで接続先のサーバー情報を表示します。`demo-2`のIPが返るので、Read Replicaに接続していることがわかります。

```
demo=> SELECT inet_server_addr() AS server_ip, inet_server_port() AS server_port;
 server_ip | server_port 
-----------+-------------
 10.42.0.7 |        5432
(1 row)
```

次のコマンドで、先ほどinsertしたデータが検索できることを確認します。

```
demo=> select organization_id,organization_name from organization;
 organization_id | organization_name 
-----------------+-------------------
               1 | foo
               2 | bar
(2 rows)
```

insertを試みるとエラーになります。read-onlyなので期待通りです。

```
demo=> INSERT INTO organization(organization_id, organization_name) VALUES(3, 'baz');
ERROR:  cannot execute INSERT in a read-only transaction
```

quitします。

```
demo=> \q
```

`psql`を実行したコンテナからexitします。

```bash
pg-client:/# exit
```

作成したPostgresインスタンスを一旦削除します。

```bash
kubectl delete -f /tmp/demo-postgres.yaml -n demo
```

### サンプルアプリのデプロイ

次にPostgresインスタンスにアプリから接続してみます。ここではサンプルアプリとして https://github.com/categolj/room-reservation を使用します。
動作確認したrevisionは[`cbcf650054bfac1562d473eed1aded84b020a4e7`](https://github.com/categolj/room-reservation/tree/cbcf650054bfac1562d473eed1aded84b020a4e7)です。

次のコマンドでこのアプリ向けのPostgresインスタンスを作成します。

```yaml
cat <<EOF > /tmp/room-reservation-db.yaml
---
apiVersion: sql.tanzu.vmware.com/v1
kind: Postgres
metadata:
  name: room-reservation-db
spec:
  postgresVersion:
    name: postgres-17.5
  storageSize: 1Gi
  highAvailability:
    readReplicas: 2
    enabled: true
  resources:
    data:
      limits:
        memory: 1024Mi
      requests:
        memory: 1024Mi
---
EOF

kubectl apply -f /tmp/room-reservation-db.yaml -n demo
```

次のコマンドでPostgresインスタンスが作成されたことを確認します。

```bash
$ kubectl get postgres,pod,secret -n demo -l app=postgres
NAME                                                STATUS    POSTGRES VERSION   BACKUP LOCATION   AGE
postgres.sql.tanzu.vmware.com/room-reservation-db   Running   17.5                                 50s

NAME                        READY   STATUS    RESTARTS   AGE
pod/room-reservation-db-0   4/4     Running   0          50s
pod/room-reservation-db-1   4/4     Running   0          50s
pod/room-reservation-db-2   4/4     Running   0          50s

NAME                                                        TYPE                           DATA   AGE
secret/room-reservation-db-additional-db-creds              Opaque                         1      50s
secret/room-reservation-db-app-user-db-secret               servicebinding.io/postgresql   8      50s
secret/room-reservation-db-ca-bundle-secret                 Opaque                         3      42s
secret/room-reservation-db-db-secret                        Opaque                         5      50s
secret/room-reservation-db-empty-secret                     Opaque                         0      50s
secret/room-reservation-db-internal-ssl-secret              kubernetes.io/tls              3      42s
secret/room-reservation-db-metrics-secret                   Opaque                         4      50s
secret/room-reservation-db-metrics-tls-secret               kubernetes.io/tls              3      41s
secret/room-reservation-db-mounted-replication-ssl-secret   Opaque                         3      41s
secret/room-reservation-db-pgbackrest-secret                Opaque                         3      50s
secret/room-reservation-db-read-only-user-db-secret         servicebinding.io/postgresql   8      50s
secret/room-reservation-db-read-write-user-db-secret        servicebinding.io/postgresql   8      50s
secret/room-reservation-db-replication-ssl-secret           kubernetes.io/tls              3      41s
```

次にアプリをデプロイします。まずはシンプルにread-writeなエンドポイントにアクセスします。

`room-reservation-db-app-user-db-secret`は[Service Binding Specification for Kubernetes](https://github.com/servicebinding/spec)に対応しているので、[Spring Cloud Bindings](https://github.com/spring-cloud/spring-cloud-bindings)により、マウントした情報から自動で`spring.datasource.*`プロパティを設定します。


```bash
helm repo add stakater https://stakater.github.io/stakater-charts
```

```yaml
cat <<EOF > room-reservation-values.yaml
---
applicationName: room-reservation
deployment:
  image:
    repository: ghcr.io/categolj/room-reservation
    tag: jvm
    pullPolicy: Always
  ports:
  - name: http
    containerPort: 8080
    protocol: TCP
  env:
    SERVICE_BINDING_ROOT:
      value: /bindings
    spring.datasource.hikari.data-source-properties.sslmode:
      value: require
    logging.structured.format.console:
      value: ecs
    logging.level.root:
      value: error
  volumeMounts:
    room-reservation-db:
      mountPath: /bindings/room-reservation-db
  volumes:
    room-reservation-db:
      secret:
        secretName: room-reservation-db-app-user-db-secret
  resources:
    limits:
      memory: 1024Mi
    requests:
      memory: 1024Mi
  livenessProbe:
    enabled: true
    httpGet:
      path: /actuator/health/liveness
      port: 8080
      scheme: HTTP
  readinessProbe:
    enabled: true
    httpGet:
      path: /actuator/health/readiness
      port: 8080
      scheme: HTTP
  containerSecurityContext:
    allowPrivilegeEscalation: false
    capabilities:
      drop:
      - ALL
    readOnlyRootFilesystem: false
    runAsNonRoot: true
    runAsUser: 1002
    seccompProfile:
      type: RuntimeDefault
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - podAffinityTerm:
          labelSelector:
            matchLabels:
              app.kubernetes.io/part-of: room-reservation
          topologyKey: kubernetes.io/hostname
        weight: 1
ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
  hosts:
  - host: room-reservation.lan.ik.am
    paths:
    - path: /
      pathType: Prefix
  tls:
  - hosts:
    - room-reservation.lan.ik.am
    secretName: room-reservation-tls
---
EOF
```

次のコマンドでテンプレートを確認します。

```bash
helm template room-reservation stakater/application -n demo -f room-reservation-values.yaml
```

次のコマンドでデプロイします。

```bash
helm upgrade --install room-reservation stakater/application -n demo -f room-reservation-values.yaml --wait
```

次のPodとService、Ingressが作成されます。

```bash
$ kubectl get pod,svc,ing -n demo
NAME                                    READY   STATUS    RESTARTS   AGE
pod/room-reservation-677496b99d-f82wt   1/1     Running   0          18s
pod/room-reservation-db-0               4/4     Running   0          97s
pod/room-reservation-db-1               4/4     Running   0          97s
pod/room-reservation-db-2               4/4     Running   0          97s

NAME                                    TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
service/room-reservation                ClusterIP   10.43.67.214   <none>        8080/TCP   18s
service/room-reservation-db             ClusterIP   10.43.13.215   <none>        5432/TCP   97s
service/room-reservation-db-agent       ClusterIP   None           <none>        <none>     97s
service/room-reservation-db-read-only   ClusterIP   10.43.41.99    <none>        5432/TCP   97s

NAME                                         CLASS     HOSTS                        ADDRESS          PORTS     AGE
ingress.networking.k8s.io/room-reservation   traefik   room-reservation.lan.ik.am   192.168.11.240   80, 443   18s
```

Ingressに設定したドメイン（この例では https://room-reservation.lan.ik.am ）にアクセスすると、次の画面が表示されます：

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/d90a5daf-ecb8-40a6-9a00-30f87ba11865.png)


Ingressがない場合は、Serviceに対するport-forwardingでこのアプリ (http://localhost:8080) にアクセスします。

```bash
kubectl port-forward -n demo svc/room-reservation 8080:8080
```


![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/bab7297c-f6ff-4dd6-ac02-3c40da5d41b8.png)

部屋の予約ができることを確認します。

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/93efb8a7-e3a8-4049-b829-e450622c371a.png)

この部屋の予約情報をREST APIで取得します。(`date`と`roomId`パラメータを変更してください。)

```
$ curl "https://room-reservation.lan.ik.am/api/reservations?date=2025-11-28&roomId=018422b2-4843-7a62-935b-b4e65649de3f"
# or
$ curl "http://localhost:8080/api/reservations?date=2025-11-28&roomId=018422b2-4843-7a62-935b-b4e65649de3f"

[{"reservationId":"019ac989-d981-7d1c-acf0-aed4ea19035e","roomId":"018422b2-4843-7a62-935b-b4e65649de3f","date":"2025-11-28","startTime":"09:00:00","endTime":"11:00:00","purpose":"Team Meeting","userId":"018422b2-4843-7a62-935b-b4e65649de46"}]
```

このAPIに対して、[`vegeta`](https://github.com/tsenart/vegeta)を使って負荷をかけてみます。`vegeta`は次のコマンドでインストールできます。

```bash
brew install vegeta
```

次のコマンドで負荷をかけます。`kubectl port-forward -n demo svc/room-reservation 8080:8080`を実行している前提です。

```
echo "GET http://localhost:8080/api/reservations?date=2025-11-28&roomId=018422b2-4843-7a62-935b-b4e65649de3f" | vegeta attack -duration=10s | tee results.bin | vegeta report
```

全てステータスコード200で返ればOKです。

```
Requests      [total, rate, throughput]         500, 50.10, 49.98
Duration      [total, attack, wait]             10.005s, 9.979s, 25.081ms
Latencies     [min, mean, 50, 90, 95, 99, max]  12.846ms, 20.438ms, 18.752ms, 26.455ms, 28.479ms, 46.112ms, 89.53ms
Bytes In      [total, mean]                     1000, 2.00
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           100.00%
Status Codes  [code:count]                      200:500  
Error Set:
```

次に負荷をかけている最中にこのPatroniクラスタをアップデートしてみます。次のコマンドで120秒負荷をかけます。

```bash
echo "GET http://localhost:8080/api/reservations?date=2025-11-28&roomId=018422b2-4843-7a62-935b-b4e65649de3f" | vegeta attack -duration=120s | tee results.bin | vegeta report
````

`/tmp/room-reservation-db.yaml`を編集し、例えば、次のようにPostgresのメモリを少しだけ変更してみます。

```yaml
  resources:
    data:
      limits:
        memory: 1025Mi
      requests:
        memory: 1025Mi
```

別のターミナルでPatroniクラスタを更新します。

```bash
kubectl apply -f /tmp/room-reservation-db.yaml -n demo
```

vegetaの負荷テスト結果は次の通りです。5%ほどが500エラーです。

```
Requests      [total, rate, throughput]         6000, 50.01, 47.38
Duration      [total, attack, wait]             2m0s, 2m0s, 13.718ms
Latencies     [min, mean, 50, 90, 95, 99, max]  6.81ms, 207.614ms, 21.618ms, 657.208ms, 2.011s, 2.07s, 2.506s
Bytes In      [total, mean]                     1440990, 240.16
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           94.75%
Status Codes  [code:count]                      200:5685  500:315  
Error Set:
500 
```

次のようなエラーログを確認できました。


```json
{
  "@timestamp": "2025-11-29T10:12:08.453672643Z",
  "log": {
    "level": "ERROR",
    "logger": "com.example.problem.ProblemControllerAdvice"
  },
  "process": {
    "pid": 1,
    "thread": {
      "name": "tomcat-handler-11865"
    }
  },
  "service": {
    "name": "room-reservation",
    "version": "0.0.1.cbcf650054bfac1562d473eed1aded84b020a4e7",
    "node": {}
  },
  "message": "Unexpected runtime error occurred!",
  "traceId": "d3a7566524db016b5cce9a155bd1d6e0",
  "spanId": "115a387db4dd469b",
  "error": {
    "type": "org.springframework.transaction.CannotCreateTransactionException",
    "message": "Could not open JDBC Connection for transaction",
    "stack_trace": "org.springframework.transaction.CannotCreateTransactionException: Could not open JDBC Connection for transaction\n\tat org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:313)\n\tat org.springframework.transaction.support.AbstractPlatformTransactionManager.startTransaction(AbstractPlatformTransactionManager.java:532)\n\tat org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:405)\n\tat org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:639)\n\tat org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:374)\n\tat org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.query.ReservationViewRepository$$SpringCGLIB$$0.findByRoomIdAndDate(<generated>)\n\tat com.example.reservation.query.ReservationService.findByRoomIdAndDate(ReservationService.java:74)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)\n\tat com.example.config.ServiceLogAspect.logServiceMethod(ServiceLogAspect.java:37)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.modulith.observability.ModuleEntryInterceptor.invoke(ModuleEntryInterceptor.java:91)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.config.ServiceLogAspect$$SpringCGLIB$$0.logServiceMethod(<generated>)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:642)\n\tat org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:632)\n\tat org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:173)\n\tat org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.query.ReservationService$$SpringCGLIB$$0.findByRoomIdAndDate(<generated>)\n\tat com.example.reservation.web.ReservationController.findReservation(ReservationController.java:47)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.modulith.observability.ModuleEntryInterceptor.invoke(ModuleEntryInterceptor.java:91)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.web.ReservationController$$SpringCGLIB$$0.findReservation(<generated>)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:258)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:191)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.zalando.logbook.servlet.LogbookFilter.doFilter(LogbookFilter.java:76)\n\tat org.zalando.logbook.servlet.HttpFilter.doFilter(HttpFilter.java:32)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:114)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:732)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1769)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\n\tat java.base/java.lang.VirtualThread.run(Unknown Source)\nCaused by: java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 2000ms (total=0, active=0, idle=0, waiting=100)\n\tat com.zaxxer.hikari.pool.HikariPool.createTimeoutException(HikariPool.java:709)\n\tat com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:188)\n\tat com.zaxxer.hikari.pool.HikariPool.getConnection(HikariPool.java:146)\n\tat com.zaxxer.hikari.HikariDataSource.getConnection(HikariDataSource.java:127)\n\tat org.springframework.jdbc.datasource.DataSourceTransactionManager.doBegin(DataSourceTransactionManager.java:269)\n\t... 99 common frames omitted\nCaused by: org.postgresql.util.PSQLException: Connection attempt timed out.\n\tat org.postgresql.Driver$ConnectThread.getResult(Driver.java:412)\n\tat org.postgresql.Driver.connect(Driver.java:305)\n\tat com.zaxxer.hikari.util.DriverDataSource.getConnection(DriverDataSource.java:139)\n\tat com.zaxxer.hikari.pool.PoolBase.newConnection(PoolBase.java:368)\n\tat com.zaxxer.hikari.pool.PoolBase.newPoolEntry(PoolBase.java:205)\n\tat com.zaxxer.hikari.pool.HikariPool.createPoolEntry(HikariPool.java:483)\n\tat com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator.call(HikariPool.java:747)\n\tat com.zaxxer.hikari.pool.HikariPool$PoolEntryCreator.call(HikariPool.java:726)\n\tat java.base/java.util.concurrent.FutureTask.run(Unknown Source)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source)\n\tat java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source)\n\tat java.base/java.lang.Thread.run(Unknown Source)\n"
  },
  "ecs": {
    "version": "8.11"
  }
}
```

次のコマンドで負荷テストの結果をグラフ化します。

```bash
cat results.bin | vegeta plot > plot.html
```

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/311de3c7-6d2a-4e98-959f-bfa7c28f9173.png)


* primaryが切断され、read replica(1)がprimaryに昇格。primaryだったノードはread replica(3)に降格
* 昇格したprimaryが切断され、read replica(2)がprimaryに昇格
* 昇格したprimaryが切断され、read replica(3)がprimaryに昇格

の間にエラーが発生していたと思われます。

### Read Replicaに対するRoutingを設定

アプリのダウンタイムを短くするため、参照形のアクセスはRead Replicaにroutingされるようにアプリ側を設定します。

このアプリには次の定義が含まれています。

* https://github.com/categolj/room-reservation/blob/cbcf650054bfac1562d473eed1aded84b020a4e7/src/main/java/com/example/config/ReadOnlyTransactionRoutingDataSource.java
* https://github.com/categolj/room-reservation/blob/cbcf650054bfac1562d473eed1aded84b020a4e7/src/main/java/com/example/config/DataSourceConfig.java

`spring.datasource.*`プロパティ同様に`read-replica.datasource.*`プロパティを設定すると`@Transactional(readOnly = true)`アノテーションが設定されたコードはRead Replicaにアクセスするようになります。

そこで`spring.datasource.*`プロパティは引き続きSpring Cloud Bindingで自動設定させつつ、

* `read-replica.datasource.url`
* `read-replica.datasource.username`
* `read-replica.datasource.password`

プロパティはread-onlyなエンドポイントに対する接続情報が設定されるように環境変数を設定します。

```yaml
cat <<'EOF' > room-reservation-values.yaml
---
applicationName: room-reservation
deployment:
  image:
    repository: ghcr.io/categolj/room-reservation
    tag: jvm
    pullPolicy: Always
  ports:
  - name: http
    containerPort: 8080
    protocol: TCP
  env:
    SERVICE_BINDING_ROOT:
      value: /bindings
    read-replica.datasource.url:
      value: jdbc:postgresql://${k8s.bindings.room-reservation-db.database}-read-only:${k8s.bindings.room-reservation-db.port}/${k8s.bindings.room-reservation-db.database}
    read-replica.datasource.username:
      value: ${spring.datasource.username}
    read-replica.datasource.password:
      value: ${spring.datasource.password}
    jdbc.datasource-proxy.enabled:
      value: "false"
    spring.datasource.hikari.data-source-properties.sslmode:
      value: require
    logging.structured.format.console:
      value: ecs
    logging.level.root:
      value: error
  volumeMounts:
    room-reservation-db:
      mountPath: /bindings/room-reservation-db
  volumes:
    room-reservation-db:
      secret:
        secretName: room-reservation-db-app-user-db-secret
  resources:
    limits:
      memory: 1024Mi
    requests:
      memory: 1024Mi
  livenessProbe:
    enabled: true
    httpGet:
      path: /actuator/health/liveness
      port: 8080
      scheme: HTTP
  readinessProbe:
    enabled: true
    httpGet:
      path: /actuator/health/readiness
      port: 8080
      scheme: HTTP
  containerSecurityContext:
    allowPrivilegeEscalation: false
    capabilities:
      drop:
      - ALL
    readOnlyRootFilesystem: false
    runAsNonRoot: true
    runAsUser: 1002
    seccompProfile:
      type: RuntimeDefault
  affinity:
    podAntiAffinity:
      preferredDuringSchedulingIgnoredDuringExecution:
      - podAffinityTerm:
          labelSelector:
            matchLabels:
              app.kubernetes.io/part-of: room-reservation
          topologyKey: kubernetes.io/hostname
        weight: 1
ingress:
  enabled: true
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt
  hosts:
  - host: room-reservation.lan.ik.am
    paths:
    - path: /
      pathType: Prefix
  tls:
  - hosts:
    - room-reservation.lan.ik.am
    secretName: room-reservation-tls
---
EOF
```

次のコマンドでデプロイします。

```bash
helm upgrade --install room-reservation stakater/application -n demo -f room-reservation-values.yaml --wait
```

> [!TIP] このアプリは[datasource-proxy](https://jdbc-observations.github.io/datasource-proxy/docs/snapshot/user-guide/index.html)を使用していましたが、データソースのroutingが想定通りに動作しなかったため、`jdbc.datasource-proxy.enabled=false`を設定してProxyを無効にしました。

設定変更後に、再びvegetaで負荷を与えます。

```bash
echo "GET http://localhost:8080/api/reservations?date=2025-11-28&roomId=018422b2-4843-7a62-935b-b4e65649de3f" | vegeta attack -duration=90s | tee results.bin | vegeta report
````

別のターミナルでPatroniクラスタを更新します。変更したメモリを元に戻して、applyしましょう。

```bash
kubectl apply -f /tmp/room-reservation-db.yaml -n demo
```

vegetaの負荷テスト結果は次の通りです。エラーが503の2件になりました。 (件数は実行時の条件によって変わるでしょう。)

```
Requests      [total, rate, throughput]         6000, 50.01, 49.99
Duration      [total, attack, wait]             2m0s, 2m0s, 12.602ms
Latencies     [min, mean, 50, 90, 95, 99, max]  6.813ms, 29.719ms, 19.347ms, 35.162ms, 67.051ms, 287.953ms, 609.905ms
Bytes In      [total, mean]                     12382, 2.06
Bytes Out     [total, mean]                     0, 0.00
Success       [ratio]                           99.97%
Status Codes  [code:count]                      200:5998  503:2  
Error Set:
503 
```

グラフは次の通りです。80-90秒あたりに一瞬エラーが発生しています。

![image](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1852/8a719059-79c4-46f8-87df-7be75b05da15.png)

次のエラーログを確認できました。

```json
{
  "@timestamp": "2025-11-29T12:03:42.527566205Z",
  "log": {
    "level": "ERROR",
    "logger": "com.example.problem.ProblemControllerAdvice"
  },
  "process": {
    "pid": 1,
    "thread": {
      "name": "tomcat-handler-4863"
    }
  },
  "service": {
    "name": "room-reservation",
    "version": "0.0.1.cbcf650054bfac1562d473eed1aded84b020a4e7",
    "node": {}
  },
  "message": "There is a problem with database access.",
  "traceId": "bc811b274e300785c0e5e3afff39dbba",
  "spanId": "b9816fca5faa3788",
  "error": {
    "type": "org.springframework.dao.DataAccessResourceFailureException",
    "message": "PreparedStatementCallback; SQL [SELECT reservation_id,\n    room_id,\n    date,\n    start_time,\n    end_time,\n    purpose,\n    user_id\nFROM reservation_view\nWHERE room_id = ? AND date = ?]; FATAL: terminating connection due to administrator command",
    "stack_trace": "org.springframework.dao.DataAccessResourceFailureException: PreparedStatementCallback; SQL [SELECT reservation_id,\n    room_id,\n    date,\n    start_time,\n    end_time,\n    purpose,\n    user_id\nFROM reservation_view\nWHERE room_id = ? AND date = ?]; FATAL: terminating connection due to administrator command\n\tat org.springframework.jdbc.support.SQLStateSQLExceptionTranslator.doTranslate(SQLStateSQLExceptionTranslator.java:121)\n\tat org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:107)\n\tat org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:116)\n\tat org.springframework.jdbc.core.JdbcTemplate.translateException(JdbcTemplate.java:1556)\n\tat org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:677)\n\tat org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:723)\n\tat org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:748)\n\tat org.springframework.jdbc.core.JdbcTemplate.query(JdbcTemplate.java:804)\n\tat org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate.query(NamedParameterJdbcTemplate.java:218)\n\tat org.springframework.jdbc.core.simple.DefaultJdbcClient$DefaultStatementSpec$NamedParamMappedQuerySpec.list(DefaultJdbcClient.java:366)\n\tat com.example.reservation.query.ReservationViewRepository.findByRoomIdAndDate(ReservationViewRepository.java:49)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:138)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:380)\n\tat org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.query.ReservationViewRepository$$SpringCGLIB$$0.findByRoomIdAndDate(<generated>)\n\tat com.example.reservation.query.ReservationService.findByRoomIdAndDate(ReservationService.java:74)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:89)\n\tat com.example.config.ServiceLogAspect.logServiceMethod(ServiceLogAspect.java:37)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.modulith.observability.ModuleEntryInterceptor.invoke(ModuleEntryInterceptor.java:91)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.config.ServiceLogAspect$$SpringCGLIB$$0.logServiceMethod(<generated>)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:642)\n\tat org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:632)\n\tat org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:71)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:173)\n\tat org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:97)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.query.ReservationService$$SpringCGLIB$$0.findByRoomIdAndDate(<generated>)\n\tat com.example.reservation.web.ReservationController.findReservation(ReservationController.java:47)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:359)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:196)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)\n\tat org.springframework.modulith.observability.ModuleEntryInterceptor.invoke(ModuleEntryInterceptor.java:91)\n\tat org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:184)\n\tat org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:728)\n\tat com.example.reservation.web.ReservationController$$SpringCGLIB$$0.findReservation(<generated>)\n\tat java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)\n\tat java.base/java.lang.reflect.Method.invoke(Unknown Source)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:258)\n\tat org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:191)\n\tat org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:118)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:986)\n\tat org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:891)\n\tat org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1089)\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:979)\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1014)\n\tat org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564)\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:195)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.zalando.logbook.servlet.LogbookFilter.doFilter(LogbookFilter.java:76)\n\tat org.zalando.logbook.servlet.HttpFilter.doFilter(HttpFilter.java:32)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.ServerHttpObservationFilter.doFilterInternal(ServerHttpObservationFilter.java:114)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140)\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167)\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:116)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\n\tat org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:732)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344)\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:398)\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903)\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1769)\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\n\tat java.base/java.lang.VirtualThread.run(Unknown Source)\nCaused by: org.postgresql.util.PSQLException: FATAL: terminating connection due to administrator command\n\tat org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2734)\n\tat org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2421)\n\tat org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:372)\n\tat org.postgresql.jdbc.PgStatement.executeInternal(PgStatement.java:518)\n\tat org.postgresql.jdbc.PgStatement.execute(PgStatement.java:435)\n\tat org.postgresql.jdbc.PgPreparedStatement.executeWithFlags(PgPreparedStatement.java:196)\n\tat org.postgresql.jdbc.PgPreparedStatement.executeQuery(PgPreparedStatement.java:139)\n\tat com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)\n\tat com.zaxxer.hikari.pool.HikariProxyPreparedStatement.executeQuery(HikariProxyPreparedStatement.java)\n\tat org.springframework.jdbc.core.JdbcTemplate$1.doInPreparedStatement(JdbcTemplate.java:732)\n\tat org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:658)\n\t... 109 common frames omitted\n"
  },
  "ecs": {
    "version": "8.11"
  }
}
```

`FATAL: terminating connection due to administrator command`というエラーメッセージからわかるように、アクセスしているread replicaへのコネクションが再起動によって切断された"瞬断"が数件発生しただけのようです。
別のread replicaへのコネクションの復旧は瞬時に行われているようです。アクセスしているread replicaが先に再起動するか、後に再起動するかでエラーの回数は変わると思います。
ちなみにこのエラー(`DataAccessResourceFailureException`)はRetryで救済可能でしょう。

参照系のみですが、ダウンタイムはかなり改善されました。

---

Patroniを使ったTanzu for Postgres on Kubernetesを試しました。アプリ側の設定次第ではPostgresアップデート時のダウンタイムをかなり小さく抑えられそうです。
