ScalarDB Cluster 体験記

前回は ScalarDB のチュートリアルをこなした記事を書きました。

https://qiita.com/yebihara/items/d47f3e63fe213edfe917

今回は **ScalarDB Cluster** のチュートリアルをこなしていきます。

# ScalarDB Cluster とは
前回のチュートリアルでは ScalarDB を利用する際、「ScalarDB を起動する」のようなステップは存在しなかった。これはどういうことかというと、ScalarDB のデーモンプロセスのようなものは存在せず、ScalarDB の jar ファイルが Java で書かれたクライアントアプリケーションにリンクされ、APIコールの裏側でバックエンドDBにアクセスしていた、ということ。

一方、この記事でこれから試す ScalarDB Cluster では、Kubernetes クラスター内に ScalarDB のサーバープロセスが常駐し、クライアントアプリケーションはネットワーク経由で ScalarDB にアクセスすることになる。

なおこのクラスター構成はオープンソース版の ScalarDB では利用することができない。
今回はバイト先からライセンスを提供してもらってこの記事を書いている（バイト先公認）。

# チュートリアルに挑戦
ScalarDB Cluster についても Getting Started というチュートリアルが用意されているので、今回もこれをひと通りなぞってみる。

https://scalardb.scalar-labs.com/docs/latest/scalardb-cluster/getting-started-with-scalardb-cluster/

## チュートリアルの概要
Eコマースアプリケーションのサンプルを用いて、ユーザーの注文・支払いを ScalarDB で処理する。

全体アーキテクチャーはこんな感じ。
Kubernetes クラスターの中で ScalarDB Cluster と関連サービス、バックエンドDB（PostgreSQL)が動作していて、Eコマースアプリケーションはネットワーク経由でアクセスする。
DBスキーマの構築は前回と同様に Schema Loader (ただしCluster版) を使う。

```
+----------------+    +---------------+
| E-Commerce App |    | Schema Loader |
+----------------+    +---------------+
        |                     |
        +---------------------+
        |
+-------+-------------------------------------------------------------------------------------------------------------------------------+
|       |    [Kubernetes Cluster]                                                                                                       |
|       |                                                                                                                               |
|       |                 [Pod]                                                                [Pod]                         [Pod]      |
|       |                                                                                                                               |
|       |               +-------+                                                                                                       |
|       |         +---> | Envoy | ---+                                                                                                  |
|       |         |     +-------+    |                                                                                                  |
|       |         |                  |                                                                                                  |
|  +---------+    |     +-------+    |           +--------------------+                                                                 |
|  | Service | ---+---> | Envoy | ---+---------> |      Service       | ---+                                                            |
|  | (Envoy) |    |     +-------+    |           | (ScalarDB Cluster) |    |                                                            |
|  +---------+    |                  |           +--------------------+    |         +-----------------------+                          |
|                 |     +-------+    |                                     |   +---> | ScalarDB Cluster Node | ---+                     |
|                 +---> | Envoy | ---+                                     |   |     +-----------------------+    |                     |
|                       +-------+                                          |   |                                  |                     |
|                                                                          |   |     +-----------------------+    |     +------------+  |
|                                                                          +---+---> | ScalarDB Cluster Node | ---+---> | PostgreSQL |  |
|                                                                          |   |     +-----------------------+    |     +------------+  |
|                                                                          |   |                                  |                     |
|                                                                          |   |     +-----------------------+    |                     |
|                                                                          |   +---> | ScalarDB Cluster Node | ---+                     |
|                                                                          |         +-----------------------+                          |
|                                        +----------------------------+    |                                                            |
|                                        |          Service           | ---+                                                            |
|                                        | (ScalarDB Cluster GraphQL) |                                                                 |
|                                        +----------------------------+                                                                 |
|                                                                                                                                       |
+---------------------------------------------------------------------------------------------------------------------------------------+
```

なお、今回も比較的きれいな Mac 環境で試していく。

## 前提条件
ScalarDB と同様に OpenJDK か Oracle JDK の 8, 11, 17 が必要。

そして Kubernetes クラスター上で動作する ScalarDB Cluster 環境。つまり上の図の `[Kubernetes Cluster]` で囲まれているところすべて。

え？ それってすごく大変なのでは…？

## 1. Kubernetes クラスターの作成
ScalarDB Cluster を動かす Kubernetes クラスターは、プロダクション環境向けには Amazon EKS などのサービスを利用することになるのだろうが、検証目的であればすべてローカルマシン内で完結させられる。

そしてその手順は次のページにガイドがあり、比較的簡単に構築することができる。

https://scalardb.scalar-labs.com/docs/latest/scalardb-cluster/setup-scalardb-cluster-on-kubernetes-by-using-helm-chart/


### 1-0. 前提ソフトウェアのインストール
ローカルで Kubernetes クラスターを動かすためのソフトウェアとして [minikube](https://minikube.sigs.k8s.io/docs/)、または [kind](https://kind.sigs.k8s.io/) を利用する。今回は minikube を使うことにする。

加えて [kubectl](https://kubernetes.io/ja/docs/reference/kubectl/) と [helm](https://helm.sh/ja/) も必要。

すべて Homebrew でインストールできる。

```
$ brew install minikube
$ brew install kubectl
$ brew install helm
```

終わったら minikube を起動しておく（ずいぶんポップなログだな）。

```
$ $ minikube start
😄  Darwin 13.3 (arm64) 上の minikube v1.33.1
✨  docker ドライバーが自動的に選択されました
📌  root 権限を持つ Docker Desktop ドライバーを使用
👍  Starting "minikube" primary control-plane node in "minikube" cluster
🚜  Pulling base image v0.0.44 ...
🔥  Creating docker container (CPUs=2, Memory=6100MB) ...
🐳  Docker 26.1.1 で Kubernetes v1.30.0 を準備しています...
    ▪ 証明書と鍵を作成しています...
    ▪ コントロールプレーンを起動しています...
    ▪ RBAC のルールを設定中です...
🔗  bridge CNI (コンテナーネットワークインターフェース) を設定中です...
🔎  Kubernetes コンポーネントを検証しています...
    ▪ gcr.io/k8s-minikube/storage-provisioner:v5 イメージを使用しています
🌟  有効なアドオン: storage-provisioner, default-storageclass
🏄  終了しました！kubectl がデフォルトで「minikube」クラスターと「default」ネームスペースを使用するよう設定されました
```

### 1-1. PostgreSQL コンテナを起動
まずは Bitnami の Helm リポジトリを追加（Bitnamiって何だ？）。

```
$ helm repo add bitnami https://charts.bitnami.com/bitnami
```

そして PostgreSQL をデプロイ。

```
$ helm install postgresql-scalardb-cluster bitnami/postgresql --set auth.postgresPassword=postgres --set primary.persistence.enabled=false
...
```

長いメッセージが出てくるのだが、基本的には起動された DB に psql コマンドでアクセスする方法について説明しているだけだ。
ここではその説明よりもシンプルに psql を実行する方法を紹介しておく。

```
$ kubectl exec -it postgresql-scalardb-cluster-0 -- bash -c 'PGPASSWORD=postgres psql -U postgres'
```

これは起動した　PostgreSQL コンテナの Pod の中で psql コマンドを実行している。
パスワードが直書きだが、このパスワードはそもそも先ほど `helm install` したときにコマンドラインで指定したものなので、今さら隠す意味はない。

いちおう DB の中身を確認しておこう。

```
postgres=# \dn
      List of schemas
  Name  |       Owner
--------+-------------------
 public | pg_database_owner
(1 row)

postgres=# \dt public.*
Did not find any relation named "public.*".
```

唯一存在する `public` スキーマにはテーブルはひとつも定義されていない。

### 1-2. ScalarDB Cluster をデプロイ
ここから先は ScalarDB の有償ライセンスが必要になる。そこらへんの説明は割愛。

まずは Helm に ScalarDB のチャートリポジトリを追加。

```
$ helm repo add scalar-labs https://scalar-labs.github.io/helm-charts
```

ScalarDB Cluster のコンテナイメージを GitHub Packages からプルするために必要な k8s シークレットリソースを作成する。

```
$ kubectl create secret docker-registry reg-docker-secrets --docker-server=ghcr.io --docker-username=<YOUR_GITHUB_USERNAME> --docker-password=<YOUR_PERSONAL_ACCESS_TOKEN>
```

`<YOUR_GITHUB_USERNAME>` は、自分の GitHub アカウント名に置き換える。
`<YOUR_PERSONAL_ACCESS_TOKEN>` には GitHub で作成した Personal Access Token (PAT) 文字列をセット。
PAT を作成するときのスコープには `read:packages` を含める必要がある。

以下の長〜い複数行コマンドを実行して、`scalardb-cluster-custom-values.yaml` というファイルを作成。

```
$ cat << 'EOF' > scalardb-cluster-custom-values.yaml
envoy:
  enabled: true
  service:
    type: "LoadBalancer"

scalardbCluster:
  scalardbClusterNodeProperties: |
    # ScalarDB Cluster configurations
    scalar.db.cluster.membership.type=KUBERNETES
    scalar.db.cluster.membership.kubernetes.endpoint.namespace_name=${env:SCALAR_DB_CLUSTER_MEMBERSHIP_KUBERNETES_ENDPOINT_NAMESPACE_NAME}
    scalar.db.cluster.membership.kubernetes.endpoint.name=${env:SCALAR_DB_CLUSTER_MEMBERSHIP_KUBERNETES_ENDPOINT_NAME}

    # Storage configurations
    scalar.db.storage=jdbc
    scalar.db.contact_points=jdbc:postgresql://postgresql-scalardb-cluster.default.svc.cluster.local:5432/postgres
    scalar.db.username=postgres
    scalar.db.password=postgres

    # For ScalarDB Cluster GraphQL tutorial.
    scalar.db.graphql.enabled=true
    scalar.db.graphql.namespaces=emoney

    # For ScalarDB Cluster SQL tutorial.
    scalar.db.sql.enabled=true
  graphql:
    enabled: true
    service:
      type: "LoadBalancer"
EOF
```

そして ScalarDB Cluster をデプロイ。

```
$ helm install scalardb-cluster scalar-labs/scalardb-cluster -f ./scalardb-cluster-custom-values.yaml
```

ポッドの状況を確認。しばらく経つと以下のような構成となり、全てのポッドのステータスが Running となるはず。

```
$ kubectl get pods
NAME                                      READY   STATUS    RESTARTS   AGE
postgresql-scalardb-cluster-0             1/1     Running   0          26m
psql                                      1/1     Running   0          10m
scalardb-cluster-envoy-547dbbbf4b-cv4hx   1/1     Running   0          9m46s
scalardb-cluster-envoy-547dbbbf4b-hqq2b   1/1     Running   0          9m46s
scalardb-cluster-envoy-547dbbbf4b-wcrft   1/1     Running   0          9m46s
scalardb-cluster-node-6896885945-2rnzb    1/1     Running   0          9m46s
scalardb-cluster-node-6896885945-md2t7    1/1     Running   0          9m46s
scalardb-cluster-node-6896885945-qfcpx    1/1     Running   0          9m46s
```

ここでデプロイされた ScalarDB Cluster のバージョンを確認しておこう。
3つある `scalardb-cluster-node-*` という名前のポッドのうち、任意のひとつの名前を引数に次のコマンドを実行する。

```
$ kubectl get pod scalardb-cluster-node-6896885945-qfcpx  -o jsonpath='{.spec.containers[*].image}'
ghcr.io/scalar-labs/scalardb-cluster-node:3.13.0
```

バージョンは `3.13.0` だ。この情報は後の手順で参照するので覚えておく。

ロードバランサーサービス向けに別ターミナルを開いて `minikube tunnel` コマンドを実行。
minikube を利用したローカル k8s クラスターでは、これをやらないと k8s クラスター内のロードバランサーにアクセスできない。

```
$ minikube tunnel
✅  トンネルが無事開始しました

📌  注意: トンネルにアクセスするにはこのプロセスが存続しなければならないため、このターミナルはクローズしないでください ...

🏃  scalardb-cluster-envoy サービス用のトンネルを起動しています。
🏃  scalardb-cluster-graphql サービス用のトンネルを起動しています。
```

このコマンドは Ctrl-C するまで実行中のままとなる。

元のターミナルに戻ってサービスの状況を確認。

```
$ kubectl get svc
NAME                             TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)           AGE
kubernetes                       ClusterIP      10.96.0.1        <none>        443/TCP           16h
postgresql-scalardb-cluster      ClusterIP      10.107.134.66    <none>        5432/TCP          29m
postgresql-scalardb-cluster-hl   ClusterIP      None             <none>        5432/TCP          29m
scalardb-cluster-envoy           LoadBalancer   10.108.134.40    127.0.0.1     60053:31087/TCP   12m
scalardb-cluster-envoy-metrics   ClusterIP      10.110.174.15    <none>        9001/TCP          12m
scalardb-cluster-graphql         LoadBalancer   10.100.115.226   127.0.0.1     8080:32195/TCP    12m
scalardb-cluster-headless        ClusterIP      None             <none>        60053/TCP         12m
scalardb-cluster-metrics         ClusterIP      10.102.96.82     <none>        9080/TCP          12m
```

チュートリアルの例では LoadBalancer の EXTERNAL-IP が `localhost` になっているが、自分の環境では `127.0.0.1` となった。まあでも大きな違いはなさそうなので、細かいことは気にせず先へ進むことにする。

ここまででようやく ScalarDB Cluster チュートリアルの前提条件を満たすことができたので、チュートリアル本編に戻ってシナリオを続行する。

## 2. ScalarDB サンプルリポジトリをクローン
サンプルのEコマースアプリケーションと関連ファイルが詰まったリポジトリをローカルにクローンする。

```
$ git clone https://github.com/scalar-labs/scalardb-samples
```

リポジトリ内には `*-sample` というディレクトリがたくさんあるが、このチュートリアルではそのうちの `scalardb-sample` を利用するので、そこに移動する。

```
$ cd scalardb-samples/scalardb-sample
```

## 3. build.gradle ファイルを修正
サンプルリポジトリは元々、非クラスター構成の ScalarDB で動かす設定になっているので、これを ScalarDB Cluster で使えるようビルド設定を変更する。

変更内容は以下の diff 参照。

```diff
$ git diff build.gradle
diff --git a/scalardb-sample/build.gradle b/scalardb-sample/build.gradle
index e9af1fb..34da5ed 100644
--- a/scalardb-sample/build.gradle
+++ b/scalardb-sample/build.gradle
@@ -11,7 +11,7 @@ repositories {
 }

 dependencies {
-    implementation 'com.scalar-labs:scalardb:3.9.1'
+    implementation 'com.scalar-labs:scalardb-cluster-java-client-sdk:3.13.0'
     implementation 'info.picocli:picocli:4.7.1'
 }
```

`com.scalar-labs:scalardb-cluster-java-client-sdk:3.13.0` の中の `3.13.0` の部分は、先ほど確認した ScalarDB Cluster のバージョンに合わせて置き換える。

## 4. database.properties ファイルを修正
`database.properties` というのは、クライアントアプリケーションから ScalarDB への接続情報を記述する設定ファイル。これも ScalarDB Cluster 用に変更する。

まずはアプリケーションの直接の通信先となる Envoy サービス（先の構成図参照）のアドレスを、次のようにして取得する。

```
$ kubectl get svc scalardb-cluster-envoy
NAME                     TYPE           CLUSTER-IP       EXTERNAL-IP   PORT(S)           AGE
scalardb-cluster-envoy   LoadBalancer   10.107.213.118   127.0.0.1     60053:32378/TCP   12h
```

`EXTERNAL-IP` で示される `127.0.0.1` がそのアドレス。
そして `database.properties` を以下のように書き換える。

```diff
$ git diff database.properties
diff --git a/scalardb-sample/database.properties b/scalardb-sample/database.properties
index a44993a..5cf4abb 100644
--- a/scalardb-sample/database.properties
+++ b/scalardb-sample/database.properties
@@ -1,4 +1,2 @@
-scalar.db.storage=cassandra
-scalar.db.contact_points=localhost
-scalar.db.username=cassandra
-scalar.db.password=cassandra
+scalar.db.transaction_manager=cluster
+scalar.db.contact_points=indirect:127.0.0.1
```

先ほど確認したアドレスは `scalar.db.contact_points` に `indirect:127.0.0.1` のような形式で指定する。
ここで `indirect` というのは**クライアントモード**を示している。

クライアントモードには `indirect` と `direct-kubernetes` の２種類がある。ざっくり説明すると、`indirect` モードでは k8s クラスターの外に配置されたアプリケーションが Envoy サービス経由で ScalarDB Cluster にアクセスする。`direct-kubernetes` モードではアプリケーションと ScalarDB Cluster を同一の k8s クラスター内に配置し、より効率的なアクセスが可能になる。これ以上の細かい話は[別のページ](https://scalardb.scalar-labs.com/docs/latest/scalardb-cluster/developer-guide-for-scalardb-cluster-with-java-api/#client-modes)を参照されたし。

で、このチュートリアルでは `indirect` モードを利用する。

## 5. データベーススキーマの作成
DBスキーマは `schema.json` ファイルに定義されている。その内容については後述。

このスキーマ定義を元に ScalarDB のデータベースとテーブルを作成するには、**ScalarDB Cluster Schema Loader** というツールを利用する。
前回の非クラスター構成の ScalarDB で利用した ScalarDB Schema Loader は名前がちょっと違う別のツールなので注意。

ScalarDB Cluster Schema Loader は以下のページからダウンロードできる。

https://github.com/scalar-labs/scalardb/releases

ダウンロードするのは `scalardb-cluster-schema-loader-<version>-all.jar`。
`<version>` の部分は、先ほど確認した ScalarDB Cluster のバージョンに従う。

これを `database.properties` や `schema.json` と同じ `scalardb-samples/scalardb-sample` ディレクトリに保存する。

そして `database.properties` と `schema.json` を引数に ScalarDB Cluster Schema Loader を実行。

```shell-session
$ java -jar scalardb-cluster-schema-loader-3.13.0-all.jar --config database.properties -f schema.json --coordinator
[main] INFO com.scalar.db.schemaloader.command.SchemaLoaderCommand - Config path: database.properties
[main] INFO com.scalar.db.schemaloader.command.SchemaLoaderCommand - Schema path: schema.json
[main] INFO com.scalar.db.cluster.common.ClusterNodeManager - Local IP addresses: [fe80:0:0:0:ce81:b1c:bd2c:69e%utun2, fe80:0:0:0:ce3f:5330:f35a:e6b5%utun1, fe80:0:0:0:d6cb:351c:550e:46e3%utun0, fe80:0:0:0:9863:97ff:feda:a21e%llw0, fe80:0:0:0:9863:97ff:feda:a21e%awdl0, fe80:0:0:0:4008:edff:feae:5644%anpi0, fe80:0:0:0:4008:edff:feae:5645%anpi1, fe80:0:0:0:8f0:9204:d36:e2dd%en5, 124.34.222.219, fdfe:1cff:ac75:c24d:1460:b321:c393:d620%en0, fe80:0:0:0:4bb:c15b:3025:b301%en0, 192.168.0.25, fe80:0:0:0:0:0:0:1%lo0, 0:0:0:0:0:0:0:1%lo0, 127.0.0.1]
[main] INFO com.scalar.db.cluster.common.ConsistentHashingDistribution - Refreshed the ring for [127.0.0.1]
[main] INFO com.scalar.db.schemaloader.SchemaOperator - Creating the table customers in the namespace sample succeeded
[main] INFO com.scalar.db.schemaloader.SchemaOperator - Creating the table orders in the namespace sample succeeded
[main] INFO com.scalar.db.schemaloader.SchemaOperator - Creating the table statements in the namespace sample succeeded
[main] INFO com.scalar.db.schemaloader.SchemaOperator - Creating the table items in the namespace sample succeeded
[main] INFO com.scalar.db.schemaloader.SchemaOperator - Creating the coordinator tables succeeded
```

成功すると PostgreSQL DB に以下のようなテーブルが作成されるらしい。注文と信用枠を管理する感じですかね。

![サンプルスキーマのER図](https://scalardb.scalar-labs.com/assets/images/ERD-65f1e89a1a48a5b8b05a712ad99ad331.png "サンプルスキーマのER図")
（チュートリアルより引用）

データベースを確認してみよう。

```
$ kubectl run psql --rm -it --image docker.io/bitnami/postgresql:16.3.0-debian-12-r14 --env="PGPASSWORD=postgres" --command -- psql --host postgresql-scalardb-cluster -U postgres -d postgres -p 5432

postgres=# \dn
         List of schemas
    Name     |       Owner
-------------+-------------------
 coordinator | postgres
 public      | pg_database_owner
 sample      | postgres
 scalardb    | postgres
(4 rows)

postgres=# \dt sample.*
           List of relations
 Schema |    Name    | Type  |  Owner
--------+------------+-------+----------
 sample | customers  | table | postgres
 sample | items      | table | postgres
 sample | orders     | table | postgres
 sample | statements | table | postgres
(4 rows)
```

`coordinator`, `sample`, `scalardb` の３つのスキーマができた。`coordinator` と `scalardb` は前回確認したものと一緒だろう。
そして `sample` スキーマにはER図どおりの4つのテーブルが作成されている。

## 6. 初期データのロード
まだテーブルを作成しただけで全てのテーブルは空っぽなので、マスター系のテーブル(`customers` と `items`)に初期データをロードしていく。

```
$ ./gradlew run --args="LoadInitialData"
Starting a Gradle Daemon (subsequent builds will be faster)
...
BUILD SUCCESSFUL in 10s
2 actionable tasks: 2 executed
```

データを確認。

```
postgres=# select * from sample.customers ;
 customer_id |     name      | credit_limit | credit_total |                tx_id                 | tx_state | tx_version | tx_prepared_at | tx_committed_at | before_tx_id | before_tx_state | before_tx_version | before_tx_prepared_at | before_tx_committed_at | before_name | before_credit_limit | before_credit_total
-------------+---------------+--------------+--------------+--------------------------------------+----------+------------+----------------+-----------------+--------------+-----------------+-------------------+-----------------------+------------------------+-------------+---------------------+---------------------
           1 | Yamada Taro   |        10000 |            0 | 38681f6a-ecda-4320-9c56-fd97b5559af5 |        3 |          1 |  1720760130050 |   1720760130497 |              |                 |                   |                       |                        |             |                     |
           3 | Suzuki Ichiro |        10000 |            0 | 38681f6a-ecda-4320-9c56-fd97b5559af5 |        3 |          1 |  1720760130050 |   1720760130497 |              |                 |                   |                       |                        |             |                     |
           2 | Yamada Hanako |        10000 |            0 | 38681f6a-ecda-4320-9c56-fd97b5559af5 |        3 |          1 |  1720760130050 |   1720760130497 |              |                 |                   |                       |                        |             |                     |
(3 rows)

postgres=# select * from sample.items ;
 item_id |  name  | price |                tx_id                 | tx_state | tx_version | tx_prepared_at | tx_committed_at | before_tx_id | before_tx_state | before_tx_version | before_tx_prepared_at | before_tx_committed_at | before_price | before_name
---------+--------+-------+--------------------------------------+----------+------------+----------------+-----------------+--------------+-----------------+-------------------+-----------------------+------------------------+--------------+-------------
       2 | Orange |  2000 | 38681f6a-ecda-4320-9c56-fd97b5559af5 |        3 |          1 |  1720760130050 |   1720760130497 |              |                 |                   |                       |                        |              |
       1 | Apple  |  1000 | 38681f6a-ecda-4320-9c56-fd97b5559af5 |        3 |          1 |  1720760130050 |   1720760130497 |              |                 |                   |                       |                        |              |
       4 | Mango  |  5000 | 38681f6a-ecda-4320-9c56-fd97b5559af5 |        3 |          1 |  1720760130050 |   1720760130497 |              |                 |                   |                       |                        |              |
       3 | Grape  |  2500 | 38681f6a-ecda-4320-9c56-fd97b5559af5 |        3 |          1 |  1720760130050 |   1720760130497 |              |                 |                   |                       |                        |              |
       5 | Melon  |  3000 | 38681f6a-ecda-4320-9c56-fd97b5559af5 |        3 |          1 |  1720760130050 |   1720760130497 |              |                 |                   |                       |                        |              |
(5 rows)
```

これでようやくサンプルアプリケーションを実行する環境が整った。

## 7. サンプルアプリケーションでトランザクション実行

### 7-1. 顧客情報の参照
ID=1 の顧客の情報。

```
$ ./gradlew run --args="GetCustomerInfo 1"

...
{"id": 1, "name": "Yamada Taro", "credit_limit": 10000, "credit_total": 0}
...
```

名前は Yamada Taro さん。
信用枠は 10000 円で、現在はまだ枠を消費していない。

### 7-2. 注文登録
ID=1 の Yamada さんがリンゴを３つ、オレンジを２つ注文。
リンゴは １個 1000円、オレンジは １個 2000円なので、合計７０００円。

`items` テーブルによると、リンゴのIDは 1 で、オレンジは 2、呪文のようなコマンドだがサンプルアプリケーションなのでぜいたくは言わない。

```
$ ./gradlew run --args="PlaceOrder 1 1:3,2:2"

...
{"order_id": "da92b8db-3c73-4236-a3d2-33b03d4be371"}
...
```

注文は `da92b8db-3c73-4236-a3d2-33b03d4be371` という ID で登録された。

### 7-3. 注文確認
ID を利用して注文詳細を確認してみる。

```
$ ./gradlew run --args="GetOrder da92b8db-3c73-4236-a3d2-33b03d4be371"

...
{"order": {"order_id": "da92b8db-3c73-4236-a3d2-33b03d4be371","timestamp": 1720760994700,"customer_id": 1,"customer_name": "Yamada Taro","statement": [{"item_id": 1,"item_name": "Apple","price": 1000,"count": 3,"total": 3000},{"item_id": 2,"item_name": "Orange","price": 2000,"count": 2,"total": 4000}],"total": 7000}}
...
```

分からん。JSONを整形してみよう。

```
{
   "order": {
      "order_id": "da92b8db-3c73-4236-a3d2-33b03d4be371",
      "timestamp": 1720760994700,
      "customer_id": 1,
      "customer_name": "Yamada Taro",
      "statement": [
         {
            "item_id": 1,
            "item_name": "Apple",
            "price": 1000,
            "count": 3,
            "total": 3000
         },
         {
            "item_id": 2,
            "item_name": "Orange",
            "price": 2000,
            "count": 2,
            "total": 4000
         }
      ],
      "total": 7000
   }
}
```

### 7-4. 注文登録 その２
Yamada さんが追加でメロンを1つ注文してくれた。メロンは 1個 3000 円だ。

```
$ ./gradlew run --args="PlaceOrder 1 5:1"

...
{"order_id": "22f327b7-9bc5-465e-95ae-331cd9edfc0e"}
...
```

### 7-5. 注文履歴の確認
Yamada さんの注文履歴を確認してみる。

```
$ ./gradlew run --args="GetOrders 1"

...
{"order": [{"order_id": "da92b8db-3c73-4236-a3d2-33b03d4be371","timestamp": 1720760994700,"customer_id": 1,"customer_name": "Yamada Taro","statement": [{"item_id": 1,"item_name": "Apple","price": 1000,"count": 3,"total": 3000},{"item_id": 2,"item_name": "Orange","price": 2000,"count": 2,"total": 4000}],"total": 7000},{"order_id": "22f327b7-9bc5-465e-95ae-331cd9edfc0e","timestamp": 1720761558116,"customer_id": 1,"customer_name": "Yamada Taro","statement": [{"item_id": 5,"item_name": "Melon","price": 3000,"count": 1,"total": 3000}],"total": 3000}]}
...
```

整形。

```
{
   "order": [
      {
         "order_id": "da92b8db-3c73-4236-a3d2-33b03d4be371",
         "timestamp": 1720760994700,
         "customer_id": 1,
         "customer_name": "Yamada Taro",
         "statement": [
            {
               "item_id": 1,
               "item_name": "Apple",
               "price": 1000,
               "count": 3,
               "total": 3000
            },
            {
               "item_id": 2,
               "item_name": "Orange",
               "price": 2000,
               "count": 2,
               "total": 4000
            }
         ],
         "total": 7000
      },
      {
         "order_id": "22f327b7-9bc5-465e-95ae-331cd9edfc0e",
         "timestamp": 1720761558116,
         "customer_id": 1,
         "customer_name": "Yamada Taro",
         "statement": [
            {
               "item_id": 5,
               "item_name": "Melon",
               "price": 3000,
               "count": 1,
               "total": 3000
            }
         ],
         "total": 3000
      }
   ]
}
```

### 7-6. 売掛金額の確認
Yamada さんはこれまでに10000円分の注文をしたが、まだお金を支払っていない。
現在の売掛金合計を確認する。

```
$ ./gradlew run --args="GetCustomerInfo 1"

...
{"id": 1, "name": "Yamada Taro", "credit_limit": 10000, "credit_total": 10000}
...
```

信用枠10000円を使い切ってしまっている。
この状態でさらにブドウとマンゴーを1つずつ、計7500円の注文をしてみる。

```
$ ./gradlew run --args="PlaceOrder 1 3:1,4:1"

...
java.lang.RuntimeException: Credit limit exceeded
        at sample.Sample.placeOrder(Sample.java:204)
        at sample.command.PlaceOrderCommand.call(PlaceOrderCommand.java:33)
        at sample.command.PlaceOrderCommand.call(PlaceOrderCommand.java:8)
        at picocli.CommandLine.executeUserObject(CommandLine.java:2041)
        at picocli.CommandLine.access$1500(CommandLine.java:148)
        at picocli.CommandLine$RunLast.executeUserObjectOfLastSubcommandWithSameParent(CommandLine.java:2461)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2453)
        at picocli.CommandLine$RunLast.handle(CommandLine.java:2415)
        at picocli.CommandLine$AbstractParseResultHandler.execute(CommandLine.java:2273)
        at picocli.CommandLine$RunLast.execute(CommandLine.java:2417)
        at picocli.CommandLine.execute(CommandLine.java:2170)
        at sample.command.SampleCommand.main(SampleCommand.java:35)
...
```

`Credit limit exceeded`（信用枠超過）というエラーになってしまった。

### 7-7. 支払い
Yamada さんは買掛残 10000円のうち、8000円を支払ってくれた。

```
$ ./gradlew run --args="Repayment 1 8000"
...
```

枠を確認。

```
$ ./gradlew run --args="GetCustomerInfo 1"

...
{"id": 1, "name": "Yamada Taro", "credit_limit": 10000, "credit_total": 2000}
...
```

買掛残が2000円に減った。
では改めてブドウとマンゴーの注文を入れてみよう。

```
$ ./gradlew run --args="PlaceOrder 1 3:1,4:1"

...
{"order_id": "0d84e309-fc2c-43e3-ba3f-f85f74df0119"}
...
```

今度は成功！

チュートリアルのシナリオはここまで！

## 8． お片付け
起動した ScalarDB Cluster と PostgreSQL を停止。

```
$ helm uninstall scalardb-cluster postgresql-scalardb-cluster
```

`minikube tunnel` を実行しっぱなしになっているターミナルは Ctrl-C。

最後に k8s クラスターを削除。

```
$ minikube delete
🔥  docker の「minikube」を削除しています...
🔥  コンテナー「minikube」を削除しています...
🔥  /Users/pc-0056_ebihara/.minikube/machines/minikube を削除しています...
💀  クラスター「minikube」の全てのトレースを削除しました。
```

# まとめ
ScalarDB Cluster を Kubernetes クラスターの中で動作させることができました。

実のところ自分は k8s に関しては、他の人が構築したクラスターをちょこちょこ触る程度の経験しかなかったのですが、一連の作業を通じて k8s リソースの種類や役割、kubectl コマンドの基本的な使い方について学べたのは想定外に大きな収穫でした。ラッキー！
ただ helm についてはまだちょっとよく分からないですね…。

非クラスターな ScalarDB 用のサンプルアプリケーションが、ビルド設定と接続設定を変更するだけで、コード修正なしに ScalarDB Cluster でも動作しました。つまり、ScalarDB の構成の違いは、クライアントアプリケーションコードに対して透過的ということです。

なお、今回のチュートリアルの内容だけだと、素の ScalarDB に対する ScalarDB Cluster の有用性をあまり感じられないかもしれません。
別途勉強したところによると、ScalarDB Cluster では gRPC や GraphQL、SQL といった API が利用でき、Java や JVM 言語以外のプログラミング言語による開発が可能なようです。
また認証・認可など、ScalarDB Cluster でのみ提供されるエンタープライズ向け機能もあるということです。

# おまけ: ホストマシンから PostgreSQL への便利な接続方法
k8s で起動した PostgreSQL にアクセスするのにいちいち psql 用の Pod を起動するのはタルいので、自分はホストマシンで起動した psql から k8s クラスター内の PostgreSQL DB に接続できるようにしておきたい。

まずは PostgreSQL クライアントをインストール。

```
$ brew install libpq
```

.zshrc にパスとデフォルトの接続情報を追加。

```
export PATH=/opt/homebrew/opt/libpq/bin:$PATH

export PGHOST=127.0.0.1
export PGPORT=5432
export PGDATABASE=postgres
export PGUSER=postgres
```

`~/.pgpass` ファイルを作成(パーミッションは 600 でないと psql がエラーになる)。

```
$ echo "127.0.0.1:5432:postgres:postgres:postgres" > ~/.pgpass
$ chmod 600 ~/.pgpass
```

k8s のポートフォワードを設定。ホストマシンの5432番ポートへのアクセスが、PostgreSQLが起動するPodの5432番ポートにフォワードされるようにする。

```
$ kubectl port-forward --namespace default svc/postgresql-scalardb-cluster 5432:5432
```

`kubectl port-forward` を単純に実行すると、ポートフォワードしている間はずっとコマンドが実行中になってしまい制御が返ってこない。
このターミナルはポートフォワード専用にして、psql の実行自体は別のターミナルから行うか、`kubectl port-forward` コマンドラインの最後に `&` を付けてバックグラウンド実行してしまえばよい。

これでホストのターミナルから `psql` とタイプするだけで、クラスター内の PostgreSQL にアクセスできるようになる。

```
$ psql
psql (16.3)
Type "help" for help.

postgres=#
```