JavaでKubernetesのsidecar-injectorを作る

## これは何?

JavaでKubernetesのMutatingWebhookを利用した、簡易なsidecar-injectorを作成したのでその時に調べたことや作成したコードの解説を書く。

作成したコードはこちら: [github.com/10hin/sidecar-injector][sidecar-injector-repo]

[sidecar-injector-repo]: https://github.com/10hin/sidecar-injector

## どうしてJava?

Kubernetes/コンテナ界隈はどうしてもGo言語の利用率が高いが、今回は筆者の個人的好みでJavaを利用した。少なくともMutatingWebhookを書く分には、JSONをHTTP BodyとするHTTPのやり取りができれば十分だということはした調べで分かったので、最悪の場合はライブラリを使って生のJSONを編集すればよいだけである。

## pom.xml

プロジェクトは特にこだわりなく start.spring.io で作成した。「うまくいかなければあきらめるだけだ」と思い、native-image対応は入れておいた。pluginを追加するだけ(のはず)だがここで入れておくと楽だと思う。依存ライブラリはSpring Web Reactiveを入れた。

とりあえずkubernetesにはアクセスしない(Webhookなので)のだが、[io.kubernetes:client-java][maven-central-client-java] は入れておいた。

[maven-central-client-java]: https://mvnrepository.com/artifact/io.kubernetes/client-java

また、いろいろ調べてから [io.kubernetes:client-java-admission-review][maven-central-client-java-admission-review]が存在することに気づいた。とてもありがたい。

[maven-central-client-java-admission-review]: https://mvnrepository.com/artifact/io.kubernetes/client-java-admission-review

もし、自身が使うAPI ObjectのJava SDKがない場合、CRDからJavaソースを生成するための[ガイド][guide-to-generate-java-codes-from-crds]もclient-javaリポジトリにあるので参考にしたい。

[guide-to-generate-java-codes-from-crds]: https://github.com/kubernetes-client/java/blob/352de0ac3183519242286e3c3424c8aad4086cf4/docs/generate-model-from-third-party-resources.md

> io.kubernetes:client-java-api が依存として含まれているが、client-javaから推移的に依存している気がするのでこれは不要な気がする。が、まだ検証していない。

## InjectorController.java

まともなコードはここだけである。

基本的にはごく普通のSpring Webfluxのコードになる。

controllerメソッドの引数/戻り値に一苦労した。結論からいうと、CRDから必要なオブジェクトを生成して引数・戻り値に指定するだけであり、今回は admission.k8s.io/v1/AdmissionReview が使えればよいので、pom.xmlのところでも言及した io.kubernetes:client-java-admission-review に含まれる io.kubernetes.client.admissionreview.models.AdmissionReview を引数と戻り値に使えばよい。

ただそれにたどり着くまでは紆余曲折があった。(ここからこの節の最後まではただの苦労話なので読み飛ばしてよい)

当初はclient-java-proto に含まれる io.kubernetes.client.proto.V1Admission.AdmissionReview を引数や戻り値に使っており、SpringがHTTPリクエストを解析してコントローラーメソッドに渡す前のところで失敗した。

そのあと、HTTP Bodyは生のbyte列([DataBuffer][spring-webflux-databuffer])を受け取るようにし、それを com.google.protobuf.util.JsonFormat[^1]を使ってparse/printしてみるものの、これらはあくまでProtocolBufferの内容をJSONに書き出すだけなので、Kubernetesオブジェクトがやるようなmetadataをつけてくれなかった。具体的には `apiVersion`/`kind` フィールドがつかないため、WebHookとしてapiserverから呼び出すとエラーになってしまった。

[spring-webflux-databuffer]: https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-codecs-buffers

このmetadataをどのようにつけるべきか試行錯誤して、途中ではGSONをつかってJsonFormatが返したバイト列を編集したりしていたのだが、「AdmissionReviewのprotoファイルから(Podなどほかのk8sオブジェクトと同じように)OpenAPI Specを書き出してそこからJavaオブジェクトを生成すればよいのでは」と気づき、調べ始めたところ前述の[ガイド][guide-to-generate-java-codes-from-crds]や、そのサンプルとして公開されている client-java-admission-review を発見することができた。

[protobuf-java]: https://mvnrepository.com/artifact/com.google.protobuf/protobuf-java
[javadoc-jsonformat]: https://javadoc.io/doc/com.google.protobuf/protobuf-java-util/latest/com/google/protobuf/util/JsonFormat.Printer.html

## ビルド

SpringBootはGraalVMを使用したnative-imageによるネイティブバイナリへのコンパイルをサポートしている。

コンテナとして起動するにはコンテナイメージの最小化や起動時間・起動時の所要メモリなどでメリットのあるネイティブコンパイルができるとうれしいので試してみた。

紆余曲折している最中は余計な依存を入れてみたりしたものの、あとから見ればそれらは特に必要なく、SpringBootのリファレンス通り、以下のコマンドでビルドすれば十分だった。

```terminal
$ mvn -Pnative spring-boot:build-image
```

## デプロイ

Admission WebhookはTLS認証(独自認証局でもOK)が必須なので[CertManager][cert-manager-site]をつかって[独自CAとそれを利用するIsseurを作成][provision-ca-and-issuer]してTLS証明書を作成した。

[cert-manager-site]: https://cert-manager.io
[provision-ca-and-issuer]: https://cert-manager.io/docs/configuration/selfsigned/#bootstrapping-ca-issuers

MutatingWebhookConfigurationリソースでMutatingWebhookをapiserverに登録するが、その際にCA証明書を caBundle フィールドに設定する。PEM形式の証明書を直接設定するのではなく、Base64エンコードして設定する。

> このあたりの話はリポジトリのREADME.mdでも作って書いておくべきなのだができていない

## 感想

やりかたさえ知っていればもっと簡単にできたし、こんな記事を書こうと思わなかったかもしれない。

かなりいろいろなことがJavaでもできることは分かった。どうしてJavaでcontrollerなりを開発するコミュニティや企業が出てこないのだろう。

[^1]: [com.google.protobuf:protobuf-java][protobuf-java]に[含まれ][javadoc-jsonformat]る。