kubernetesでもぷよぷよがしたいので同じ色のPodが4個くっついたらdeleteされるcustom controller「くべくべ」を作った

Kubernetes使ってると、Nodeにえらい数のPodが溜まってくじゃないですか。消したくなりますよね。連鎖してほしいですよね。なりません？なので、4つ同じ色のPodが4個くっついたらdeleteされる、爽快感のあるカオスエンジニアリング用のcustom contollerを作りました。

deleteされるだけでは寂しいので、deleteされていく様子を見るためのkubectl pluginも作りました。合わせて使うとこんな感じになります。

![kubectl-kbkb-rensa.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38448/f53e2941-7d16-0a61-61b8-cbe207b2277c.gif)

左側の●のひとつひとつがPodです。Nodeが列に対応してます。6Node構成です。各色8個ずつpodを立てていて、右側にreplicasetの増減を置いてみました。

レポジトリはこちらです。

https://github.com/omakeno/kubectl-kbkb
https://github.com/omakeno/kbkb-controller

使い方と実装を説明します。使用は自己責任でお願いします。

## 動作

横軸をNode、縦軸をPodとして二次元のフィールドにPodが配置されてると見立てて動作します。それぞれcreationTimestampでソートされています。

特定のAnnotationからPodの色を判定し、周囲との隣接数を数えて必要数を超えていたら、それらのPodを削除します。AnnotationがないPodは白と判定されます。白はくっついても消えないですし、周りの色Podに巻き込まれて消えることもありません。

Annotationは`kbkb.k8s.omakenoyouna.net/color`です。Podごとに設定してください。`red`, `green`, `yellow`, `blue`, `purple`が使えます。 

```yaml
metadata:
  annotations:
    kbkb.k8s.omakenoyouna.net/color: blue
```

AnnotationがPodに設定されていると、`kubectl kbkb`で色がついた状態で表示されます。bashでしか試していません。
等幅フォント前提です。`-L`オプションで見やすく全角表示することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38448/e69b4091-9dbb-b276-1f1d-baf999b19d3b.png)

`-w`オプションでwatchできます。Podが消えていく様を見ることができます。
![kubectl-kbkb-watch.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/38448/cd67b98a-5c3a-7fdb-5de9-e723fbb7b1db.gif)


## 使い方

### kbkb-controller

kbkb-controllerと関連オブジェクトをdeployします。

```bash
kubectl apply -f https://raw.githubusercontent.com/omakeno/kbkb-controller/master/deploy/deploy.yaml
```

適用したいnamespace内にkbkbオブジェクトを作成します。
下記は「4個消し」ですが、「2個消し」「6個消し」などの設定も可能です。

```yaml
apiVersion: k8s.omakenoyouna.net/v1beta1
kind: Kbkb
metadata:
  name: kbkb-four
spec:
  kokeshi: 4
```

これだけです。


### kubectl kbkb

バイナリを落として、pathが通るようにしてください。
kubectlのpluginの機構で、kubectl-xxxxにpathが通っていると、`kubectl xxxx` のようにサブコマンドとして使えます。

```bash
wget https://github.com/omakeno/kubectl-kbkb/releases/download/v0.2.3/kubectl-kbkb
chmod +x kubectl-kbkb
sudo cp kubectl-kbkb <your-path>
```

あとは叩くだけです。

```bash
kubectl kbkb
```

`--watch,-w`, `--namespace,-n`, `--large,-L`, `--kubeconfig`のオプションがあります。

## 実装

ここでは詳細な解説はせず、紹介程度にします。コード量も少ないので、気になる方はリポジトリを見てみてください。
別途記録用に記事を書くかもです。

### kbkb-controller

いわゆるカスタムコントローラーです。podをwatchして、4つ隣接した同色のPodを見つけてdeleteします。

リポジトリはここです。
https://github.com/omakeno/kbkb-controller

Operator SDKをgolangで利用しています。Tutorialに沿って進めばめっちゃ簡単です。
https://sdk.operatorframework.io/

初心者でもTutorialに沿って進めばほぼほぼ完成されたコードを吐き出してくれるので、`Reconcile`のfunctionだけ実装すれば動きます。この1個の関数だけに処理をゴリゴリ書いています。かんたん。
それ以外のコードはほとんど自動生成されたものをそのまま使っているだけです。

```golang
func (r *KbkbReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
	ctx := context.Background()
	reqLogger := r.Log.WithValues("pod", req.NamespacedName)

	reqLogger.Info("Reconciling")

	// reconcileされたオブジェクトのNamespace
	listOption := &client.ListOptions{
		Namespace: req.Namespace,
	}

	// 同一Namespace内のkbkbを取得
	kbkbList := &k8sv1beta1.KbkbList{}
	if err := r.Client.List(ctx, kbkbList, listOption); err != nil {
		reqLogger.Error(err, "failed to get kbkb")
		return ctrl.Result{}, err
	}
	if len(kbkbList.Items) == 0 {
		reqLogger.Info("kbkb not found. Ignore not found")
		return ctrl.Result{}, nil
	}
	kbkbObj := kbkbList.Items[0]
	kokeshi := *(kbkbObj.Spec.Kokeshi)

	// Pod一覧とNode一覧を取得
	podList := &corev1.PodList{}
	if err := r.Client.List(ctx, podList, listOption); err != nil {
		reqLogger.Error(err, "failed to get list of pods")
		return ctrl.Result{}, err
	}

	nodeList := &corev1.NodeList{}
	if err := r.Client.List(ctx, nodeList); err != nil {
		reqLogger.Error(err, "failed to get list of nodes")
		return ctrl.Result{}, err
	}

	// 隣接判定
	kf := kbkb.BuildKbkbFieldFromList(podList, nodeList)
	if !kf.IsStable() {
		reqLogger.Info("All containers are not Ready.")
		return ctrl.Result{}, nil
	}

	erasablePods := kf.ErasableKbkbPodList(kokeshi)

	//podの削除
	for _, kp := range erasablePods {
		pod := &corev1.Pod{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: kp.ObjectMeta.Namespace,
				Name:      kp.ObjectMeta.Name,
			},
		}

		if err := r.Client.Delete(ctx, pod); err != nil {
			reqLogger.Error(err, "failed to delete pod", "deleteing pod", pod.ObjectMeta.Name)
		} else {
			reqLogger.Info("suceeded to delete pod", "deleted pod", pod.ObjectMeta.Name)
		}
	}

	return ctrl.Result{}, nil
}

```

隣接判定は別パッケージに切り出しました。[以前みた実装](https://qiita.com/yoitomakenouta/items/8b93d0b9dacf365ce0b1)を参考にしつつ、今回は深さ優先探索で判定してます。
https://github.com/omakeno/kbkb

```golang
func (kf *KbkbField) ErasableKbkbPodList(kokeshi int) []*KbkbPod {
	checkedPods := []*KbkbPod{}
	erasablePods := []*KbkbPod{}

	for x, col := range *kf {
		for y, _ := range col.kbkbs {
			var neighborPods []*KbkbPod
			neighborPods, checkedPods = kf.getNeighbors(x, y, checkedPods)
			if len(neighborPods) >= kokeshi {
				erasablePods = append(erasablePods, neighborPods...)
			}
		}
	}
	return erasablePods
}

// 再帰で深さ優先探索する関数
func (kf *KbkbField) getNeighbors(x int, y int, checkedPods []*KbkbPod) (neighborPods, checkedPodsAfter []*KbkbPod) {
	p := kf.GetKbkbPod(x, y)
	neighborPods = []*KbkbPod{p}
	if contains(checkedPods, p) {
		checkedPodsAfter = checkedPods
		return
	}
	checkedPodsAfter = append(checkedPods, p)

	if p.Color() == "white" {
		return
	}

	// 上下左右のpodを走査
	neighborPos := [][]int{
		{x + 1, y},
		{x - 1, y},
		{x, y + 1},
		{x, y - 1},
	}
	for _, pos := range neighborPos {
		if np := kf.GetKbkbPod(pos[0], pos[1]); np != nil && !contains(checkedPodsAfter, np) && np.Color() == p.Color() {
			var neighborPodsHere []*KbkbPod
			neighborPodsHere, checkedPodsAfter = kf.getNeighbors(pos[0], pos[1], checkedPodsAfter)
			neighborPods = append(neighborPods, neighborPodsHere...)
		}
	}
	return
}
```

というわけで作りは簡単、とはいえ`controller-runtime`などのパッケージは使えないといけないですし、kubebuilderのマーカーについても抑えとかなきゃです。私は@go_vargoさんの書籍で一通り学習しました。やりたいことはこの中で全部書いてありました。
https://booth.pm/ja/items/1566979

書籍の中でoperator-sdkの解説もありますが、現在はバージョンが上がっていて、コマンド体系も変わっているので注意が必要です。

またgolang自体も初心者だったので、プログラミング言語Go完全入門にもお世話になりました。
https://drive.google.com/file/d/1fLlg3Xw7CV680GQ65WkjxU5qX-PsApJg/view

### kubectl-kbkb

kubectlのプラグインです。単なるシングルバイナリのCLIツールです。
4個くっついたらdeleteされたくなるような見た目でpodを表示することができます。
Krewに入れてもらうのはさすがに無理かなあと思って諦めてます。Krewもカスタムのリポジトリが使えるようになったみたいなので、こういうネタツールでも入れてくれるリポジトリがあったらいいなあ。

リポジトリはここです。
https://github.com/omakeno/kubectl-kbkb

こちらはあまりまとまった情報がなかったのですが、下記をベースにいじって出来ました。
https://github.com/kubernetes/sample-cli-plugin

cobraをシンプルに使います。cli-runtimeを使うと良いらしいのですが、今回は使ってません。それでも`o.Execute`を実装すればcliツールが簡単に作れます。
あとは公式パッケージであるclient-goの使い方さえわかれば、controller同様に書けます。golangは入力補完でなんとかなりますね。その分だけドキュメントは弱めですが。

ちょっとだけ抜粋して載せます。

```golang
func CreateCmd() *cobra.Command {
	// コマンドを定義
	o := NewKbkbOptions()
	var rootCmd = &cobra.Command{
		Use:          "kbkb [flags]",
		Short:        "Show pods as kbkb format.",
		Example:      fmt.Sprintf(kbkbExample, "kubectl"),
		SilenceUsage: true,
		RunE: func(c *cobra.Command, args []string) error {
			if err := o.Execute(c, args); err != nil {
				return err
			}

			return nil
		},
	}

	// オプションをフラグとして設定
	rootCmd.PersistentFlags().StringVarP(&o.namespace, "namespace", "n", "default", "specify namespace to show as kbkb format.")
	rootCmd.PersistentFlags().BoolVarP(&o.watch, "watch", "w", false, "watch kbkb")
	rootCmd.PersistentFlags().StringVarP(&o.kubeconfig, "kubeconfig", "", filepath.Join(homeDir(), ".kube", "config"), "(optional) absolute path to the kubeconfig file")
	rootCmd.PersistentFlags().BoolVarP(&o.large, "large", "L", false, "view on large size")
	return rootCmd
}

func (o *KbkbOptions) Execute(cmd *cobra.Command, args []string) error {

	config, err := clientcmd.BuildConfigFromFlags("", o.kubeconfig)
	if err != nil {
		panic(err.Error())
	}

	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		panic(err.Error())
	}

	if o.watch {
		o.Watch(clientset)
	} else {
		o.Get(clientset)
	}

	return nil
}
func (o *KbkbOptions) Get(clientset *kubernetes.Clientset) {
	// pod, nodeの一覧取得
	podList, err := clientset.CoreV1().Pods(o.namespace).List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		panic(err.Error())
	}
	nodeList, err := clientset.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{})
	if err != nil {
		panic(err.Error())
	}

	// 描画処理(kbkbパッケージを使うだけ)
	kf := kbkb.BuildKbkbFieldFromList(podList, nodeList)
	writer := bashoverwriter.GetBashoverwriter()
	var kcs kbkb.KbkbCharSet
	if o.large {
		kcs = kbkb.GetKbkbCharSetWide()
	} else {
		kcs = kbkb.GetKbkbCharSet()
	}
	kcs.PrintKbkb(&writer, kf)
}
```

肝心の出力のフォーマットも、kbkbパッケージに寄せちゃってるので、こっちは至ってシンプルです。

## まとめ

というわけで、削除処理と可視化はできました。
client-goやcontroller-runtimeやらのパッケージにkubebuilderやらoperator-sdkやら、すごく綺麗に整理されているので本当に簡単に実装が出来ます。ありがたい。

さて、まだやりたいことが残ってます。
せっかくCNDT2020で話を聞いたのでOperator Lifecycle Managerとかで使えるようにしてみるのもいいのですが、まずは機能。

* 全てのPodがRunningになるとPodを2つ生成してくれるCustom Controller
* createされるpodにランダムでAnnotationを付与するAdmission Controller
* Queuingされるpodを2個ずつ操作して手動でnodeにschedulingするCustom Scheduler

2つずつ生成されるpodを上から落として積み上げて行きたくなりますよね。
めざせ19連鎖！