---
title: Tanzu for Postgres on Kubernetes 4.3をインストールしてSpring Bootアプリからアクセスするメモ
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)がリリースされ、 インストールの方法が若干変わったため、ほぼ同じ内容で再度メモします。
**目次**
### 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"をクリックします。
バージョン"4.3.1"の"Token Download"アイコンをクリックします。

次のようなダイアログが現れます。
下にスクロールするとRegistryの情報の記載が見えます。
次のコマンドで`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//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//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//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 < /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 5432/TCP 11m
service/demo-agent ClusterIP None 11m
service/demo-read-only ClusterIP 10.43.243.24 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 11m
persistentvolumeclaim/demo-pgdata-demo-1 Bound pvc-e6ad273e-501c-4f3e-88ae-7ba3d920b134 1Gi RWO synology-iscsi-storage 11m
persistentvolumeclaim/demo-pgwal-demo-0 Bound pvc-fee7725c-8943-4c8c-856f-8cb54298e680 1Gi RWO synology-iscsi-storage 11m
persistentvolumeclaim/demo-pgwal-demo-1 Bound pvc-f18d19bd-15f0-40bf-9ca2-7309d331400e 1Gi RWO synology-iscsi-storage 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 < /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 5432/TCP 163m
service/demo-agent ClusterIP None 163m
service/demo-read-only ClusterIP 10.43.243.24 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 163m
persistentvolumeclaim/demo-pgdata-demo-1 Bound pvc-e6ad273e-501c-4f3e-88ae-7ba3d920b134 1Gi RWO synology-iscsi-storage 163m
persistentvolumeclaim/demo-pgdata-demo-2 Bound pvc-fbf5a41e-19ac-4b81-befa-3b1e215ba0c4 1Gi RWO synology-iscsi-storage 18s
persistentvolumeclaim/demo-pgwal-demo-0 Bound pvc-fee7725c-8943-4c8c-856f-8cb54298e680 1Gi RWO synology-iscsi-storage 163m
persistentvolumeclaim/demo-pgwal-demo-1 Bound pvc-f18d19bd-15f0-40bf-9ca2-7309d331400e 1Gi RWO synology-iscsi-storage 163m
persistentvolumeclaim/demo-pgwal-demo-2 Bound pvc-7ab78448-701e-4de5-a343-812c098f5d08 1Gi RWO synology-iscsi-storage 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 5432/TCP 163m
service/demo-agent ClusterIP None 163m
service/demo-read-only ClusterIP 10.43.243.24 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 163m
persistentvolumeclaim/demo-pgdata-demo-1 Bound pvc-e6ad273e-501c-4f3e-88ae-7ba3d920b134 1Gi RWO synology-iscsi-storage 163m
persistentvolumeclaim/demo-pgdata-demo-2 Bound pvc-fbf5a41e-19ac-4b81-befa-3b1e215ba0c4 1Gi RWO synology-iscsi-storage 72s
persistentvolumeclaim/demo-pgwal-demo-0 Bound pvc-fee7725c-8943-4c8c-856f-8cb54298e680 1Gi RWO synology-iscsi-storage 163m
persistentvolumeclaim/demo-pgwal-demo-1 Bound pvc-f18d19bd-15f0-40bf-9ca2-7309d331400e 1Gi RWO synology-iscsi-storage 163m
persistentvolumeclaim/demo-pgwal-demo-2 Bound pvc-7ab78448-701e-4de5-a343-812c098f5d08 1Gi RWO synology-iscsi-storage 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
demo-1 4/4 Running 0 165m 10.42.2.10 192.168.11.52
demo-2 4/4 Running 0 3m8s 10.42.0.7 192.168.11.150
```
クラスタの外から、このクラスタにアクセスしてみます。次のコマンドで接続情報を確認します。
```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 < /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 < 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 8080/TCP 18s
service/room-reservation-db ClusterIP 10.43.13.215 5432/TCP 97s
service/room-reservation-db-agent ClusterIP None 97s
service/room-reservation-db-read-only ClusterIP 10.43.41.99 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 )にアクセスすると、次の画面が表示されます:

Ingressがない場合は、Serviceに対するport-forwardingでこのアプリ (http://localhost:8080) にアクセスします。
```bash
kubectl port-forward -n demo svc/room-reservation 8080:8080
```

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

この部屋の予約情報を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()\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()\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()\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()\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
```

* 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秒あたりに一瞬エラーが発生しています。

次のエラーログを確認できました。
```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()\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()\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()\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()\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アップデート時のダウンタイムをかなり小さく抑えられそうです。