アジョブジ星通信

進捗が出た頃に更新されるブログ。

Kubernetesで隔離Mastodonネットワークを作った

はじめに

前回、 k3s の紹介をしましたが、もともと Kubernetes に興味を持ったのは Docker のネットワークが、簡単だけれど細かい設定ができないという問題を抱えていたからです。細かく設定したネットワーク環境を使って何がやりたかったかというと、 Mastodon をはじめとする、 ActivityPub サーバーを隔離環境で動かし、他人のサーバーに迷惑をかけずに通信を眺めたかったのです。さらに、それを使って ActivityPub サーバーを自作するところまで行きたいのですが、正直もう体力と時間がないので、他に作っている方の動向だけ眺めていることにします……。

というわけで、要件はこんな感じです。

  • Mastodon サーバーを 2 つ立ち上げ、相互に通信できるようにする
    • 実環境に近くするため HTTPS で通信する
    • サーバー名として「mastodon1.fediverse.local.azyobuzi.net」、「mastodon2.fediverse.local.azyobuzi.net」を割り当て、解決できるようにする
  • 隔離ネットワーク外のサーバーにアクセスできないようにする
  • Mastodon サーバー間の通信内容を簡単に覗き見ることができるようにする

KubernetesMastodon を構築した!といった記事は星の数ほどあると思いますので、この記事のポイントを挙げると、ローカル限定の隔離ネットワークであるところと、サーバー間の通信を監視できる UI を用意したところです。

先に完成品をお見せしておきます。トゥートしたりふぁぼったりすると、相手サーバーにリクエストが飛んでいることが確認できます。

ここまでできて、とりあえずやりきった感が強く、飽きたので、あとはこれを読んだみなさんが、より強いシステムを作って、 ActivityPub サーバーの開発に役立てていただけると幸いです。

このブログ記事では、構成要素や、 k3s での構築について書き残しておきたいと思います。 k3s に限らない構築方法については、 GitHub に置いておきます。

構成

まずはこんな感じにしたいという構成を確認しておきます。

f:id:azyobuzin:20190312025303p:plain
コンポーネントと HTTP リクエストの方向

矢印は、 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」というものをしれっと書き込みましたが、これは自作のリバースプロキシサーバーです。

といっても、 ASP.NET Core でリバースプロキシサーバーを作成する ProxyKit と、 ASP.NET Core アプリの通信とログを調査するための GUI を提供する Rin を組み合わせた簡単なものです(とはいえ、 ProxyKit の WebSocket 対応では使いにくかったので、 WebSocket に関しては完全に書き直しました)。

このように、リバースプロキシを行う際に、リクエスト/レスポンスのヘッダー/ボディと、レスポンスを返すまでにかかった時間が記録され、眺めてにやにやすることができます。

f:id:azyobuzin:20190320231437p:plain
Mastodon の /about ページを開いたときのリクエストリストと、タイムライン取得 API のレスポンス

このスクリーンショットでは、ブラウザからアクセスしたときの全リクエストを表示させていますが、ブラウザっぽいリクエストを弾くように、パスやヘッダーのフィルターを設定することで、サーバー同士の ActivityPub プロトコルによる通信だけを覗き見るよう設定することができます。というかリポジトリに入っている設定は、そうなっています。

ドキュメントを書くのをサボっているので、僕以外使えなさそうですが、使いたい方はご連絡ください。またはもっといいツール知ってるよという方もご連絡ください。あっ Istio は巨大すぎるからやめて。

完全ローカルな永続ストレージ戦略

Storage Class

Mastodon を動かすには PostgreSQL が必要ですし、プロフィール画像や添付画像といった画像類の保存先も必要になります。普通 Kubernetes を使うようなブルジョア環境ならば、データベースもオブジェクトストレージもマネージドサービスに突っ込んでしまうところですが、手元の環境だけでお金をかけずに構築するが今回のモットーなのでローカルで完結していきましょう。

ストレージの要求性能によって 2 つに分けることにします。

  1. データベース用の高速なストレージ
  2. 画像のような単一ファイルを保存する取り回しの楽で、そこそこ高速なストレージ

要求性能によって分けるというのは、まさに StorageClass ですね。用意した YAML ファイルでは、1を fediverse-database、2を fediverse-appdata と名付けてあります。

適当なノードのファイルシステムを利用する

まず、 1 について構築していきます。 1 については、安定して高速なストレージが欲しいということで、 Pod がスケジュールされたノード上の適当なディレクトリを使っていく作戦にしましょう。

一番簡単な方法は、 hostPath ボリュームを使うことです。本当に単純だし簡単ですね。しかし、複数ノードで運用している場合、 Pod がスケジュールされるノードを固定しないと、データを永続化できたことになりません。ノードが違ったらそのディレクトリにあるデータが違いますからね。というわけで、 PodSpec に nodeNamenodeSelector を設定して、スケジュールされるノードを固定してあげる必要があります。

それでいいといったら、それでいいんですけれども、なんか風情が足りませんね。知らんけど。

というわけで、ちょっと面白いストレージの動的プロビジョナーを紹介します。 Rancher Labs が公開している Local Path Provisioner です。これを使うと、 Pod が最初にスケジュールされたノードにディレクトリを作成し、それを hostPath のようにマウントしてくれます。そして、それ以降、そのストレージを使用する Pod は、ディレクトリを作成したノードにスケジュールされるので、スケジュールされるノードが違う!という問題は発生しません。

Local Path Provisioner がどのように動的プロビジョニングを行うかを説明します。まず、 Local Path Provisioner の Deployment を作成します。このコンテナが PersistentVolumeClaim の監視を行います。あとは、このプロビジョナーを使う StorageClass を定義し、その StorageClass を使う PersistentVolumeClaim を用意します。 Local Path Provisioner では accessModesReadWriteOnce しか指定できません。ノードを越えられないので当たり前ですが。

f:id:azyobuzin:20190321012701p:plain
初期状態

ここで、作成した PersistentVolumeClaim を使用する Pod が作成されたとします。 Local Path Provisoner は、自分が担当する StorageClass に対応する PersistentVolumeClaim への使用要求が来たことを検知して、 Pod がスケジュールされるノード上で hostPath ボリュームとして振舞う PersistentVolume を作成し、それを PersistentVolumeClaim に登録(バインド)します。次の図では、オレンジの線がプロビジョナーが行う操作です。これで、 Pod は、 PersistentVolumeClaim を経由して、 PersistentVolume を手に入れ、永続ストレージが使えるようになりました。

f:id:azyobuzin:20190321013329p:plain
Pod を作成したときの動的プロビジョニング結果

Pod が削除されても、次の図のように、 PersistentVolume は残り続けます。もう一度 Pod を作成すると、バインド済みの PersistentVolumeClaim を経由して、前回と同じ PersistentVolume が使用されます。このとき、 PersistentVolume には nodeAffinity でノードのホスト名が指定されているので、 Pod がスケジュールされうるノードは、前回と同じノードになります。

f:id:azyobuzin:20190321013821p:plain
Pod を削除しても PersistentVolume のバインドは残る

PersistentVolumeClaim が削除されると、 Local Path Provisioner はこれを検知して、 Persistent Volume およびノード上に作成したディレクトリを削除します。

というわけで、雑に複数ノード構成にしても、それなりにうまくいくのが Local Path Provisioner です。良い。

どのノードからでもアクセスできるストレージを作る

Local Path Provisioner では、一度ノードが決まると、その PersistentVolumeClaim を使う Pod はすべてそのノードにスケジュールされてしまいます。しかも、 accessModesReadWriteOnce しか指定できないので、レプリケーションを行うことができません。明確に 1 コンテナしか作成しないステートフルなコンテナを相手にはそれでも良いですが、アプリケーション側はストレージの要求性能も低いので、もう少し柔軟な構成にしましょう。そこで、使用するのが、 nfs-provisoner です。 NFS を使用すれば、 NFS サーバーがどのノードにあろうと、コンテナにマウントすることができます。その代わり、ファイルシステムへの操作がすべてネットワークを経由するので、素のディレクトリよりは性能低下は覚悟してね、ということになります。

nfs-provisioner の面白いところは、プロビジョナーのコンテナ自体が NFS サーバーになっているところです。動的プロビジョニングの手順については Local Path Provisioner で説明したので、 nfs-provisioner が作成する PersistentVolume だけを紹介します。 PersistentVolumeClaim への使用通知を受けて、プロビジョナーはまず NFS サーバーで公開しているディレクト/exports 下に適当な名前のディレクトリを作成します。そして、 nfs ボリュームとして PersistentVolume を作成します。そして接続先サーバーとして、プロビジョナー Pod に向いている Service の IP アドレスが指定され、パスとして作成したディレクトリが指定されます。

f:id:azyobuzin:20190321015617p:plain
nfs-provisioner がプロビジョニングする PersistentVolume

プロビジョナーコンテナの /exports ディレクトリは、また別に永続化しなければなりません。リポジトリに入れた YAML ファイルでは、 fediverse/nfs: "true" というラベルが指定されたノードでこの Pod が動くように指定しておき、 /exportshostPath で設定しました。試していないのですが、最強の方法として、このボリュームを 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 のインストールにはそれが用いられていますspecset というフィールドがありますが、これが 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 ライフを。

*1:rewrite プラグインは仕様が最悪なので、今回の正規表現では応答を適切な形にしてあげることができず、 MastodonRuby 実装の DNS を使って解決している特殊条件で死んでしまったので CNAME にしました。特殊条件すぎてめっちゃ悩んだんだぞ!!