前回に引き続きKubernetes Operator。
今日はOperatorからそれが生成したPod/コンテナ内でexecを実行するところにチャレンジしてみたい。
とりあえずGoogleでリサーチしてみた感じでは、Kubernetesのクライアントライブラリにremotecommandと言うのがあるのでそれを使うのが簡単で良さそうだ。
・・・と思うだろうが、これは無理筋である。remotecommand.NewSPDYExecutor()という関数でチャチャっとOperator内からexec実行と言うのは世界中のOperator仲間が夢見ている事ではあるらしいが、かなり一筋縄では行かない。とりあえずschema.GroupVersionKindなんだろうねというところで今回は挫折した。
なんか、ドキュメントの整備具合とか、Operator開発をちょっと試みただけでもGo言語というかそのエコシステムに限界を感じるな。。
そういう時はとりあえずos/execである。OSでコマンド実行すれば大抵の事は何とかなる。
Operatorのひな型を作る
カスタムリソースの作成
# mkdir -p ~/projects/examples
# cd ~/projects/examples
# operator-sdk new exec-test --repo github.com/examples/exec-test
# cd exec-test
# operator-sdk add api --api-version=example.com/v1alpha1 --kind=ExecTest
ファイル「pkg/apis/example/v1alpha1/exectest_types.go」編集して、どうすっか、Specに文字列のプロパティ「Message」を追加。
type ExecTestSpec struct {
Message string `json:"message"`
}
CRD等を再生成。
# operator-sdk generate k8s
# operator-sdk generate crds
CRのサンプルにプロパティ「message」を追加。
ファイル「deploy/crds/example.com_v1alpha1_exectest_cr.yaml」を編集する。
(修正前)
spec:
# Add fields here
size: 3
(修正後)
spec:
message: "Hello My Child!"
また、Operator内でkubectl execを実行するため、それが可能な様にSAというかroleに権限を追加する。「deploy/role.yaml」の最後に以下を追加する。
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
コントローラーのDockerイメージ
今回Operator内でkubectlコマンドやシェルスクリプトを実行するため、Dockerイメージに仕込んでおく。
「build/Dockerfile」を修正しても良いだろうが、その中で「build/bin」ディレクトリの中を丸ごと/usr/local/binにコピーしている処理があるので、build/binに実行したいファイルを全部配置する。
kubectlのダウンロードについては以下。
https://kubernetes.io/docs/tasks/tools/install-kubectl/#install-kubectl-on-linux
# pushd build/bin
# curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
# chmod +x kubectl
# cat>custom_probe<<'EOF'
#!/bin/sh
kubectl exec $1 -- sh -c "echo $(date) $2 >>~/messages"
kubectl exec $1 -- ls -l ~/messages
EOF
# chmod +x custom_probe
# popd
カスタムコントローラーの作成
カスタムコントローラーのひな型を作る。
# operator-sdk add controller --api-version=example/v1alpha1 --kind=ExecTest
続いて、ファイル「pkg/controller/exectest/exectest_controller.go」を編集する。
importの追加
ファイル先頭のimportを編集。パッケージ "os/exec" と "time" を追加する。
import (
...
"os/exec"
"time"
)
custom_probe実行の処理を追加
130行目あたり、Operatorの子オブジェクトが生成済みの場合の処理を弄れば良いか。
(修正前)
// Pod already exists - don't requeue
(修正後)
// exec-test
reqLogger.Info("exec-test: ", "Pod.Namespace", found.Namespace, "Pod.Name", found.Name, "Message", instance.Spec.Message)
out, err := exec.Command("custom_probe", found.Name, instance.Spec.Message).Output()
reqLogger.Info("exec-test(output): ", "out", out)
Reconcile loopをRequeueAfterに変更
コンテナ生成時、exec.Command実行時のそれぞれのreturn処理をRequeueAfterに変え、Reconcileが5秒に一回実行されるようにする。
(123行目あたり)
// // Pod created successfully - don't requeue
// return reconcile.Result{}, nil
// Pod created successfully - and requeue
return reconcile.Result{RequeueAfter: time.Second*5}, nil
(135行目あたり)
// return reconcile.Result{}, nil
return reconcile.Result{RequeueAfter: time.Second*5}, nil
}
Operatorをビルドしてデプロイ
# operator-sdk build exec-test
# sed -i "s|REPLACE_IMAGE|exec-test|g" deploy/operator.yaml
# sed -i "s|imagePullPolicy:.*|imagePullPolicy: Never|" deploy/operator.yaml
# kubectl create -f deploy/service_account.yaml
# kubectl create -f deploy/role.yaml
# kubectl create -f deploy/role_binding.yaml
# kubectl create -f deploy/crds/example.com_exectests_crd.yaml
# kubectl create -f deploy/operator.yaml
# kubectl create -f deploy/crds/example.com_v1alpha1_exectest_cr.yaml
動作確認
CRまでデプロイした所で、クラスターにはoperatorとCRに紐づいたPodが1つずつ生成されているはずである。
[root@ip-172-26-4-124 exec-test]# kubectl get pod
NAME READY STATUS RESTARTS AGE
example-exectest-pod 1/1 Running 0 8s
exec-test-5df88dfb47-s9lts 1/1 Running 0 14s
「exec-test-*」がOperatorであり、「example-exectest-pod」がCRを反映して生成されたOperatorの子Podである。
カスタムコントローラーで、コマンド実行後にその出力をログに出すようにしていたためまずはそれを見てみよう。
[root@ip-172-26-4-124 exec-test]# kubectl logs --tail 3 exec-test-5df88dfb47-s9lts
{"level":"info","ts":1583056428.8085327,"logger":"controller_exectest","msg":"Reconciling ExecTest","Request.Namespace":"default","Request.Name":"example-exectest"}
{"level":"info","ts":1583056428.8086174,"logger":"controller_exectest","msg":"exec-test: ","Request.Namespace":"default","Request.Name":"example-exectest","Pod.Namespace":"default","Pod.Name":"example-exectest-pod","Message":"Hello My Child!"}
{"level":"info","ts":1583056429.2487488,"logger":"controller_exectest","msg":"exec-test(output): ","Request.Namespace":"default","Request.Name":"example-exectest","out":"LXJ3LXItLXItLSAgICAxIHJvb3QgICAgIHJvb3QgICAgICAgICAgIDQ4NCBNYXIgIDEgMDk6NTMgL3Jvb3QvbWVzc2FnZXMK"}
「"out":"LXJ3LXItLXItLSAgICAxIHJvb3QgICAgIHJvb3QgICAgICAgICAgIDQ4NCBNYXIgIDEgMDk6NTMgL3Jvb3QvbWVzc2FnZXMK"」は、シェルスクリプトがコンテナでlsを実行した結果の出力なのだが、base64エンコードされているのでデコードする。
[root@ip-172-26-4-124 exec-test]# echo "LXJ3LXItLXItLSAgICAxIHJvb3QgICAgIHJvb3QgICAgICAgICAgIDQ4NCBNYXIgIDEgMDk6NTMgL3Jvb3QvbWVzc2FnZXMK" | base64 -d
-rw-r--r-- 1 root root 484 Mar 1 09:53 /root/messages
うんまあ、良さそうじゃないか。
子Pod「example-exectest-pod」を調べると「/root/messages」というファイルが作られており、これは親であるOperatorがkubectl execを実行した結果としてできたものである。
[root@ip-172-26-4-124 exec-test]# kubectl exec example-exectest-pod cat /root/messages
Sun Mar 1 09:52:59 UTC 2020 Hello My Child!
Sun Mar 1 09:52:59 UTC 2020 Hello My Child!
Sun Mar 1 09:53:05 UTC 2020 Hello My Child!
Sun Mar 1 09:53:10 UTC 2020 Hello My Child!
Sun Mar 1 09:53:16 UTC 2020 Hello My Child!
...
まあ、大体5秒おきにexec(custom_probe)が実行されている様子も見て取れる。
Operatorを消す
クラスターにデプロイしたOperatorを削除する。
# kubectl delete -f deploy/crds/example.com_v1alpha1_exectest_cr.yaml
# kubectl delete -f deploy/operator.yaml
# kubectl delete -f deploy/role.yaml
# kubectl delete -f deploy/role_binding.yaml
# kubectl delete -f deploy/service_account.yaml
# kubectl delete -f deploy/crds/example.com_exectests_crd.yaml
おわりに
困ったときはOS上でコマンド実行すれば何とかなる。
ていうか、カスタムコントローラーをGO言語でガリガリ作り込むって多分あまり現実的じゃないので、実用的と言うかちょっと込み入ったことをOperatorで実装しようと思ったら、処理の大半をシェルスクリプトとかGO言語の外に移管するんじゃないか?
で、その場合ReconcileはKubernetesオブジェクトのイベントがトリガーではなく、ReconcileAfterで定期的に実行する形にせざるを得ない。
次はOLM、Operator Lifecycle Managerあたりか。
また日を改めて。