Kubernetes使ってると、Nodeにえらい数のPodが溜まってくじゃないですか。消したくなりますよね。連鎖してほしいですよね。なりません?なので、4つ同じ色のPodが4個くっついたらdeleteされる、爽快感のあるカオスエンジニアリング用のcustom contollerを作りました。
deleteされるだけでは寂しいので、deleteされていく様子を見るためのkubectl pluginも作りました。合わせて使うとこんな感じになります。
左側の●のひとつひとつが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
オプションで見やすく全角表示することができます。
-w
オプションでwatchできます。Podが消えていく様を見ることができます。
使い方
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連鎖!