KubebuilderでKubernetesのカスタムコントローラを作ってみる

Kubernetesの特徴の一つに、その拡張性の高さがあります。
拡張性を感じる機能は非常に多くありますが、本記事ではその内の一つ、カスタムコントローラについて注目し、KubebuilderというSDKを使って実際にカスタムコントローラを作ってみます。

## TL;DR
- カスタムコントローラのSDK Kubebuilderを動かしてみた
- Kubebuilderのサンプルを読み解いてみた
- Deploymentで許可していないイメージを動かしたら勝手に止めてしまうカスタムコントローラを書いてみた

## Kubernetesのカスタムコントローラとは?
KubernetesではManifestファイルやCLIの操作により、DeploymentやConfigMap, Job, Podなどが動作します。
これらのManifestはAPIサーバで解釈され、保存されるだけであり、実処理はすべてコントローラにより実装されています。
KubernetesではこのManifestをDeclarative Configurationで望ましい状態・あるべき状態を定義しています。
コントローラではこの状態を読み取り、実際にその状態にするためのビジネスロジックが定義されています。
例えば、Deploymentですと、[DeploymentController](https://github.com/kubernetes/kubernetes/blob/master/pkg/controller/deployment/deployment_controller.go)により内部ロジックが定義されています。

さて、このコントローラですが、自前で作成することもでき、Kubernetesクラスタにそのコントローラを適用することができます。
カスタムコントローラ[^1]で独自のマニフェストファイルを扱いたい場合は、KubernetesのAPIにCRD(Custom Resource Definition)をAPIサーバに登録する必要があります。
詳細が気になる方は下記公式ドキュメントや弊社の登壇資料を読んでください。

- [Custom Resources
 ](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/)
- [Z Lab 登壇資料](https://speakerdeck.com/shmurata/how-we-accomplish-noops-by-kubernetes-operator?slide=43)


## Kubebuilderとは?
https://book.kubebuilder.io/

カスタムコントローラをスクラッチから作るためには知るべきことが多く、またKubernetesのバージョンに合わせたライブラリの準備など、気にかけることが多く非常に大変です。
Kubebuilderはカスタムコントローラを作るためのSDKであり、これらの手間を軽減することができます。
詳細は公式に書いてあるものを見てください。[^2]
さっそく、Kubebuilderでカスタムコントローラを作ってみましょう。

## 事前準備
まずは公式手順に従いセットアップしましょう。https://book.kubebuilder.io/quick-start.html

今回の実行環境は以下の通りです。

- macOS Mojave 10.14.1
- go1.11.2 darwin/amd64
- dep version v0.5.0
- kubebuilder v1.0.5
- Kubernetes v1.12.3 (minikube) ※kubeconfigの設定は完了しておいてください


### Project作成
Projectを作成します。今回は以下の設定で作っています。

- ドメイン: foo.bar
- ライセンス: apache2ライセンス
- オーナー: TakanariKo

まずは、Project用のディレクトリを作ります。

```bash
# Go 1.12未満の場合
$ mkdir -p $GOPATH/src/foo.bar && cd $_

# 以下のエラーになる場合は、Go 1.12以降の手順に沿ってください
#   go: modules disabled inside GOPATH/src by GO111MODULE=auto; see 'go help modules'

# Go 1.12以降の場合
$ mkdir -p src/foo.bar && cd $_
```

作成したディレクトリの中で初期化用のコマンドを実行します。

```
$ kubebuilder init --domain foo.bar --license apache2 --owner "TakanariKo"
Run `dep ensure` to fetch dependencies (Recommended) [y/n]?
y
dep ensure
Running make...
.....

```

完了するとGOPATH/src/foo.bar以下にProjectが作成されます。


### API作成
次にAPIを作成します。これにより、APIとカスタムコントローラのスケルトンができます。
カスタムリソースのグループ・名前・バージョンは任意の値で作成可能です。
今回は以下の設定で作成します。理由は後述。

- group: police
- kind: WhiteList
- version: v1beta1


```bash
$ kubebuilder create api --group police --version v1beta1 --kind WhiteList
Create Resource under pkg/apis [y/n]?
y
Create Controller under pkg/controller [y/n]?
y
Writing scaffold for you to edit...
...
```

これで雛形のソースコードができました。
以下がファイルツリーですが、基本的には以下の**※**を変更します。

```
├── Dockerfile
├── Gopkg.lock
├── Gopkg.toml
├── Makefile
├── PROJECT
├── bin
│   └── manager
├── cmd
│   └── manager
│       └── main.go
├── config
│   ├── crds
│   │   └── police_v1beta1_whitelist.yaml
│   ├── default
│   │   ├── kustomization.yaml
│   │   └── manager_image_patch.yaml
│   ├── manager
│   │   └── manager.yaml
│   ├── rbac
│   │   ├── rbac_role.yaml
│   │   └── rbac_role_binding.yaml
│   └── samples
│       └── police_v1beta1_whitelist.yaml    ※サンプルマニフェスト
├── cover.out
├── hack
│   └── boilerplate.go.txt
├── pkg
│   ├── apis
│   │   ├── addtoscheme_police_v1beta1.go
│   │   ├── apis.go
│   │   └── police
│   │       ├── group.go
│   │       └── v1beta1
│   │           ├── doc.go
│   │           ├── register.go
│   │           ├── v1beta1_suite_test.go
│   │           ├── whitelist_types.go     ※マニフェストの定義
│   │           ├── whitelist_types_test.go
│   │           └── zz_generated.deepcopy.go
│   ├── controller
│   │   ├── add_whitelist.go
│   │   ├── controller.go
│   │   └── whitelist
│   │       ├── whitelist_controller.go    ※カスタムコントローラ
│   │       ├── whitelist_controller_suite_test.go
│   │       └── whitelist_controller_test.go
│   └── webhook
│       └── webhook.go
└── vendor
    ├── ...

```

続いてKubernetesにCRDを登録し、カスタムコントローラをローカルで実行します。



## クラスタにCRDを登録
CRDのマニフェストや必要なRBACのファイルは自動生成されます。
すべてMakefileで定義されているため、Kubernetesに簡単に登録できます。

```bash
$ make install
go run vendor/sigs.k8s.io/controller-tools/cmd/controller-gen/main.go all
CRD manifests generated under '/Users/takanariko/go/src/foo.bar/config/crds'
RBAC manifests generated under '/Users/takanariko/go/src/foo.bar/config/rbac'
kubectl apply -f config/crds
customresourcedefinition.apiextensions.foo.bar/whitelists.police.foo.bar created
```


## カスタムコントローラのビルド・実行
こちらもMakefileで定義されているので、実行するだけで、カスタムコントローラのビルドと実行がされます。

```bash
$ make run
go generate ./pkg/... ./cmd/...
go fmt ./pkg/... ./cmd/...
go vet ./pkg/... ./cmd/...
go run ./cmd/manager/main.go
{"level":"info","ts":1544584857.496564,"logger":"entrypoint","msg":"setting up client for manager"}
{"level":"info","ts":1544584857.4984121,"logger":"entrypoint","msg":"setting up manager"}
{"level":"info","ts":1544584857.5475972,"logger":"entrypoint","msg":"Registering Components."}
{"level":"info","ts":1544584857.547628,"logger":"entrypoint","msg":"setting up scheme"}
{"level":"info","ts":1544584857.547704,"logger":"entrypoint","msg":"Setting up controller"}
{"level":"info","ts":1544584857.547759,"logger":"kubebuilder.controller","msg":"Starting EventSource","controller":"whitelist-controller","source":"kind source: /, Kind="}
{"level":"info","ts":1544584857.548377,"logger":"kubebuilder.controller","msg":"Starting EventSource","controller":"whitelist-controller","source":"kind source: /, Kind="}
{"level":"info","ts":1544584857.5485451,"logger":"entrypoint","msg":"setting up webhooks"}
{"level":"info","ts":1544584857.548558,"logger":"entrypoint","msg":"Starting the Cmd."}
{"level":"info","ts":1544584857.649017,"logger":"kubebuilder.controller","msg":"Starting Controller","controller":"whitelist-controller"}
{"level":"info","ts":1544584857.749523,"logger":"kubebuilder.controller","msg":"Starting workers","controller":"whitelist-controller","worker count":1}

```

## サンプルマニフェストの適用
さて、生成されたマニフェストを適用すると何が起きるのか見てみましょう。
まずはサンプルで用意されたManifestです。
apiVersion、kindは前述の`kubebuilder create api`時に指定したものになっています。
specには`foo:bar`というmapが入っているのみです。

```bash
$ cat config/samples/police_v1beta1_whitelist.yaml
apiVersion: police.foo.bar/v1beta1
kind: WhiteList
metadata:
  labels:
    controller-tools.k8s.io: "1.0"
  name: whitelist-sample
spec:
  # Add fields here
  foo: bar
```

マニフェストをデプロイしてみましょう。

```bash
$ kubectl get deploy
No resources found.

$ kubectl apply -f config/samples/police_v1beta1_whitelist.yaml
whitelist.police.foo.bar/whitelist-sample created

$ kubectl get deploy -o wide
NAME                          DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE       CONTAINERS   IMAGES    SELECTOR
whitelist-sample-deployment   1         1         1            1           10s       nginx        nginx     deployment=whitelist-sample-deployment
```

存在していなかったdeploymentがなぜか生成され、nginxのイメージが動いています。
どうやら、WhiteList Manifestをapplyするとdeploymentが生成される仕組みのようです。
気に食わないので、消してみましょう。

```bash
$ kubectl delete deployments whitelist-sample-deployment
deployment.extensions "whitelist-sample-deployment" deleted
$ kubectl get deploy -o wide
NAME                          DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE       CONTAINERS   IMAGES    SELECTOR
whitelist-sample-deployment   1         1         1            1           2s        nginx        nginx     deployment=whitelist-sample-deployment

```
復活しています。。
コントローラのログを見ると、消した直後にDeploymentを作成している様子がわかります。

```
2018/12/12 12:57:26 Creating Deployment default/whitelist-sample-deployment
2018/12/12 12:57:26 Updating Deployment default/whitelist-sample-deployment
```

ならば、DeploymentのImageを書き換えてみましょう。

```bash
$ kubectl set image deployments whitelist-sample-deployment nginx=httpd
deployment.extensions/whitelist-sample-deployment image updated
$ kubectl get deploy -o wide
NAME                          DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE       CONTAINERS   IMAGES    SELECTOR
whitelist-sample-deployment   1         1         1            1           3m17s     nginx        nginx     deployment=whitelist-sample-deployment
```
httpdに変更したイメージがnginxに戻されています。。
このコントローラは以下のような挙動をしているようです。

- カスタムリソースをapplyすると、カスタムリソースの名前を使ったnginxのdeploymentを生成する
- deploymentを削除したり、中身を変更しても強制的に生成時の値に戻してしまう。

ここまでわかったところで、カスタムコントローラのソースコードを見て、答え合わせをします。
注目すべきは2つの関数のみです。

```go:pkg/controller/whitelist/whitelist_controller.go
func add(mgr manager.Manager, r reconcile.Reconciler) error {
...

	// Watch for changes to WhiteList
    // ※カスタムリソースのWatch※
	err = c.Watch(&source.Kind{Type: &policev1beta1.WhiteList{}}, &handler.EnqueueRequestForObject{})
	if err != nil {
		return err
	}

	// TODO(user): Modify this to be the types you create
	// Uncomment watch a Deployment created by WhiteList - change this for objects you create
    // ※ WhiteListにより生成されたDeploymentリソースのCRUD操作をWatchし、WhiteListのReconcileループにenque※
	err = c.Watch(&source.Kind{Type: &appsv1.Deployment{}}, &handler.EnqueueRequestForOwner{
		IsController: true,
		OwnerType:    &policev1beta1.WhiteList{},
	})
	if err != nil {
		return err
	}

}
```
add関数内では、Kubernetesオブジェクトに対するCRUD操作をWatchすることができます。
このadd関数では、2種類のオブジェクトをWatchしています。
一つはカスタムリソースであるWhiteList, もう一つはWhiteListがオーナーとして生成したDeploymentです。
Watchを設定しておくと、後述のReconcile関数が呼ばれます。
Reconcile関数でどう扱われているか見てみましょう。

```go:pkg/controller/whitelist/whitelist_controller.go
func (r *ReconcileWhiteList) Reconcile(request reconcile.Request) (reconcile.Result, error) {
    // ※CRUD操作のあったカスタムオブジェクトを取得※
	// Fetch the WhiteList instance
	instance := &policev1beta1.WhiteList{}
	err := r.Get(context.TODO(), request.NamespacedName, instance)
	if err != nil {
		if errors.IsNotFound(err) {
			// Object not found, return.  Created objects are automatically garbage collected.
			// For additional cleanup logic use finalizers.
			return reconcile.Result{}, nil
		}
		// Error reading the object - requeue the request.
		return reconcile.Result{}, err
	}

    // ※WhiteListのNameとNamespaceをもとにDeploymentの構造体を作成※
	// TODO(user): Change this to be the object type created by your controller
	// Define the desired Deployment object
	deploy := &appsv1.Deployment{
		ObjectMeta: metav1.ObjectMeta{
			Name:      instance.Name + "-deployment",
			Namespace: instance.Namespace,
		},
		Spec: appsv1.DeploymentSpec{
			Selector: &metav1.LabelSelector{
				MatchLabels: map[string]string{"deployment": instance.Name + "-deployment"},
			},
			Template: corev1.PodTemplateSpec{
				ObjectMeta: metav1.ObjectMeta{Labels: map[string]string{"deployment": instance.Name + "-deployment"}},
				Spec: corev1.PodSpec{
					Containers: []corev1.Container{
						{
							Name:  "nginx",
							Image: "nginx",
						},
					},
				},
			},
		},
	}
    // ※このDeploymentはWhiteListオブジェクトにより作られたという紐づけ※
    // ※これによりカスタムオブジェクト削除時にDeploymentも削除される※
    // ※またadd関数内のWatch時のEnqueの対象にもなる※
	if err := controllerutil.SetControllerReference(instance, deploy, r.scheme); err != nil {
		return reconcile.Result{}, err
	}
	// TODO(user): Change this for the object type created by your controller
	// Check if the Deployment already exists
	found := &appsv1.Deployment{}
	err = r.Get(context.TODO(), types.NamespacedName{Name: deploy.Name, Namespace: deploy.Namespace}, found)
	if err != nil && errors.IsNotFound(err) {
		log.Printf("Creating Deployment %s/%s\n", deploy.Namespace, deploy.Name)
        // ※Deploymentを生成※
		err = r.Create(context.TODO(), deploy)
		if err != nil {
			return reconcile.Result{}, err
		}
	} else if err != nil {
		return reconcile.Result{}, err
	}

	// TODO(user): Change this for the object type created by your controller
	// Update the found object and write the result back if there are any changes
    // ※もしもSpecの内容が書き換えられたら生成時の状態に強制的に変更※
	if !reflect.DeepEqual(deploy.Spec, found.Spec) {
		found.Spec = deploy.Spec
		log.Printf("Updating Deployment %s/%s\n", deploy.Namespace, deploy.Name)
		err = r.Update(context.TODO(), found)
		if err != nil {
			return reconcile.Result{}, err
		}
	}
```
これでDeploymentが勝手に生成されたり、消したり、Specを編集しても巻き戻される理由がわかりました。
このようにして、カスタムコントローラが作ることができました。
実際にコントローラをイメージ化し、Push、Kubernetesクラスタ内に以下のコマンドでデプロイできます。

```
$ export IMG=[Push先のイメージ名]
$ make docker-build
$ make docker-push

# 必要に応じて、コントローラのデプロイ先NamespaceはKustomization.yamlで設定しましょう
$ make deploy

```

## 独自コントローラ Container Image Police 
ここまでわかったところで、実践です。
今回は、Deploymentで不正なコンテナイメージを動かしていたら、そのDeploymentを勝手に削除してしまうコントローラ、**Container Image Police**です。
ソースコードはこちらです。
https://github.com/TakanariKo/kubebuilder-example-container-police
以下のようなManifestを使います。

```
apiVersion: police.foo.bar/v1beta1
kind: WhiteList
metadata:
  labels:
    controller-tools.k8s.io: "1.0"
  name: whitelist-sample
spec:
  images:
    - name: nginx
      tags: 
        - latest
        - 1.14.2
    - name: gcr.io/busybox
      tags: 
        - latest
```
このホワイトリストを任意Namespaceで登録すると、そのNamespace内のDeploymentに対し、ホワイトリスト外のImage,Tagを使用している場合、Deploymentを削除します。ちなみにImageのDigest表記には対応させてません。

```bash

# 上記Manifestを適用
$ kubectl apply -f config/samples/police_v1beta1_whitelist.yaml
# ホワイトリストで許可されているイメージのPodを持つDeployment nginxを作成
$ kubectl run nginx --image=nginx
deployment.apps/nginx created
$ kubectl get deploy -o wide
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE       CONTAINERS   IMAGES         SELECTOR
nginx     1         1         1            1           18s       nginx        nginx:1.14.2   run=nginx

# ホワイトリストで許可されていないイメージのPodを持つDeployment illegal-nginxを作成
$ kubectl run illegal-nginx --image=nginx:1.15.7
deployment.apps/nginx2 created
# すぐにコントローラによって削除されてしまう
$ kubectl get deploy -o wide
NAME      DESIRED   CURRENT   UP-TO-DATE   AVAILABLE   AGE       CONTAINERS   IMAGES         SELECTOR
nginx     1         1         1            1           47s       nginx        nginx:1.14.2   run=nginx
```
見た目の変化がないので、地味ですね。。
詳細はコードを見ていただければ早いですが、このコントローラはWhiteListというカスタムリソースと、DeploymentをWatchします。
WhiteListの作成・更新があると、WhiteListのnamespace内のDeploymentすべてに対し、妙なイメージが動いていないか確認します。
また、Deploymentの作成・更新があるとそのDeploymentがWhiteListの対象かどうか、妙なイメージが動いていないかを確認しています。

このContainer Image Policeをうっかりkube-systemに対してapplyしない方がよいでしょう。Kubernetesクラスタの内乱の元になります。
このコントローラをクラスタ内にDeploymentとしてデプロイする際は、デプロイ先のnamespaceには気をつけましょう。うっかり自分と同じnamespaceに置くと、自分を逮捕するかわいいところがあります。

## さいごに
Kubebuilderに代表されるSDKの登場で、Kubernetesのカスタムコントローラ開発の敷居がぐっと下がったように感じます。
今回実際に触ってみて、SDKの詳細を知らなくても、サクッと開発できることを実感しました。
このカスタムコントローラに代表されるKubernetes自体をフレームワークのように使う仕組みは今後も様々なところで利用されるでしょう。
そういう意味でも気軽に作り、親しんでおくのも大事かもしれません。

Container Image PoliceのおかげでKubernetesの秩序が保たれた。

![computer_cyber_police.png](https://qiita-image-store.s3.amazonaws.com/0/100376/2da7800c-c0c1-47e9-3ad9-86af0b8678c0.png)

このエントリは、Z Lab Advent Calendar 2018 の12日目として業務時間中に書きました。



[^1]: 主にKubernetesのリソースに対して処理を行うものをコントローラと呼びますが、それとは異なり、特定のアプリケーション向けのカスタムコントローラはオペレータと呼ばれます。オペレータにはEtcd, Spark, Prometheusや弊社内製Kubernetes as a Serviceなどがあります。
[^2]: https://book.kubebuilder.io/getting_started/what_is_kubebuilder.html