# 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 は見合わせています。 ## 続く 続きます。