Try   HackMD

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 を使って 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 のアップデートを行うことができます。

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)、Mahout はそれを勝手もやってくれます。便利!(3 回目)

アーキテクチャ

Mahout は Kubernetes コントローラです。よくあるコントローラと同様、Mahout ではカスタムリソースである Mastodon リソースを導入します。ユーザが Mastodon リソースを作ったり編集したりすると、Mahout がそれを検知し、必要な Kubernetes リソース(Deployment や Job など)を作ってくれます。

OCaml で Kubernetes API を叩く

Kubernetes のドキュメントを見ると:

オペレーター(すなわち、コントローラー)はどの言語/ランタイムでも実装でき、Kubernetes APIのクライアントとして機能させることができます。

と書いてありますが、公式のクライアントライブラリ が用意されているのは当然 Go や Java など一部の言語だけです。非公式のライブラリの一覧も同じページにありますが、残念ながら OCaml のクライアントライブラリはありません。というわけで、OCaml で Kubernetes コントローラを書くためには、まず Kubernetes API を叩くライブラリを整備する必要があります。

幸い、Kubernetes API は OpenAPI spec の形で記述されています(swagger.json)。OpenAPI には OCaml 用のコード生成器もあるので、これに食わせればいい……かと思いきや、そう話は簡単ではありません。

OpenAPI の OCaml 対応は正直微妙で、単純に Kubernetes の swagger.json を入力しても動く OCaml コードは出てきてくれません。これはコンパイルが通らないというのもそうですし、コンパイルできても動作が不良であるという点もありました。一例を挙げると、object 型の入力を Yojson.Safe.t ではなく (string * string) list だと思って展開してしまう(のでリクエストの JSON を正しくパーズできない)とか、フィールド名を勝手にスネークケースにしてしまう(のでリクエストの JSON を正しくパーズできない)とか。ということでなんやかんや修正しました。ついでに、Lwt の代わりに Eio を使うようにしました。Eio は OCaml 5.0 で導入された effect handler を使った非同期 I/O を提供するナウいライブラリです。Mahout では全体で Eio を使っています。

修正したコードはここで公開しています。Kubernetes 用に特化している部分があるので、OpenAPI 本体への contribution は見合わせています。

続く

続きます。