Kubernetes Meetup Tokyo 23でClusterAPIについてLTしてきた

https://k8sjp.connpass.com/event/145942

たまにはLTするかなーと思って、イベントページが公開されてすぐにLT枠で申し込み、LTしてきた。テーマは最近触っていたClusterAPIについて。v1alpha1からv1alpha2で結構変化があるので、そこを中心にトークした。あとはClusterAPIのペパボでの利用事例や、コントローラ実装で使える小ネタなども話した。

今回はオペレータ・コントローラ会ということで、非常に濃いセッションばかりでよかったなあ。

困った時、質問する時に整理すること

少し前、今年入社した人の研修に少し関わっていたのだけど、その時Slackに投下したテキストを手直ししてこちらにも書いておきます。

意識するといいポイントがあって、それは、何か困ったことがあったり、質問する時に「前提条件」と「期待していた挙動」、「実際に起きた挙動」を整理するといい。

「前提条件」は、例えば「自分が今動かそうとしているソースコード」「使っているソフトウェアのバージョン」などを指す。「期待していた挙動」とは、「何をしようとしているのか」や「こうなって欲しかった」などを指す。「実際に起きた挙動」とは、「実行したコマンドとその実行結果のログ」だったり、「操作した結果発生した挙動」などを指す。

これらの事実を質問者と回答者が共有することで、より適切な回答が行える。最初の質問に欠けているものがあると前提条件がずれて間違った回答をしてしまう可能性があったりする。リモートのエンジニアだったり、異なる拠点にいるエンジニアだと同じ画面を見てあれこれ考えることができない(Google Meetのようなツールを使って画面共有することはできるけどね)ので、効率的な共有は大事だろう。

毎回あらゆる情報を整理しているとそれはそれで時間がもったいない。なので、ポイントとなる事実に絞って集めるとよい。難しいけど少しずつ意識して取り組んでもらえるとどんどん「良い質問」ができるようになってくると思う。

fishのヒストリ重複が邪魔なので掃除するツールを作った

fishに乗り換えて不便に感じることも減ってきたのだけど、ヒストリの重複が邪魔だなーと思ったので重複を掃除できるようにした。標準の設定でそれらしいものは見つからなかったのでGoでさっと書いた。設定でもしあれば教えてください。

https://github.com/takaishi/fish-history-gc

以下のようにfish_postexecで実行して重複しているエントリを消すようにしている。実行時刻は新しいものを残すようにした。

function history-merge --on-event fish_postexec
  history --save
  history --merge
  fish-history-gc -overwrite
end

ヒストリサイズが大きくなったら遅くなるかもだけどその時考えることにする。

ingress-nginxの仕組み

ingress-nginxを使っているのだけど、中の仕組みをあまり知らなかったので調べた。まず、ngix-ingress-controllerからバックエンドへリクエストがどう流れるのかだが、簡単に以下の図のようになる。

nginx-ingress-controllerまでの流れはService Typeによるが、ここではNodePortを指定している。externalTrafficPolicyをClusterにしておくと、どのノードにリクエストが届いてもnginx-ingress-controller Podにルーティングされる。もちろん、Podが複数ある場合は、ロードバランスされる。

さて、気になるのはnginx-ingress-controllerから先。結論から書くと、IngressにのBackendに設定したサービスのエンドポイントにロードバランスしている。Podの中をみてみると、nginx-ingress-controllerとnginxが動いている。nginx-ingress-controllerはk8sのapi-serverにアクセスし、サービスのエンドポイントを取得する。nginx側ではluaが動いていて、Backendを登録するためのURLを提供している。ngix-ingress-controllerはそのURLへPOSTしてサービスのエンドポイントをnginxに伝える。

エンドポイントはngx.shared.configuration_dataの中に保存されており、balancer_by_lua_blockから呼ばれるbalancer.balance()から参照され、ロードバランスするという仕組み。なお、ロードバランスのアルゴリズムはラウンドロビンとEWMAの2種類が使える。デフォルトはラウンドロビン。他にも色々設定とかありそうだけど、概ねの仕組みはこのようになっているようだ。

Reference:

I wrote my first Kubernetes custom-controller to operate OpenStack SecurityGroup

CRD/カスタムコントローラー周りの知識をつけたいと思ってちょこちょこ試していたのだけど、ようやく動くものを作ることができた。ものとしては takaishi/openstack-sg-controller で、kubebuilderを使って作った。これは何かというと、OpenStack上でKubernetesを動かしている時に使うもので、カスタムリソースを作るとそれに基づいてセキュリティグループを作成、ノードにアタッチしてくれる。

動作例

CRDを定義済み、Podが動いている状態で以下のようなカスタムリソースを作成する。

apiVersion: openstack.repl.info/v1beta1
kind: SecurityGroup
metadata:
labels:
controller-tools.k8s.io: "1.0"
name: securitygroup-sample
spec:
nodeSelector:
role: master
name: sg-test
rules:
- portRangeMax: "8888"
portRangeMin: "8888"
remoteIpPrefix: "127.0.0.1/32"

これをapplyすると、openstack-sg-controllerによってSGの作成とアタッチが行われる(ログのメッセージ部分のみを切り出し)。

 "msg":"Debug: deletion timestamp is zero"}
"msg":"Creating SG","name":"sg-test"}
"msg":"Success creating SG","name":"sg-test","id":"13214109-2807-490a-80d2-1b9909d9d7b3"}
"msg":"Creating SG Rule","cidr":"127.0.0.1/32","port":"8888-8888"}
"msg":"Success to create SG Rule","cidr":"127.0.0.1/32","port":"8888-8888"}
"msg":"Deleting SG Rule","cidr":"","port":"0-0"}
"msg":"Success to delete SG Rule","cidr":"","port":"0-0"}
"msg":"Deleting SG Rule","cidr":"","port":"0-0"}
"msg":"Success to delete SG Rule","cidr":"","port":"0-0"}
"msg":"Info","Attach SG to Server":"0cf05471-90b1-4390-aa5d-7a36b73a0efa"}
"msg":"Info","Attach SG to Server":"eff5ef40-7f27-4402-9f5e-48c714eeb18c"}
"msg":"Info","Attach SG to Server":"02ddd567-b8c7-4f75-abef-8ad84117c9d0"}
"msg":"Debug: deletion timestamp is zero"}

コマンド実行結果は省略するが、OpenStack側を見ると、インスタンスにSGがアタッチされていることがわかる。

さて、コントローラーなのでカスタムリソースの状態が変わったらそれに追随してほしい。現時点ではrulesの変更とnodeSelectorの変更に対応している。試しにnodeSelectorを変更してみる。foo: barという条件を追加した。

apiVersion: openstack.repl.info/v1beta1
kind: SecurityGroup
metadata:
labels:
controller-tools.k8s.io: "1.0"
name: securitygroup-sample
spec:
nodeSelector:
role: master
foo: bar
name: sg-test
rules:
- portRangeMax: "8888"
portRangeMin: "8888"
remoteIpPrefix: "127.0.0.1/32"

この条件にマッチするノードは1台のみなので、SGはそのノードにだけついてほしい。

$ k get node
NAME STATUS ROLES AGE VERSION
nks-dev-master-001 Ready master 13d v1.11.6
nks-dev-master-002 Ready master 13d v1.11.6
nks-dev-master-003 Ready master 13d v1.11.6
$ k get node -l "foo=bar"
NAME STATUS ROLES AGE VERSION
nks-dev-master-001 Ready master 13d v1.11.6

applyすると、nodeSelectorにマッチしないノードからはSGがデタッチされる。

 "msg":"Debug: deletion timestamp is zero"}
"msg":"Info","Dettach SG from Server":"eff5ef40-7f27-4402-9f5e-48c714eeb18c"}
"msg":"Info","Dettach SG from Server":"02ddd567-b8c7-4f75-abef-8ad84117c9d0"}
"msg":"Debug: deletion timestamp is zero"}

さらに、何かの拍子でノードからSGが外れた場合には再度アタッチしてほしい。OpenStackのAPIをポーリングする必要があるのだが、 「Kubebuilder で Operator を作ってプライベートクラウドの操作を自動化してみる」を参考にして定期実行するようにした。

最後に、カスタムリソースを削除する。SGがデタッチされた後、削除される。

 "msg":"Info","deleting the external dependencies":"sg-test"}
"msg":"Info","Dettach SG from Server: ":"0cf05471-90b1-4390-aa5d-7a36b73a0efa"}
"msg":"Debug: instance not found","SecurityGroup":""}

処理の流れ

まず、カスタムリソースオブジェクトの作成や更新、削除のイベントが発生するとReconcileメソッドが呼び出される。Reconcileメソッドの先頭ではオブジェクトを取得し、存在しなければ削除されたとして終了する。

次に、DeletionTimestampをみて、削除リクエストを受けたオブジェクトかどうかを確認する。DeletionTimestampが設定されている場合、Kubernetesの外のリソースを削除する。具体的には、ノードからSGをデタッチし、SGを削除している。DeletionTimestampが設定されていない場合はFinalizerを設定しておく。

ここまで通過したら、作成時や更新時の処理が始まる。まずはカスタムリソースで指定した名前のSGが既に存在するかをチェックする。存在しない場合はSGを作成する。SGのルールについても、カスタムリソース側と比較してカスタムリソースに書いたようなルールになるように作成・削除を行う。

SGの作成・更新が終わったらノードへのアタッチを行う。OpenStack上でKubernetesを動かすと、NodeInfo.SystemUUIDがOpenStack上におけるインスタンスのIDとなることがわかったので、このIDを使ってノードのSGをチェックし、SGがアタッチされていない場合はアタッチする。

アタッチ先のノードの絞り込みにはNodeSelectorとして、ラベルで指定できるようにした。条件を厳しくしてカスタムリソースを更新した場合、例えばアタッチ先ノードがnode-01、node-02、node-03からnode-01のみに変わる、ということが考えられる。この時、既にSGがアタッチされているノードをチェックする必要がある。これを都度調べるのは困難そうなので、カスタムリソースのStatusにSGをアタッチしたノードのIDを保存することにした。これにより、以前アタッチしたノードがわかるので、条件にマッチしないノードからデタッチすることが可能となる。

所感

作り込みが大変だが、うまく使えばかなり便利だなあ。テストをどう書くかが悩みどころ。後、今の所SGの名前で有無を判別しており、事故がおこりかねないのでここをIDに変更したい。