Kubernetesで隔離Mastodonネットワークを作った
はじめに
前回、 k3s の紹介をしましたが、もともと Kubernetes に興味を持ったのは Docker のネットワークが、簡単だけれど細かい設定ができないという問題を抱えていたからです。細かく設定したネットワーク環境を使って何がやりたかったかというと、 Mastodon をはじめとする、 ActivityPub サーバーを隔離環境で動かし、他人のサーバーに迷惑をかけずに通信を眺めたかったのです。さらに、それを使って ActivityPub サーバーを自作するところまで行きたいのですが、正直もう体力と時間がないので、他に作っている方の動向だけ眺めていることにします……。
というわけで、要件はこんな感じです。
- Mastodon サーバーを 2 つ立ち上げ、相互に通信できるようにする
- 実環境に近くするため HTTPS で通信する
- サーバー名として「mastodon1.fediverse.local.azyobuzi.net」、「mastodon2.fediverse.local.azyobuzi.net」を割り当て、解決できるようにする
- 隔離ネットワーク外のサーバーにアクセスできないようにする
- Mastodon サーバー間の通信内容を簡単に覗き見ることができるようにする
Kubernetes で Mastodon を構築した!といった記事は星の数ほどあると思いますので、この記事のポイントを挙げると、ローカル限定の隔離ネットワークであるところと、サーバー間の通信を監視できる UI を用意したところです。
先に完成品をお見せしておきます。トゥートしたりふぁぼったりすると、相手サーバーにリクエストが飛んでいることが確認できます。
ここまでできて、とりあえずやりきった感が強く、飽きたので、あとはこれを読んだみなさんが、より強いシステムを作って、 ActivityPub サーバーの開発に役立てていただけると幸いです。
このブログ記事では、構成要素や、 k3s での構築について書き残しておきたいと思います。 k3s に限らない構築方法については、 GitHub に置いておきます。
構成
まずはこんな感じにしたいという構成を確認しておきます。
矢印は、 HTTP リクエストを送信する方向を表しています。 Mastodon サーバーへ向かうすべてのリクエストは、 Ingress Controller を通過します。 Ingress Controller は、 TLS ターミネーションプロキシとして働き、 Host ヘッダーを見て、転送先のサービスを決定します。監視用プロキシは、通過するリクエストとレスポンスを記録し、ログを確認できる GUI を提供します。
さて、このような構成にすると、 DNS もひと工夫必要です。それぞれのサーバーに対して、「mastodon1.fediverse.local.azyobuzi.net」、「mastodon2.fediverse.local.azyobuzi.net」を割り当てるので、 Kubernetes クラスタ内から「*.fediverse.local.azyobuzi.net」を解決するときは、 Ingress Controller の IP アドレスを返す必要があります。一方、ホストマシンから解決するときは、クラスタのノード(今回は仮想マシン)の IP アドレスを返す必要があります。前者については、 Kubernetes にデプロイされている CoreDNS の設定でなんとかします。後者は、自宅ネットワーク内に配備してある DNS サーバーを設定します(用意しておいてよかった。やっとまともに使う日が来た)。自宅 DNS を用意するのが面倒ならば、 hosts ファイルを書き換えるだけでもいいでしょう。
k3s で Network Policy
構成図には書き込めませんでしたが、今回の目標として、外部のサーバーとの通信を禁止することがありました。 Kubernetes では Network Policy を使って、 Pod 単位やアドレス範囲で、通信の許可・禁止を制御することができます。細かく制御できるとはいえ、ノードは LAN 内で動いており、 WAN からはアクセスできないので、今回禁止するべき通信は、 Mastodon が動いている Pod からインターネットへの通信です。 YAML で表すと、このようなポリシーになります。
apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: mastodon1-no-outgoing namespace: fediverse spec: podSelector: matchLabels: # Mastodon 1 に関連する Pod にこのラベルを付けておく fediverse/app: mastodon1 egress: # k3s の初期設定では # * 10.42.0.0/16 クラスタネットワーク # * 10.43.0.0/16 サービスネットワーク # なので、 10.42.0.0/15 でこれらを指定できる - to: - ipBlock: cidr: 10.42.0.0/15 # 内向き(Ingress)はすべて許可 # 外向き(Egress)は指定した範囲のみ許可 policyTypes: [Egress]
k3s のデフォルトの状態では、 NetworkPolicy リソースを作成しても、何も起こりません。 Network Policy に対応するプラグインの導入が必要です。 k3s で簡単に導入できそうなものとして、 Kube-Router がありました。導入方法については、 Qrunch に書いたものがありますので、参照してください。
MateProxy
構成図の中に「MateProxy」というものをしれっと書き込みましたが、これは自作のリバースプロキシサーバーです。
- ソースコード: GitHub azyobuzin/mateproxy
- Docker イメージ: Docker Hub azyobuzin/mateproxy
といっても、 ASP.NET Core でリバースプロキシサーバーを作成する ProxyKit と、 ASP.NET Core アプリの通信とログを調査するための GUI を提供する Rin を組み合わせた簡単なものです(とはいえ、 ProxyKit の WebSocket 対応では使いにくかったので、 WebSocket に関しては完全に書き直しました)。
このように、リバースプロキシを行う際に、リクエスト/レスポンスのヘッダー/ボディと、レスポンスを返すまでにかかった時間が記録され、眺めてにやにやすることができます。
このスクリーンショットでは、ブラウザからアクセスしたときの全リクエストを表示させていますが、ブラウザっぽいリクエストを弾くように、パスやヘッダーのフィルターを設定することで、サーバー同士の ActivityPub プロトコルによる通信だけを覗き見るよう設定することができます。というかリポジトリに入っている設定は、そうなっています。
ドキュメントを書くのをサボっているので、僕以外使えなさそうですが、使いたい方はご連絡ください。またはもっといいツール知ってるよという方もご連絡ください。あっ Istio は巨大すぎるからやめて。
完全ローカルな永続ストレージ戦略
Storage Class
Mastodon を動かすには PostgreSQL が必要ですし、プロフィール画像や添付画像といった画像類の保存先も必要になります。普通 Kubernetes を使うようなブルジョア環境ならば、データベースもオブジェクトストレージもマネージドサービスに突っ込んでしまうところですが、手元の環境だけでお金をかけずに構築するが今回のモットーなのでローカルで完結していきましょう。
ストレージの要求性能によって 2 つに分けることにします。
- データベース用の高速なストレージ
- 画像のような単一ファイルを保存する取り回しの楽で、そこそこ高速なストレージ
要求性能によって分けるというのは、まさに StorageClass ですね。用意した YAML ファイルでは、1を fediverse-database
、2を fediverse-appdata
と名付けてあります。
適当なノードのファイルシステムを利用する
まず、 1 について構築していきます。 1 については、安定して高速なストレージが欲しいということで、 Pod がスケジュールされたノード上の適当なディレクトリを使っていく作戦にしましょう。
一番簡単な方法は、 hostPath
ボリュームを使うことです。本当に単純だし簡単ですね。しかし、複数ノードで運用している場合、 Pod がスケジュールされるノードを固定しないと、データを永続化できたことになりません。ノードが違ったらそのディレクトリにあるデータが違いますからね。というわけで、 PodSpec に nodeName
か nodeSelector
を設定して、スケジュールされるノードを固定してあげる必要があります。
それでいいといったら、それでいいんですけれども、なんか風情が足りませんね。知らんけど。
というわけで、ちょっと面白いストレージの動的プロビジョナーを紹介します。 Rancher Labs が公開している Local Path Provisioner です。これを使うと、 Pod が最初にスケジュールされたノードにディレクトリを作成し、それを hostPath
のようにマウントしてくれます。そして、それ以降、そのストレージを使用する Pod は、ディレクトリを作成したノードにスケジュールされるので、スケジュールされるノードが違う!という問題は発生しません。
Local Path Provisioner がどのように動的プロビジョニングを行うかを説明します。まず、 Local Path Provisioner の Deployment を作成します。このコンテナが PersistentVolumeClaim の監視を行います。あとは、このプロビジョナーを使う StorageClass を定義し、その StorageClass を使う PersistentVolumeClaim を用意します。 Local Path Provisioner では accessModes
は ReadWriteOnce
しか指定できません。ノードを越えられないので当たり前ですが。
ここで、作成した PersistentVolumeClaim を使用する Pod が作成されたとします。 Local Path Provisoner は、自分が担当する StorageClass に対応する PersistentVolumeClaim への使用要求が来たことを検知して、 Pod がスケジュールされるノード上で hostPath
ボリュームとして振舞う PersistentVolume を作成し、それを PersistentVolumeClaim に登録(バインド)します。次の図では、オレンジの線がプロビジョナーが行う操作です。これで、 Pod は、 PersistentVolumeClaim を経由して、 PersistentVolume を手に入れ、永続ストレージが使えるようになりました。
Pod が削除されても、次の図のように、 PersistentVolume は残り続けます。もう一度 Pod を作成すると、バインド済みの PersistentVolumeClaim を経由して、前回と同じ PersistentVolume が使用されます。このとき、 PersistentVolume には nodeAffinity でノードのホスト名が指定されているので、 Pod がスケジュールされうるノードは、前回と同じノードになります。
PersistentVolumeClaim が削除されると、 Local Path Provisioner はこれを検知して、 Persistent Volume およびノード上に作成したディレクトリを削除します。
というわけで、雑に複数ノード構成にしても、それなりにうまくいくのが Local Path Provisioner です。良い。
どのノードからでもアクセスできるストレージを作る
Local Path Provisioner では、一度ノードが決まると、その PersistentVolumeClaim を使う Pod はすべてそのノードにスケジュールされてしまいます。しかも、 accessModes
は ReadWriteOnce
しか指定できないので、レプリケーションを行うことができません。明確に 1 コンテナしか作成しないステートフルなコンテナを相手にはそれでも良いですが、アプリケーション側はストレージの要求性能も低いので、もう少し柔軟な構成にしましょう。そこで、使用するのが、 nfs-provisoner です。 NFS を使用すれば、 NFS サーバーがどのノードにあろうと、コンテナにマウントすることができます。その代わり、ファイルシステムへの操作がすべてネットワークを経由するので、素のディレクトリよりは性能低下は覚悟してね、ということになります。
nfs-provisioner の面白いところは、プロビジョナーのコンテナ自体が NFS サーバーになっているところです。動的プロビジョニングの手順については Local Path Provisioner で説明したので、 nfs-provisioner が作成する PersistentVolume だけを紹介します。 PersistentVolumeClaim への使用通知を受けて、プロビジョナーはまず NFS サーバーで公開しているディレクトリ /exports
下に適当な名前のディレクトリを作成します。そして、 nfs
ボリュームとして PersistentVolume を作成します。そして接続先サーバーとして、プロビジョナー Pod に向いている Service の IP アドレスが指定され、パスとして作成したディレクトリが指定されます。
プロビジョナーコンテナの /exports
ディレクトリは、また別に永続化しなければなりません。リポジトリに入れた YAML ファイルでは、 fediverse/nfs: "true"
というラベルが指定されたノードでこの Pod が動くように指定しておき、 /exports
を hostPath
で設定しました。試していないのですが、最強の方法として、このボリュームを Local Path Provisioner に作らせることも可能です。
CoreDNS の設定
さて、最後にクラスタ内の DNS の設定を何とかするという要件が残っていましたね。ここで大問題があり、 k3s の CoreDNS の設定を永続化できません。やりたかったら k3s server --no-deploy coredns
で起動して、自分でデプロイしてねといった感じです。 k3s が起動中ならば /var/lib/rancher/k3s/server/manifests/coredns.yaml
を編集すると勝手にデプロイされる機能があるのですが、 k3s を再起動すると元のマニフェストに上書きされます。
まぁテスト環境なので、起動するたびにデプロイすればいいんですよ(最悪な結論)。 CoreDNS の設定はデフォルトでこのようになっています。
apiVersion: v1 kind: ConfigMap metadata: name: coredns namespace: kube-system data: Corefile: | .:53 { errors health kubernetes cluster.local in-addr.arpa ip6.arpa { pods insecure upstream fallthrough in-addr.arpa ip6.arpa } prometheus :9153 proxy . 1.1.1.1 cache 30 loop reload loadbalance }
k3s/coredns.yaml at v0.2.0 · rancher/k3s · GitHub
デフォルトの応答をする前に手を加えたいならば、 kubernetes
の行の前に差し込めば良いです。今回は、「*.fediverse.local.azyobuzi.net」をすべて Ingress Controller である Traefik に向けたいので、これにマッチするクエリに対して、 CNAME で traefik.kube-system.svc.cluster.local.
を返してあげることにします*1。このような操作には template プラグインが利用できます。 upstream
を付け加えて、 CNAME の解決結果も応答に含めるようにすれば完璧です。
template IN A { match ^(.*\.)?fediverse\.local\.azyobuzi\.net\.$ answer "{{ .Name }} 60 IN CNAME traefik.kube-system.svc.cluster.local." upstream fallthrough }
Traefik の設定
要件にはありませんが、 Traefik をいじるのは結構簡単だよというお話です。 k3s の独自拡張で、 Helm のインストールが YAML を突っ込むだけでできるようになっていて、 Traefik のインストールにはそれが用いられています。 spec
に set
というフィールドがありますが、これが helm install --set foo=bar
に指定する設定です。 Traefik チャートで使用できる設定は、 Helm Charts リポジトリで確認できます。 set
フィールドを書き換えて、デプロイすると再インストールが走り、設定が適用されます(0.2.0 での新機能です。 0.1.0 では、一度削除してから作成し直してください)。例えば、僕は Traefik のダッシュボード機能を有効化しています。
dashboard.enabled: "true" dashboard.domain: "traefik.k8s.local.azyobuzi.net"
さて、この HelmChart
リソースですが、 Traefik 以外にも使うことができます。ただし、 kube-system
名前空間限定です。他の名前空間に作成しても、何も起こらないので注意してください。
まとめ
k3s の登場で、ローカルに小規模だけど、それだけで完結する Kubernetes クラスタを作れるようになりました。 Kubernetes について、今までは富豪的な tips ばかりだったと思いますが、これから少しずつ小規模環境で使うための技も出てくると良いなと思っています。
では、みなさん、良い Kubernetes と分散 SNS ライフを。