# Mahout: OCaml で Mastodon 用 K8s operator を書いて、NuSMV でモデル検査した
Mahout: A K8s operator for Mastodon, written in OCaml and model checked by NuSMV
OCaml で Mastodon 用の Kubernetes operator を書きました。Mahout という名前がついています。ついでに、operator のコアのロジック(reconciler の部分)について NuSMV でモデル検査しました。
## つくったもの
百聞は一見にしかずということで、Mahout がどんな感じで動くのかお見せしたいと思います。
### 前準備
Mahout には e2e テストがあって、Mahout のデモにはこの環境を利用するのが楽なのでこれを使います。Mahout のレポジトリを clone してきて `make create-cluster` を打つと [kind](https://github.com/kubernetes-sigs/kind) を使って Kubernetes クラスタを立ち上げてくれます:
```
$ git clone https://github.com/ushitora-anqou/mahout.git
$ cd mahout/e2e
$ make create-cluster
$ alias kubectl=${PWD}/_bin/kubectl-1.28.0
$ alias helm=${PWD}/_bin/helm-3.14.0
```
このクラスタでは、Mastodon が動作するために必要な PostgreSQL や Redis が動いています:
```
$ kubectl get pod -n e2e
NAME READY STATUS RESTARTS AGE
create-postgres-database-cp7bj 0/1 Completed 0 110s
postgres-0 1/1 Running 0 110s
redis-0 1/1 Running 0 110s
```
また、Mastodon が動作するために必要な環境変数の定義は `Secret` として保存されています:
```
$ kubectl get secret -n e2e secret-env -o yaml
apiVersion: v1
data:
DB_HOST: cG9zdGdyZXMuZTJlLnN2Yw==
DB_NAME: bWFzdG9kb25fcHJvZHVjdGlvbg==
DB_PASS: cGFzc3dvcmQ=
DB_PORT: NTQzMg==
DB_USER: bWFzdG9kb24=
...
```
### Mahout のインストール
Helm を使って Mahout をセットアップします:
```
$ helm install --repo https://ushitora-anqou.github.io/mahout --namespace mahout --name-template ket mahout
NAME: ket
LAST DEPLOYED: Wed Feb 28 21:13:16 2024
NAMESPACE: mahout
STATUS: deployed
REVISION: 1
TEST SUITE: None
$ kubectl get pod -n mahout
NAME READY STATUS RESTARTS AGE
ket-mahout-78f5565f7d-bzvg5 1/1 Running 0 10s
```
Mahout が立ちました。同時に、この Helm Chart は `Mastodon` リソースという CRD も導入しています:
```
$ kubectl get crd
NAME CREATED AT
mastodons.mahout.anqou.net 2024-02-28T12:36:28Z
```
### Mastodon サーバを建てる
これで準備ができたので、Mastodon サーバを立てましょう。Mahout が導入した `Mastodon` リソースを作ります。以下のようなマニフェストを適用します。
```
cat <<EOS | kubectl apply -f -
apiVersion: mahout.anqou.net/v1alpha1
kind: Mastodon
metadata:
name: mastodon
namespace: e2e
spec:
serverName: "mastodon.test"
image: "ghcr.io/mastodon/mastodon:v4.1.9"
envFrom:
- secretRef:
name: secret-env
gateway:
replicas: 1
image: "nginx:1.25.4"
sidekiq:
replicas: 1
streaming:
replicas: 1
web:
replicas: 1
EOS
```
すると Mahout が勝手に Mastodon 本体(web)や WebSocket サーバ(streaming)、Sidekiq などを立ち上げてくれて、サービスが開始されます。しばらく待っていると、それぞれの Pod が起動しているのがわかります:
```
$ kubectl get pod -n e2e
NAME READY STATUS RESTARTS AGE
create-postgres-database-cp7bj 0/1 Completed 0 4m47s
mastodon-gateway-nginx-6c6df7dff7-dqwdd 1/1 Running 0 40s
mastodon-post-migration-psf92 0/1 Completed 0 75s
mastodon-sidekiq-67f59bd75d-j7g58 1/1 Running 0 40s
mastodon-streaming-64bcb67b97-xxx5r 1/1 Running 0 40s
mastodon-web-66d9f6995f-57wzd 1/1 Running 0 40s
postgres-0 1/1 Running 0 4m47s
redis-0 1/1 Running 0 4m47s
```
立ち上がった Mastodon に接続してみます。gateway を port-forward して:
```
$ kubectl port-forward -n e2e svc/mastodon-gateway 8000:80
```
`/etc/hosts` に server name を追記します:
```
$ echo "127.0.0.1 mastodon.test" >> /etc/hosts
```
http://mastodon.test:8000/ にアクセスすると Mastodon の画面が映ります。便利!
`Mastodon` リソースを更に作れば更に別の Mastodon サーバを立てることができます。便利!(2 回目)
### Mastodon をバージョンアップする
Mastodon の新しいバージョンが公開されたら、`Mastodon` リソースの `image` フィールドを書き換えることで、Mastodon のアップデートを行うことができます。
```diff=
gateway:
image: nginx:1.25.4
replicas: 1
- image: ghcr.io/mastodon/mastodon:v4.1.9
+ image: ghcr.io/mastodon/mastodon:v4.2.0
serverName: mastodon.test
sidekiq:
replicas: 1
```
フィールドを書き換えると、Mahout がそれを検知して、勝手に DB マイグレーションを走らせてくれます。しばらくすると、Mastodon のバージョンが 4.2.0 に上がっていることを確認できます:
```
$ curl -H "Host: mastodon.test" http://localhost:8000/nodeinfo/2.0
{"version":"2.0","software":{"name":"mastodon","version":"4.2.0"},"protocols":["activitypub"],"services":{"outbound":[],"inbound":[]},"usage":{"users":{"total":0,"activeMonth":0,"activeHalfyear":0},"localPosts":0},"openRegistrations":true,"metadata":{}}
```
ちなみに、Mastodon のバージョンによってはマイグレーションを 2 度実行する必要があるのですが(参考:[Mastodon v4.2.0](https://github.com/mastodon/mastodon/releases/tag/v4.2.0))、Mahout はそれを勝手もやってくれます。便利!(3 回目)
## アーキテクチャ
Mahout は Kubernetes コントローラです。よくあるコントローラと同様、Mahout ではカスタムリソースである Mastodon リソースを導入します。ユーザが Mastodon リソースを作ったり編集したりすると、Mahout がそれを検知し、必要な Kubernetes リソース(Deployment や Job など)を作ってくれます。
## OCaml で Kubernetes API を叩く
[Kubernetes のドキュメント](https://kubernetes.io/ja/docs/concepts/extend-kubernetes/operator/)を見ると:
> オペレーター(すなわち、コントローラー)はどの言語/ランタイムでも実装でき、Kubernetes APIのクライアントとして機能させることができます。
と書いてありますが、[公式のクライアントライブラリ](https://kubernetes.io/docs/reference/using-api/client-libraries/) が用意されているのは当然 Go や Java など一部の言語だけです。非公式のライブラリの一覧も同じページにありますが、残念ながら OCaml のクライアントライブラリはありません。というわけで、OCaml で Kubernetes コントローラを書くためには、まず Kubernetes API を叩くライブラリを整備する必要があります。
幸い、Kubernetes API は OpenAPI spec の形で記述されています([swagger.json](https://github.com/kubernetes/kubernetes/blob/237d3dfda70d898e8b7641a295f5bdfecd527c2e/api/openapi-spec/swagger.json))。OpenAPI には OCaml 用のコード[生成器もある](https://github.com/OpenAPITools/openapi-generator/blob/4ba187a1a47e8901af18a38fd9a6a9cbaa471124/docs/generators/ocaml.md)ので、これに食わせればいい……かと思いきや、そう話は簡単ではありません。
OpenAPI の OCaml 対応は正直微妙で、単純に Kubernetes の swagger.json を入力しても動く OCaml コードは出てきてくれません。これはコンパイルが通らないというのもそうですし、コンパイルできても動作が不良であるという点もありました。一例を挙げると、`object` 型の入力を `Yojson.Safe.t` ではなく `(string * string) list` だと思って展開してしまう(のでリクエストの JSON を正しくパーズできない)とか、フィールド名を勝手にスネークケースにしてしまう(のでリクエストの JSON を正しくパーズできない)とか。ということでなんやかんや修正しました。ついでに、Lwt の代わりに [Eio](https://github.com/ocaml-multicore/eio) を使うようにしました。Eio は OCaml 5.0 で導入された effect handler を使った非同期 I/O を提供するナウいライブラリです。Mahout では全体で Eio を使っています。
修正したコードは[ここで公開しています](https://github.com/ushitora-anqou/openapi-generator)。Kubernetes 用に特化している部分があるので、OpenAPI 本体への contribution は見合わせています。
## 続く
続きます。