理想的な Kubernetes カスタムコントローラーの開発環境を考えた
はじめまして。メルカリの Microservice Platform CI/CD とメルペイの Architect で Software Engineer Internship をしている riita10069 です。
この記事は、Merpay Tech Openness Month 2021 の3日目の記事です。
本記事では、私が Kubernetes1のコントローラー2を開発する上で使用している開発環境について紹介したいと思います。
コントローラーの実行環境についてまとまっていなかった方や、これからコントローラーを実装するけれど環境構築で悩んでいる方などの参考になれば嬉しいです。
Abstract
Kubernetes のコントローラーを開発する場合に、動作確認をしたり、自動テストを作成するのには、実際にコントローラーを Kubernetes の環境上で動作させる必要があります。
具体的には、 Resource へのアクセスや Reconcilation Loop3 を実現するために必要です。
それによって、開発環境の構築が、一般的なWebアプリケーションよりも複雑化します。
まず、Kubernetes の環境を用意しなければならならないことです。
一言に Kubernetes の環境といっても、etcd, kube-apiserver, kube-scheduler, kube-controller-manager, kube-dnsなど、多くのコンポーネント4からなっており環境構築は簡単ではありません。
次に、コントローラーを動作させるには、CRD5と独自のコントローラー、さらには動作に必要となる他の Resource を配置した上で、適切なイベント6を起こす必要があり、大きな手間がかかります。
さらに、クラスタ上で動作するコントローラーに対して、インタラクティブな動作確認をすることは簡単ではありません。
ローカル環境で実行しているGoのデバッガに対しては、インタラクティブに使用することができますが、クラスタ上で動作するコンテナのデバッガを使用するのは困難です。
これらの課題に対して、本記事では、そもそも理想的な開発環境というものを具体的にした上で、実際に動作確認及び、自動テストの環境を作成する手段を提案します。
Keyword: Kubernetes, Controller, Operator, Test, Kind, Skaffold, Delve, kubectl
はじめに
現在のコンテナオーケストレーションツールとして、Kubernetesは
デファクト・スタンダードの地位を築いています。
そして、Kubernetes開発者は、ソフトウェアにおけるオペレーションの自動化や、効率化のためにコントローラーやオペレーターを開発します。
実際に、Kubernetesの標準の機能も全てがコントローラーの形式で記述されているだけでなく、Istio, Argo, Knativeなど主要なクラウドネイティブなエコシステムも、独自のコントローラーと独自のResourceを定義する機能によって実現しています。
また、メルカリにおいても、数多くのコントローラーが独自に開発され活用されています。
コントローラー開発のためのライブラリやフレームワークに目を向けると、client-go7, code-generator8, kubebuilder9, operator-sdk10, metacontroller11など、続々と開発されていますが、開発環境に着目した取り組みのスタンダードはまだ確立されてないように感じています。
5つの理想的な開発環境の条件
そもそも、「よい開発体験」とはどのようなものかについて考えます。
色々な観点から考察した結果、私の主観では、以下の5つの条件が思いつきました。
- Fast Reaction
- Debuggable
- Immutable Infrastructure
- Isolated from other component
- Automatically executable testing
この5つの項目についてこれから具体的に説明します。
Fast Reaction
自分の今開発したプログラムコードに対するリアクションが瞬時に返ってくるということです。
例えば、実クラスタをステージング環境として構築する場合、ちょっとしたプログラムコードの変更の挙動を確認するために、Dockerのイメージをビルドしてから、デプロイが完了するのを待たなければなりません。
それだと待ち時間が長くなってしまいます。
つまり、Fast Reactionは、自分のエディタでプログラムコードを変更したあと、すぐに挙動が確認できる状態を指します。
Debuggable
いわずもがな、デバッグという行為は非常に重要です。
思ったようにプログラムがうごかない原因を特定するのには、挙動について調査する必要があります。
その際に、
- エラーメッセージなどの標準出力がすぐに確認できること
- デバッガが設定できること
の2つが満たされていないと、原因を特定するのには難しくなると思っています。
Immutable Infrastructure
直訳すると「不変なサーバー基盤」という意味です。
具体的には、一度サーバーを構築したらその後はサーバーのソフトウェアに変更を加えないことを意味します。
手続き的な環境構築が必要になると、多くの手順がまとまったドキュメントの通りに実行する必要があります。これは、手順の読み飛ばしなどで、うまくいかないことなども多く、容易に構築できることは非常に理想的な体験だと考えています。
さらに、手続き的な構築では、本番と開発者の環境に差異が生まれてしまうため、環境差分によるバグが混入する可能性が高くなります。
本番と同じ環境を Immutable に開発環境においても構築することが望ましいと考えています。
Isolated from other component
アプリケーションバックエンドであれば、他のマイクロサービスやステートフルなDBなどが、外部コンポーネントです。
その場合、外部コンポーネントの返すレスポンスが開発するサービスに影響を与えます。
例えば、2つのシチュエーションにおいて、プログラムの挙動が想定と変わってしまう可能性があります。
- 他の開発者と同時に開発する場合、他人の変更が自分に影響すること。
- アプリケーションを動作させた結果、ステートフルなサービスの状態が変わること。
そのため、これらに対する工夫としては、
- 前者は、他の開発者の影響を受けない環境を用いること
- 後者は、動作時に望ましい状態を宣言的に定義することで、手続き的にDBの状態を管理することをやめる
というのが望ましいのではないかと考えました。
Automatically executable testing
非正常系を含めた多くのケースを長期的に保守するためには、最終的にテストの実行自体が自動化されていることが理想的だと考えます。
なぜなら、CIを用いてテストが実行されていることで、特定の動作を保証することができるため、品質が保証されるからです。
Kubernetesコントローラーで実現する方法
前項にて、5つの理想的な開発環境について議論をしてきましたが、このようなKubernetesコントローラーの開発環境をどのように実現するかについて提案します。
Fast Reaction
変更に対して瞬時にリアクションを得られるようにするためには、ホットリロードができることが重要です。
ホットリロードの仕組みを取り入れるために、私はSkaffold12を採用しました。
Skaffoldは、コンテナイメージのビルド・アップロード・デプロイといったステップをサポートしてくれるCLIツールです。
Go言語向けのホットリロードを実現する方法の中で有名なものに、cosmtrek/air13 がありますが、私はビルドやデプロイ、ポートフォワードなど多くの機能をサポートしているSkaffoldがコントローラの開発においては、より便利だと思いました。
Skaffold
Skaffoldを用いたパイプラインを構築するのに必要なものは、3つです。
- manifests
- Dockerfile
- skaffold.yaml
manifestはデプロイしたいコントローラーのDeploymentなどのリソースを作成するためのmanifestです。
namespace, serviceやPDBなど周辺のリソースについても作成する必要があります。
Dockerfileについては、コントローラーが動作するようなGoのコンテナを作成するためのものです。
そして、skaffold.yamlは、Skaffoldの設定ファイルです。
---
apiVersion: skaffold/v2beta21
kind: Config
build:
artifacts:
- image: my-controller
docker:
dockerfile: ./Dockerfile
tagPolicy:
gitCommit: {}
local:
concurrency: 1
deploy:
kubectl:
manifests:
- ./k8s/deployment.yaml
logs:
prefix: container
Skaffold は、上記のように Dockerfile と manifests を指定することで、ビルドからデプロイまでを自動化するパイプラインを構築してくれます。
また、Skaffold 経由でデプロイすることで、ポートフォワーディングをすることができます。
これによって、クラスタの外部からアクセスするための Ingress などを構築する必要がなくなります。
私が開発しているコントローラは、 WebHook を受け取る必要があるため、Skaffold のポートフォワーディングを利用しました。
skaffold.yaml に以下のような記述を追加するだけで利用可能です。
portForward:
- resourceType: deployment
resourceName: my-controller
namespace: mynamespace
port: 8080
localPort: 9000 # *Optional*
https://skaffold.dev/docs/pipeline-stages/port-forwarding/
パーツが揃えば、コマンドによって、パイプラインを実行することが可能です。
skaffold run --tail --filename=./_dev/skaffold/skaffold.yaml
「–filename」引数には、先程紹介した、skaffold.yamlを指定します。
.kubeconfigのパスを設定したい場合は、環境変数で以下のように渡すことで指定できます。
KUBECONFIG=./kubeconfig skaffold run --tail --filename=./_dev/skaffold/skaffold.yaml
指定しなければ、$HOME/.kube/config
が用いられます。
Debuggable
前章で述べた2つの条件を満たすような環境を構築したいと考えました。
1つ目は、Kubernetes 上で動作する、Kubernetes コントローラから、エラーメッセージを受け取ることです。
これは、Skaffold がサポートしていて、skaffold dev
を用いることで解決します。
skaffold dev --tail --filename=./_dev/skaffold/skaffold.yaml
このコマンドでは、開発者モードとしてパイプラインを実行し、標準出力を受け取ることができます。
2つ目のデバッガを使用できること。
これに関しても、Skaffold がサポートしていて、skaffold debug
を用いることで解決します。
このコマンドはデバッグのための機能であり、Kubernetesクラスターにデプロイしたアプリケーションに対してデバッグを行うことを可能にします。
skaffold debug
によってデバッガを使用するために、Delve14とDuct Tape15というツールを用います。
これらについて具体的に説明します。
Delve
Delveは、Go言語向けのデバッガとして公開されているOSSです。
ステップ実行やレキシカル変数など多くのデバッガ向けの機能をサポートしています。
そして、headlessモード16を用いて起動することでリモートデバッグをすることが可能になります。これによって、コンテナ内で動作するコントローラに対してデバッグを行います。
実際に、Delveをヘッドレス実行するためには、以下のようなコマンドで実行できます。
dlv exec ./main.go --headless --listen 127.0.0.1:5000
このとき、127.0.0.1:5000
で、サーバーが起動しデバッグに必要な入力を待ち受けるため、リモートデバッグが可能になります。
Duct Tape
Duct Tapeは、skaffold debug
コマンドを使用して、特定の言語ランタイムをデバッグするのに必要な仕組みです。
今回は、KubernetesクラスターにデプロイされたPod上でDelveを起動するために使います。
Delveにリモートアクセスするためには、アプリケーションの入ったコンテナ内部に、DelveのServerを立ち上げる必要があります。
そのようなパイプラインを実装することはできますが、管理が煩雑になります。
Skaffoldは、DelveのServerを立ち上げるパイプラインも提供しています。
そのためのコマンドがskaffold debug
です。
PodのマニフェストをあとからSkaffoldが改変することで、アプリケーションコンテナ内でDelveのServerを起動するという処理を加えます。
改変するポイントは、3つあります。1つ目が、コンテナの起動コマンドです。ここに、先ほど紹介したdlv exec --headless
コマンドを注入します。これによって、Podの内部で Delve サーバーを立ち上げます。しかし、開発時に用いているコンテナには、Delve コマンドはありません。あと2つの改変がそれを解決します。
それが、Volume マウントとInit Container 機能17です。Volume マウントの改変は、/dbg
ディレクトリをマウントするように変更されます。/dbg
ディレクトリは、Delveコマンドを共有するために使われます。そして、Init Container の改変は、Delve コマンドを/dbg
ディレクトリに配置するようなコンテナが指定されます。
それによって、Init Container のタイミングで、Delve コマンドが/dbg
ディレクトリにマウントされます。それをコンテナの起動コマンドが使用することによってアプリケーションコンテナ内で Delve が起動できるような仕組みを作っています。
また、DelveのServerへデバッグのための入力をうけつけられるよう、にポートフォワーディングの機能を用いる必要があることに注意してください。
skaffold debug --tail --filename=./_dev/skaffold/skaffold.yaml --port-forward=true
上記のようなコマンド一つでそれだけの処理を実行できます。
Duct Tapeのより具体的な処理は、以下のリンクを御覧ください。https://github.com/GoogleContainerTools/container-debug-support
Immutable Infrastructure
DockerfileとDeploymentを用いることで、Immutable Infrastructure を構築できます。
ビルドやデプロイの手順などはSkaffoldに隠蔽されているため、Skaffoldコマンド一つでコントローラを本番と同様の環境で動作させることができます。
しかし、前述の通り、Kubernetesのコントローラは、Kubernetesのコンポーネント郡に強く依存しています。
Kubernetesは、etcd, kube-apiserver, kube-scheduler, kube-controller-manager, kube-dnsなど非常に多くの構成要素からなり、これらを手続き的に用意するのは難しくなります。
これを解決するための適切な解決策は2種類あると思っています。
- パブリッククラウドサービスのマネージド Kubernetes を用いる
- ローカル開発環境に Kubernetes クラスタを構築する
私はKind18を用いて、後者を実現しました。
Kindは、Dockerコンテナとして Kubernetes のノードを起動することで、そのコンテナの中で、Kubernetes のコンポーネントを実現します。
Kubernetes を実行するのは Docker コンテナの内部のため、Kind そのものは単一のバイナリとして配布されます。
その結果、手軽に使い捨てできることが最大の魅力だと感じています。
また、Kubernetes 本体の開発にも使われているという大きな実績を持ちます。
Kind
実際にKindを用いてクラスタを構築するには以下のコマンドを用います。
kind create cluster --name kind-cluster-for-development --image kindest/node:v1.22.1
このコマンドを実行すると、kindest/node
というコンテナが Kubernetes のように振る舞うことがわかると思います。
kubeconfig
Kubeconfigファイルは、クラスタへのアクセスの際に、認証などに関する情報を設定するために使われるファイルです。
デフォルトでは、$HOME/.kube/config
を使用します。
Kindの場合は、--kubeconfig
オプションを用いることで別のPathのkubeconfigファイルを指定することもできます。
Isolated from other component
Kubernetesのコントローラにおいては、KubernetesのAPI Serverが外部のコンポーネントとして扱われることになります。
開発時には、特定のイベント6を起こすためにリソースに変更を加え、コントローラの挙動がリソースに変更を加えと繰り返すため、リソースの状態が安定せず、非正常系を含む動作確認を行うことが難しくなります。
このことから私は、Kubernetes コントローラーを検証する際に、望んでいるリソースの状態を宣言的にファイルで管理する方法を提案します。
下記のように、リソースを定義するマニフェストファイルをケースごとに作成します。
.
├── fixture
│ ├── test-case-1.yaml
│ ├── test-case-2.yaml
│ └── test-case-3.yaml
kubectl apply -f ./fixture/test-case-1.yaml
のように指定することで、必要なリソースを宣言的に作成することができます。
テスト終了後には、
kubectl delete
コマンドを使い生成したリソースを削除することで、次の実行に影響を与えません。
Automatically executable testing
私は上記に述べた開発環境を瞬時に用意し、Kubernetesコントローラを実行できるようにするパッケージとして、KET19を開発しました。
KETは、内部的にKind, Skaffold, kubectlを用いています。
自作のコントローラーをSkaffldを用いてビルド・デプロイします。
また、kubectl apply
コマンドを用いて、望んだリソース状態にし、テスト終了後にkubectl delete
コマンドを用いてリソースをもとに戻すような処理をサポートしています。
おわりに
本記事では、まず理想的な開発環境の条件について議論し、それを Kubernetes のコントローラ開発へ応用しました。
みなさんもぜひ、本記事で紹介した内容を踏まえてKubernetesコントローラーの開発をしてみてください。
1: Kubernetes https://kubernetes.io/ ↩
2: Kubernetes controller https://kubernetes.io/docs/concepts/architecture/controller/↩
3 RedHat How Operators work https://developers.redhat.com/articles/2021/06/22/kubernetes-operators-101-part-2-how-operators-work# ↩
4 Kubernetes components https://kubernetes.io/docs/concepts/overview/components/ ↩
5: CRD https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/ ↩
6: event https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/event ↩
7: client-go https://github.com/kubernetes/client-go ↩
8: code-generator https://github.com/kubernetes/code-generator ↩
9: kubebuilder https://github.com/kubernetes-sigs/kubebuilder ↩
10: operator-sdk https://github.com/operator-framework/operator-sdk ↩
11: metacontroller https://github.com/metacontroller/metacontroller ↩
12: Skaffold https://skaffold.dev/ ↩
13: Air https://github.com/cosmtrek/air ↩
14: Delve https://github.com/go-delve/delve ↩
15: Duct Tape https://github.com/GoogleContainerTools/container-debug-support ↩
16: Delve headless mode https://github.com/go-delve/delve/blob/master/Documentation/faq.md#how-do-i-use-delve-with-docker ↩
17 init containers https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ ↩
18: Kind https://github.com/kubernetes-sigs/kind ↩
19: KET(Kind E2e Testing Framework) https://github.com/riita10069/ket ↩