LoginSignup
293
131

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-09-13

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

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

kubectl-kbkb-rensa.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が使えます。

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

AnnotationがPodに設定されていると、kubectl kbkbで色がついた状態で表示されます。bashでしか試していません。
等幅フォント前提です。-Lオプションで見やすく全角表示することができます。
image.png

-wオプションでwatchできます。Podが消えていく様を見ることができます。
kubectl-kbkb-watch.gif

使い方

kbkb-controller

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

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

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

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

これだけです。

kubectl kbkb

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

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

あとは叩くだけです。

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個の関数だけに処理をゴリゴリ書いています。かんたん。
それ以外のコードはほとんど自動生成されたものをそのまま使っているだけです。

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://github.com/omakeno/kbkb

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は入力補完でなんとかなりますね。その分だけドキュメントは弱めですが。

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

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連鎖!

293
131
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
293
131