Kubernetesのadmission webhookでMackerelの監視を便利にしたい

はてなには、開発合宿というイベントがあります。 有志で集ったメンバーが普段の開発から離れ、テーマを決めて集中して取り組むイベントです。 2022年6月にも、私(id:dekokunid:hayajo_77 id:masayosu id:yajimasanの4人からなるチームで開発合宿を行いました。

今回は、はてなが提供するサーバー管理・監視サービスMackerelによるコンテナ監視で必須になるmackerel-container-agentというプログラムを題材に、Kubernetesのadmission webhookを作ってみました。

※ あくまでadmission webhookの知見を得る材料としてmackerel-container-agentを選択したものです。 開発初期の段階でもあり、公式にMackerelがこの使用を推奨するものではありません。

mackerel-container-agentでadmission webhookを作る狙い

mackerel-container-agentは、KubernetesではPodのサイドカーとして動作し、情報を収集してMackerelに送信するプログラムです。 Podのサイドカーとして注入するには、Kubernetesのマニフェストを適切に記述する必要があります。

Kubernetesにmackerel-container-agentをセットアップする

Mackerelで監視する場合、たいていのPodにmackerel-container-agentをサイドカーとして注入したくなります。 そのため、同じようなマニフェストを何度も記述することになります。 これをもっと簡易できると嬉しいという気持ちがありました。

具体的には、そのPodを監視するために必要な設定だけをPodのマニフェストに記述して、Kubernetesクラスタごとに統一できる基本的な設定は別の場所に記述しておきたいのです。 mackerel-container-agentをサイドカーとして注入するadmission webhookを作成すれば、この困りごとは解決できそうです。

また、はてなではKubernetesを使ってさまざまなサービスを運用していますが、admission webhookをはじめとする柔軟で便利な運用の機能を十分に使いこなせていないという課題意識もありました。 自分で作成してみることで、新しいものが学べそうだという期待もありました。

さらに3年ほど前に、CTOのid:motemenid:hayajo_77とともに、admission webhookでmackerel-container-agentを注入するためのコンセプト実装を作成していました。 この資産に乗ることで、2日しかない開発合宿でも成果を出しやすいだろうという目論見もありました。

合宿の準備: Kubebuilderをもくもくと自習

作るものが決まったので、合宿までの2ヶ月ほどを使って、admission webhook周りの知識を深めていきました。 例えば、以下のサイトの写経を行いました。

つくって学ぶKubebuilder

Kubebuilderは、KubernetesのCRDやカスタムコントローラー、admission webhookを実装するための補助ツールです。

上記のサイトは、Kubebuilderを使ってAPIやそれにひも付くadmission webhookを作るチュートリアルとなっており、admission webhookの作り方を学ぶのに最適でした。 業務中に週1時間ほど集まっては、もくもくと進めていきました。

1日目午前: Pod作成時にwebhookが動く

1日目は、コンセプト実装をKubebuilderに載せ替えました。 午前には、任意のPodが作成された際に、ただログを出力する出すだけのwebhookが動くところまでできました。

上記のチュートリアルでは、自分で作成したCRDによって構築されたカスタムリソースの変更時に実行されるwebhookを作成します。 しかし今回は、すでにCoreリソースとして存在するPod v1 coreの構築時に実行されるwebhookを作成したいのですが、どう書けばいいのかがすぐには分からず、時間がかかってしまいました。

具体的には、controller-runtimeのadmission.CustomDefaulterを使うのですが、前日にid:hayajo_77が検証してくれていたこともあり、無事に午前中に動かすことができました。 この試行錯誤では、以下の記事が非常に参考になりました。

KubebuilderでCore ResourceのAdmission Webhookを作る

1日目午後: mackerel-container-agentをサイドカーとして注入する実装

午後には、Podが構築された際にサイドカーとしてmackerel-container-agentを実際に注入する実装を組み入れました。 すでに存在するコンセプト実装からロジックを持ってくることができたため、スムーズに進みました。

変更点としては、コンセプト実装ではPodの差分を適用するためのパッチのJSONを組み立ててwebhookのレスポンスとして返すようなロジックを、以下のコード例のように自分で書いていました。

   var patches []patchOperation

    if target == nil {
        target = map[string]string{}
    }

    for k, v := range annotations {
        var op, path string
        var value interface{}
        if _, ok := target[k]; ok {
            op = "replace"
            path = annotationsBasePath + "/" + k
            value = v
        } else {
            op = "add"
            path = annotationsBasePath
            value = map[string]string{k: v}
        }

        patches = append(patches, patchOperation{
            Op:    op,
            Path:  path,
            Value: value,
        })
    }
    // これ以降、上記patchをJSONに変換してresponceとして返すコード

しかし、Kubebuilderの導入によりGoの構造体を変更するだけでよくなったため、以下のように非常に見通しの良いコードとなりました(意味的には上記と同じことをしています)。

   if pod.Annotations == nil {
        pod.Annotations = map[string]string{}
    }
    pod.Annotations[admissionWebhookAnnotationStatusKey] = "injected"

2日目: 周辺ツールを整備する

2日目は、合宿の成果を社内で発表するための準備に四半日ほど使いたく、そこまで多くの時間があるわけではありません。

当初に動かしたかった最低限の実装は1日目でできたこともあり、2日目は主に周辺のツールを整えていました。 具体的に行ったのは以下の通りです。

Helmでインストールを可能にする

簡単にインストールできるように、Helmチャートを作成しました。

Kustomize管理されているものを、Kustomize管理された状態のまま、Helmにも対応する方法をどうするかが悩ましいところでした。 ひとまずは、kustomize buildしたものをHelmのテンプレートとしてそのまま利用する方法を採用しました。 具体的な変更点は以下の通りです。

Add helm chart by dekokun · Pull Request #2

今回は実装しなかったのですが、今後Helmチャートのために可変部分を変数化したい場合は、変数化したい部分ごとにkustomization.yaml内にパッチを書くことで対応可能だと思っています。 これは以下のブログに記載されています。

How to Kustomize Helm Charts for Flexible K8s Deployments

まだ可変部分を変数にする、デプロイ時のメッセージを出すなどのHelmチャート化のうまみを享受しきれていませんが、そのあたりは今後の課題と考えています。

CI整備

make testを行いつつ、kustomize buildを行ってエラーが出ないかどうかのテストを行い、ビルドが正常かをチェックするようにしました。

今後の課題として、テスト内容を充実させる、CIによってパブリックなリポジトリにイメージを公開する、などがあります。

GitHubリポジトリ整備

合宿で作成したツールは以下に公開しています。リポジトリを公開することもあり、最低限のREADMEの整備を行いました。

mackerelio-labs/mackerel-container-agent-sidecar-injector

また、今後の運用のために、slackチャンネルを作ってそこにリポジトリの活動が流れてくるようにしました。

MackerelのAPI KeyをPodごとに変更可能に

これまでの実装では、クラスタ内でMackerelのAPI Keyを1つしか指定できませんでした。 その場合に問題となるのが、クラスタ内でMackerelの複数のオーガニゼーションに対応することができないというものです。

この対処のために、PodのAnnotationにKubernetesのSecretリソース名を記述することで、SecretからAPI Keyを取得できるようにしました。 変更点は以下の通りです。

support MACKEREL_APIKEY via annotations by hayajo · Pull Request #6

webhookのfailurePolicyの興味深い現象

すべてのPodの構築時に実行されるwebhookを実装したこともあり、webhookに壊れた実装を載せると、すべてのPodが構築できなくなりました。 すべてのPodが構築できなくなるということは、すなわちwebhook自体も変更できなくなるということです。 そのため、壊れた実装を直すことができなくなりました。

当然ですが、これの対処策はKubernetesに存在します。とは言え、この現象に当たったときは「こんなことが起きるのか」と非常に興味深かったです。

詳しく説明すると、webhookのfailurePolicyをFailにしており、webhookが失敗した際にリソースが作られないようにしていたために発生しました。 詳細は以下のドキュメントにあります。

Dynamic Admission Control - Failure policy | Kubernetes

このfailurePolicyをFailからIgnoreにすることで、たとえwebhookの実装が壊れていてもPodは構築されるようになりました。 変更は以下の通りです。

change `failurePolicy` to `ignore` by hayajo · Pull Request #5

この修正を見ると分かりますが、Goで書かれたwebhookの定義にマーカーコメントとしてfailurePolicyを書くことで、webhookをデプロイするためのマニフェストも更新するような作りになっています。 これも、Kubebuilderを使うことで便利になったところでした。

合宿後に追加した修正とまとめ

合宿の終了後にも、Mackerelの監視ルールをConfigMapに記述してPodごとに監視内容を変更できるようにする以下のプルリクエストなど、変更が加えられています。

Support mackerel agent config by hayajo · Pull Request #11

今後も開発を続けていきたいと考えています。

参加者による感想

合宿に参加した4人それぞれの感想を最後にまとめておきます。

  • id:masayosu
    • みんなでわいわい手を動かして実装できてよかったです! 技術的に知らなかったことを腰を据えて学べる機会があり助かりました。はてなのkubernetes周りが盛り上がってきている感じがする。
  • id:hayajo_77
    • なんとなく理解していたことを、手を動かすことで知識として身につけることができました。合宿当日だけではなく準備期間も含め、とても充実した取り組みだったと感じています。
  • id:yajimasan
    • Goが好きなので、実装が楽しかったです! Kubernetes全体のコンポーネントについても、理解を深めることができてよかったです。
  • id:dekokun
    • Kubebuilderなど、kubernetes関連の開発環境がとても整っていることに感動しました。今後こういうものも作れるという発想をもって、さらにはてなのKubernetes運用をより良いものにしていきたいです!

このように参加者からも良い取り組みになったという感想が上がっていました。 今後も、はてなのKubernetes運用がもっと盛り上がることを通して、はてなのサービスがよりよくなるように、さまざまな取り組みを行っていきたいです。