KEELチームの相原です。
最近開発している コマンド1発でKubernetes上にProduction Readyな環境を手に入れる コードジェネレータの話です。
Kubernetesの利用を広める上での課題
KEELチームが開発しているアプリケーション実行基盤は巨大なMulti Tenancy Kubernetesクラスタをベースとしていて、各アプリケーション開発者はKubernetes Manifestといくつかの設定を記述するだけでProduction Readyな環境ですぐにアプリケーションを稼働させることができます。
これは車輪の再発明の防止や開発速度・運用品質の向上などの大きなメリットをもたらし、LIFULL HOME'Sの大部分と新規開発のアプリケーションの多くがこの上で稼働するようになりました。
こうして我々のプロジェクトは一定の成果を得ることに成功しましたが、社内で利用を広めていくにあたっていくつかの問題点が出てきました。
Kubernetes Manifestの難しさ
一つ目はKubernetes Manifestの難しさです。
topologySpreadConstraints
やpodAntiAffinity
を利用したPodの適切な分散- PodのTerminating時にEndpoint ControllerによってPodのIPがロードバランシング対象から外れるのを待つための
preStop
フック(あるいはそれ相当のアプリケーションによるSIGTERMハンドリング) - 安全なPod Evictionのための
PodDisruptionBudget
の設定 - Recommended Labelsの付与
- 適切なSecurityContextの設定
Kubernetes Manifestを書くにあたっては、このような広く知られているベストプラクティスがいくつかあり、アプリケーション開発者にこうした設定を要求するには無理があります。
LIFULLではIstioを利用しているため更に設定量は増えますし、Service Topologyのようなバージョンアップによって増えた新しい機能も取り入れて欲しいといった思いもあります。
既存の解決策
一般にこういった課題を解決するためにはHelmや、現在ではKustomizeなどが用いられてきました。
これらのソフトウェアを利用することでベストプラクティスをテンプレート的に用意して、利用者は必要最低限のパッチを当てるだけでよいというようなものです。
ただしHelmはChartの作成者によって自由度が大きく変わるため、特定のミドルウェアであればともかく大勢が満足するようなウェブアプリケーションのテンプレートをHelm Chartとして提供することは難しいです。
Kustomizeは割とこの問題の解決だけを考えれば理想的な選択肢で、Remote Buildを用いればベストプラクティス部分をうまく共通化できるでしょう。
モノレポにしていればRemote Buildも必要ありません。
実際に我々もKustomizeは出た当初くらいから長く使ってきています。
LIFULLではモノレポではなく権限分離の考えからアプリケーションごとにリポジトリを分けているため、当時はまだなかったRemote Buildが必要となりKustomizeのWrapperを自前で書くなどしてこの問題に対処してきました。
しかし、次第にKubernetes Manifest以外の生成にも目を向ける必要が出てきて、より包括的なアプローチが求められるようになりました。
設定量の増大
我々はアプリケーション開発者の関心事を減らすため、KubernetesやIstioをはじめとしてその他開発者支援のための多くのKubernetes Operatorを提供しています。
他にもGrafanaやPrometheus(+ Thanos + Trickster), Fluentd, Spinnakerを提供することによって監視基盤やデプロイパイプラインなども運用して開発者に提供しています。 それぞれにOperatorがあったりなかったりなかったら作ったりしていますが、それでも全てをKubernetes Manifestで設定することは叶いません。
これではManifest TemplatingソフトウェアでKubernetes Manifestを生成してからも数ステップの設定作業が必要となってしまいます。
更にビルドパイプラインや各種Lintをはじめとした多数のGitHub Actions、依存AWSリソースを作成するためのCloudFormationテンプレートも提供しているため、いずれにせよ設定量の増加が課題となってきていました。
(Kustomizeと同様にCI周りもモノレポだと楽になりそうですが、モノレポをちゃんとやろうとすると先述の権限分離に加えてVCS周りを頑張らなきゃいけなくなったりするのでリポジトリ戦略を変えるという判断にはなりませんでした)
コードジェネレータで解決する
こうした課題を解決するため我々はコードジェネレータ keelctl
を開発しました。
こちらで用意したKubernetesのCustom Resource Definitionをインタフェースとして、開発者がそこに必要な項目を入力すると先に述べたようなアプリケーションの開発サイクルに必要なものが すべて 自動で生成されます。
生成されるものは以下のように多岐に及んでいます。(かろうじて秩序は保たれています)
- Kubernetes Manifest
- ConftestやDocker Imageへの動的解析など各種Lintを実行するGitHub Actions
- git-flowを実現するためのGitHub Actions
- ビルドパイプライン他各種AWSリソースを作成するCloudFormation Template
- ビルドされたDocker ImageやKubernetes ManifestをデプロイするためのSpinnakerのパイプライン
- PrometheusおよびAlertmanagerでのProduction Readyなアラート設定と通知先設定
- GrafanaのProduction ReadyなDashboard
- 上記に関する説明やダッシュボードへのリンクなどのドキュメント
まさに コマンド1発でKubernetes上にProduction Readyな環境を手に入れる コードジェネレータです。
ビルドパイプラインもついているので正真正銘コマンド1発叩いた瞬間からGitHubにpushするといい感じにデプロイされるようになります。 (小さいアプリケーションの参考実装が入ったGitHubのTemplate Repositoryも一緒に提供していて、そこにはDockerfileも入れているため開発初期からこれらを手に入れることができます)
ドキュメントの自動生成は結構気に入っています。 そのアプリケーションのリポジトリ内にドキュメントを生成することでよくありがちなオレオレドキュメント乱立の防止をしたり、諸々の設定とライフサイクルを共にしていることでドキュメントが古い状態で放置されるあるいは中央管理されているドキュメントから古い情報が消されて置いて行かれるといったことが防がれています。
辛いことや面倒なことをアプリケーション開発者のために肩代わりするようなソフトウェアであるためやってることは結構泥臭くて、Custom Resource Definitionに入力された設定値をもとに筋肉であらゆるファイルを動的に生成するといったようなことをしています。
Custom Resource Definitionを使っていますが、以下のような点を考慮しながらCustom Controllerとしてではなくコードジェネレータとして作りました。
捨てやすさ
先に述べた問題をCustom Resource Definitionで抽象化して解決するということはKubernetesの上にPaaSを構築することと等しいです。
PaaSの採用を決めるにあたって最も懸念となることが、捨てやすさです。
PaaSはその性質上、利用者から見たインタフェースの抽象度が高く内部の処理もブラックボックス化されているため、別のPaaSへの移行やPaaSをやめる際に多くのコストがかかってしまいます。
Custom Controllerはいい仕組みですが、今回のような乱暴なユースケースではいざ我に返ってこのアプローチをやめようとした時に捨てづらく、やめる際にもコストを支払うことになってしまいます。
一方、コードジェネレータであれば成果物として生成されたファイルをそのまま使い続けられるように設計することが可能です。
そこでCustom Controllerを作るときの要領でCustom Resource Definition周辺のエコシステムを活用しながらも、Kustomizeでそのまま使えるKubernetes Manifestを生成したりするなどして捨てやすさを念頭に置いて開発されています。
Custom Resource Definition周辺のエコシステムは非常に強力で、Validation, Versioningやドキュメントの自動生成、エディタの補完などの恩恵を受けることができるためCuston Controllerでなくともインタフェースとして利用する価値がありました。
抽象度
PaaSをデザインするにあたっては、抽象度をどのようにコントロールするかも考えなければなりません。
先に捨てやすさを意識しているのでロックインはされませんが、抽象度はユーザ体験と拡張性に影響します。
実際に我々はここで一度失敗をしています。
keelctl
以前に開発していたKustomizeのWrapperのように振る舞うManifest Templatingソフトウェアでは、極力アプリケーション開発者がKubernetesのことを知らなくてもいいようにKubernetesの概念を完全に隠蔽する強い抽象化をしていました。
しかし、次第にアプリケーション開発者の要求が複雑化してManifest Templatingソフトウェアが追い付かなくなったり、Kubernetesに詳しいアプリケーション開発者が細かくコントロールしたいと結局実装を読むみたいなことが起きるようになってしまいます。
その失敗を経て、keelctl
で利用するCustom Resource DefinitionではKubernetes Manifestの部分で抽象化を行わないようにしました。
こんな感じです。
apiVersion: keel.lifull.com/v1alpha1 kind: KEELConfiguration metadata: name: &name keelctl-sample spec: ... applications: - base: metadata: name: *name spec: feature: redis: enabled: true replicas: 5 deployment: spec: strategy: type: RollingUpdate rollingUpdate: maxSurge: 25% maxUnavailable: 1 template: metadata: annotations: sidecar.istio.io/inject: "true" spec: containers: - name: &main-container server env: - name: TZ value: Asia/Tokyo ... overlays: - name: *test override: metadata: namespace: *test-namespace spec: deployment: spec: template: metadata: annotations: sidecar.istio.io/proxyCPULimit: 100m sidecar.istio.io/proxyMemoryLimit: 256Mi sidecar.istio.io/proxyCPU: 100m sidecar.istio.io/proxyMemory: 256Mi spec: containers: - name: *main-container image: *test-repository
このようにDeploymentといったKubernetes Objectをそのまま露出させています。
これには、エディタの補完でupstreamのものをそのまま利用できたり、Kubernetes Manifestを生成する時の実装が楽、Kubernetesに詳しい人がそのまま弄れる、拡張性が高いみたいなメリットがあります。
実際にDeploymentと同列にIstioのVirtual ServiceやDestination Ruleなども設定できるようになっており、実装コストとyamlの秩序が許す範囲であればいくらでも拡張することができます。
(それぞれの apiVersion
はCustom Resource Definitionで管理していて、それぞれに非互換の変更が発生した際はCustom Resource Definitionの apiVersion
を変えて対応しています)
その上でアプリケーション開発者の関心事を減らすべく、topologySpreadConstraints
や Pod Disruption Budget
, Recommended Labels のような露出すべきでない情報を生成時の実装に閉じ込めていて、裏でいい感じにそれらをデフォルト値として生成してくれるようにしてKubernetes特有のベストプラクティスを知る必要がない状態にしました。
こうしてベストプラクティスを知る必要がない状態になってしまえばKubernetesに詳しくない人のための抽象化は不要で、必要な項目があらかじめ埋まっているテンプレートを用意したうえで各項目に丁寧なコメントを付与するだけで十分やっていけています。
難しいのはKubernetes ManifestではなくKubernetes特有のベストプラクティスなのでそれさえ裏に隠してしまえばこの程度で十分でした。 Kubernetes Manifestの公式ドキュメントはよくできているため、無用な抽象化はドキュメントの焼き直しにしかなりません。
Kubernetes Objectで表現できないような機能については雑に feature
というキーを用意していて、ここでキャッシュ用のRedis/memcachedクラスタやLighthouseを実行してPrometheus用のMetricsを出すPrometheus Exporterとかを利用できるようになっています。
またKustomizeに倣って base
と overlays
というキーを用意していて、ここで共通部分の定義と環境ごとの出し分けを実装しました。
ここでもStrategic Merge Patchというupstreamの機能をそのまま利用することで実装をサボっています。
抽象度をうまくコントロールすることで、upstreamの成果を利用しながら利用者の知識レベルに依存しない使いやすいインタフェースを実現することに成功しました。
変更への追従しやすさ
同じことをCustom Controllerで実現すると更新の配布のしやすさはCustom Controller側に軍配が上がります。
Custom Controller側を弄るだけで利用者に等しく更新を配布することができるためです。
逆に更新の配布を考えなければGitHubのTemplate Repositoryで配るだけでも十分かもしれません。
そこで、コードジェネレータでも似たような更新の配布しやすさを実現すべくセルフアップデートの機能を実装しました。
セルフアップデートの機能があれば、コマンドを叩くだけでバイナリを最新の状態に保つことができます。
また、生成される成果物のテンプレートはgolang 1.16から入ったembedを利用して全てバイナリに含めています。 (バイナリはそれなりに大きくなりますが2021年に問題があるレベルではありません)
こうすることでテンプレートもバージョン管理されるため、利用者は以下のようなコマンドを定期的に実行することで手元の設定ファイルを最新に保つことが可能となりました。
$ keelctl self-update $ keelctl gen keel_configuration.yaml .
こうして更新の配布を簡単にすることで、各設定を最新に保つだけでなくセキュリティ面での統制を効かせることにも成功しています。
更に .keelctl_version
のようなファイルも生成することで、それぞれのリポジトリが利用している keelctl
のバージョンを外からトラッキングできるようにもして統制を強化しました。
全社基盤をやっていて陥りやすいものとして、作った機能が使われない、責任分界点が曖昧になり更新作業などが行われない、といったものがあると思います。
keelctl
ではCustom Resource Definitionを責任分界点としつつ、更新を半ば強制的に配布する仕組みを整えたことでそうした問題への対処に成功しました。
また、.keelctlignore
というファイルに .gitignore
と同じ書式でパターンを記述すると任意のファイルをオプトアウトする機能を実装しています。
これを利用することで例えば今はKubernetes Manifestの生成はせずに、ConftestやDocker Imageへの動的解析など各種Lintを行うGitHub Actionsだけを生成したいといった要求を叶えることができます。
これにより既存のリポジトリに段階的に導入することが可能となり、利用先は今ではLIFULLの大部分にまで広がりました。
自分の書いたコードをすぐに全社に配布できることからチーム外からのContributionも多く来るようになり、keelctl
は日々目まぐるしく進化しています。
Open Application ModelとKubeVela
実は似たような試みとしてMicrosoftが主導するOpen Application Modelというプロジェクトとその実装であるKubeVelaがあります。
Open Application Modelはクラウドネイティブなアプリケーションを定義するための仕様で、KubeVelaはそれをKubernetes上で実行するための実装です。
apiVersion: core.oam.dev/v1beta1 kind: Application metadata: name: first-vela-app spec: components: - name: express-server type: webservice properties: image: crccheck/hello-world port: 8000 traits: - type: ingress properties: domain: testsvc.example.com http: "/": 8000
このようなKubernetes Manifestを適用するとKubeVelaがCustom Controllerとして いい感じ にKubernetesに展開してくれます。
KubeVelaはTraitsという概念で機能をコントロールすることができ、デフォルトでKEDAやFlaggerなどが組み込まれています。
例えば上記の type: ingress
のようなものがそうで、このような設定を記述するとFlaggerによるCanary Deploymentなどを手に入れることができるといった感じです。
つまり、KubeVelaはOpen Application ModelをインタフェースとしてKubernetes上にPaaSを構築できる、コマンド1発でKubernetes上にProduction Readyな環境を手に入れる ソフトウェアです。
TraitsはこのようにPlatform Builderによって拡張可能で、keelctl
の feature
のようにキャッシュ用のRedis/memcachedクラスタを展開するためのTraitsを提供するみたいなことが可能となります。
type: webservice
に相当するWorkload Typeも同様に拡張可能であるため、抽象度が高いことによって痒い所に手が届かないみたいな問題は少なくともWorkload Typeを拡張すれば解決できるでしょう。
keelctl
の構想当初にOpen Application Modelは無かったのでLIFULLではこのようなアプローチになりましたが、Kubernetes上でPaaSっぽいことをやりたいのであればKubeVelaは有力な選択肢であると思います。
(今や keelctl
はKubernetes Manifest以外にも手を出していたり、LIFULLが構築してきたKubernetesエコシステムを手放すという判断には中々なれないので直近での移行は検討していませんが...)
強いて言えば、KubeVelaがCustom Controllerであることの捨てづらさは理解して慎重に判断した方が良いということくらいでしょうか。
アプリケーション開発者の辛いことや面倒なことを肩代わりするために我々のようなPlatform Builderの努力を要するという点は keelctl
でもKubeVelaでも同じでそれはPaaS提供者の宿命です。
keelctl
を開発してきてみて
ということで コマンド1発でKubernetes上にProduction Readyな環境を手に入れる コードジェネレータを開発した話でした。
やっていることは泥臭いですがもたらした恩恵は非常に大きかったです。
今まで楽観的に見積もっても2週間くらいかかっていたアプリケーションのローンチまでに必要な実行環境周りの作業、デプロイフローの構築やサーバの設定に監視周りの整備といったことが コマンド1発 で終わるようになりました。
他にも多くのアプリケーション開発者を支援する機能がチーム外からのContributionも受けながら提供されています。
そして、日々の改善はセルフアップデート機能によって多くいる keelctl
利用者に配布されて各種設定は最新に保たれ、こちらから利用状況がトラッキングできることで統制面でも役立っています。
コードジェネレータによってあらゆるものを自動生成するというアプローチは一見乱暴ですが、投資した時間に対して大きな成果が出ているし、捨てやすく作っているからと楽観視しています。
Kubernetesはその拡張性からこういったPaaSのような取り組みをしやすく、アプリケーション開発者の生産性向上やアプリケーション品質の向上に大きな影響力を発揮することができました。
そこからもう少し踏み込んでこういったコードジェネレータを開発することで更に影響範囲を広げることができ、我々KEELチームは単なるKubernetesチームではなくなり、より広く生産性や品質に責任を持つチームへと変化しつつあります。
広く生産性や品質に影響力を発揮したいKubernetesな各位はコードジェネレータの開発を検討してみてはいかがでしょうか。