TL;DR
Kubernetes がどのように、人間の作業を自動化しているのかを、実際に Kubernetes がやっている作業を手作業で行なう ことで学びましょう。
このQiita の内容は、CloudNative Days Tokyo 2019 における発表の、「転職したらKubernetesだった件」を書き下ろし、実際にデモが行えるように修正を加えたものになります。
この物語はフィクションであり、登場する団体名・会社名・人名等は架空のもので、実在する団体・会社・人物等とは、一切、関係がありません。
これまでのあらすじ
ある日、某Z社に転職した稲津さんに与えられた仕事は、“Kubernetes の一員”になることだった!?
某Z社は、親会社からの依頼で Kubernetes クラスタを運用しなければならなくなりましたが、 「Kubernetes のアーキテクチャが難しすぎ、運用できない!」 と判断しました。しかし、世の中はクラウドネイティブです。とある団体曰く、 クラウドネイティブイコールKubernetesです。 世の中の流れ的に全てを拒否できないだろうと会社の上層部は判断しました。
「Kubernetes で一番重要なコンポーネントはなんだ?」
「kube-apiserverです。」
と、こんな会話があったとかなかったとか。
こうして、某Z社では、kube-apiserverのみが導入され、その他を手作業とする業務フローが生まれたのでした。
それでは、あなたは某Z社に転職した inajob(稲津さん) となり、Z社の社員としてZ社の業務を実際に体験してみましょう!
事前準備
以下のレポジトリを clone して、環境をセットアップしてください。実際に業務体験をするためには Vagrant と VirtualBox が必要です (Mac OS Xで動作確認しています、その他の環境で動かなかった場合の対応は PR お待ちしております )
$ git clone https://github.com/zlabjp/k8s-the-heartful-way
$ cd k8s-the-heartful-way/
$ vagrant up
また、以下のコマンドで、master ノードと、inajob のワーカーノードにログインできることを確認しておいてください。
$ vagrant ssh master01
$ vagrant ssh inajob
第1話 「稲津さん、"Kubernetesネイティブ"企業に転職する」
今日は稲津さんの初出社日です。
稲津__「この会社でKubernetesのスキルを身につけて、CKA,CKADを取得すればこの先しばらく引く手数多だ」__と、この時はまだワクワクしていました。
オリエンテーションを終えた稲津さん、そこにやってきた上司の須田さんから今後業務に使用するラップトップを受け取りました。
須田「稲津さん、あなたの仕事はそのラップトップを使って Kubernetesクラスタの一員になる ことです。そのラップトップでワーカノードとして必要なセットアップを実施してください。」
稲津さんは、何を言われたのかよくわかりませんでしたが、先輩社員に言われた通りにラップトップをセットアップすることにしました。
ネットワークのセットアップ
まず、稲津さんがワーカノードとして働くためには、ノード上で起動するPod同士が疎通するためのネットワークをセットアップしなければなりません。某Z社のPodネットワークはトラディショナルなhost-gwモデルを採用しており、/16
で切られたPodネットワークをさらに /24
でノードごとに割り当て、それぞれのPod間のアドレス解決はノード上のルーティングテーブルで行うというものでした。
整理しましょう、
- Pod ネットワーク: 10.244.0.0/16
- ノードのネットワーク: 192.168.43.0/24
- inajob さんのノード
- IP アドレス: 192.168.43.111
- ノードに割り当てられたPod Networkのサブネット: 10.244.1.0/24
- yuanying さんのノード
- IP アドレス: 192.168.43.112
- ノードに割り当てられたPod Networkのサブネット: 10.244.2.0/24
こんな環境で、inajob さんのノード上に起動したPod(10.244.1.0/24)から、他のノードのPodに(例えばyuanyingノードに起動したPodは10.244.2.0/24)にネットワーク到達可能でなければなりません。
須田「ということで、とりあえず inajob ノードの Pod ネットワークと yuanying ノードの Pod ネットワークを繋いでみましょう。まずはノードにログインしてください!」
$ vagrant ssh inajob
須田「うまくできましたね!」
須田「ノードにログインすると、yuanying ノードの Pod ネットワークはどのレンジが割り当たっているかについては、以下のコマンドで知ることができますよ。」
$ kubectl get nodes -o custom-columns="Name:.metadata.name, PodCIDR:.spec.podCIDR, Address:.status.addresses[?(@.type=='InternalIP')].address" | grep --color -E "^|yuanying.+$"
須田「192.168.43.112
のマシンに 10.244.2.0/24
という Pod ネットワークが割り当てられていることがわかりますね。それではこのネットワークに対してルーティングを追加しましょう。」
$ sudo ip route add 10.244.2.0/24 via 192.168.43.112
須田「ちゃんと登録されたか確認してください。」
$ ip route | grep --color -E "^|^10\.244\.2\.0.+$"
須田「大丈夫そうですね!引き続き他のメンバーのルーティングも追加する必要がありますが、とりあえず今日のところは、これで大丈夫です。」
ノードの登録
須田「Z社の社員の一覧は、kubectl
で確認することができます。色々な業務をこのコマンドで行いますので覚えていきましょう。」
$ kubectl get node
稲津「あら、まだ僕は社員一覧にいないんですね。」
須田「ということで自分の情報を YAML で書いて登録してください。社員一覧の登録も kubectl
を使います。」
稲津「新人ラベルっていうのがあるんですね。」
須田「最近じゃ node-role.kubernetes.io
系のラベルは非推奨だけどね。」
cat <<EOF | kubectl apply -f -
---
apiVersion: v1
kind: Node
metadata:
name: inajob
labels:
node-role.kubernetes.io/newbie: ""
spec:
podCIDR: 10.244.1.0/24
EOF
須田「毎回、ちゃんと登録できたか確認してください。」
稲津「えーっと、きゅーべしーてぃーえる げっと のーど、っと。」
$ kubectl get nodes -o wide | grep --color -E "^|inajob.+$"
稲津「ちゃんと登録されていました!なんか STATUS NotReady が目立ちますね。」
須田「うちは結構時間に融通きくんだよ。」
稲津「へー、いいですね。」
須田「CONTAINER-RUNTIMEやKERNEL-VERSIONなどの情報も一緒に登録よろしく。これらは現状のステータスを表すのでステータスフィールドに書いてください。」
稲津「これは kubectl じゃなくて curl を使うんですね。」
須田「そうだね、変な仕様だよまったく。」
STATUS フィールドはユーザによる変更を想定していないため、
kubectl
では修正することができません。
通常は、kubelet や kube-controller-manager などのコントローラがこのフィールドを変更します。
今回の場合は、稲津さんが kubelet そのものなので、自身で変更する必要があるわけですね。
須田「まず、ステータスを JSON で書きます。」
STATUS=$(cat <<EOF
{
"status": {
"nodeInfo": {
"kubeletVersion": "v1.15.0",
"osImage": "Human 1.0.new",
"kernelVersion": "4.15.2019-brain",
"containerRuntimeVersion": "docker://18.6.3"
},
"addresses": [
{
"type": "InternalIP",
"address": "192.168.43.111"
}
]
}
}
EOF
)
稲津「書きました」
須田「ノードのステータスはノードリソースのサブリソースなので、サブリソース用のエンドポイントに対してパッチリクエストを投げて更新してください。
稲津「こうですね、えいやっ!」
$ curl -k -X PATCH -H "Content-Type: application/strategic-merge-patch+json" \
--key ~vagrant/secrets/user.key \
--cert ~vagrant/secrets/user.crt \
--data-binary "${STATUS}" "https://192.168.43.101:6443/api/v1/nodes/inajob/status"
須田「user.keyは認証用の鍵なので他の人に教えないでくださいね。それでは、、」
稲津「はいはい、確認ですね。ポチッと。(はー、kubectl楽だわー)」
$ kubectl get nodes -o wide | grep --color -E "^|inajob.+$"
須田「よしよし、できたね。今日はこれでいいかな。」
稲津「これで晴れて私も会社の一員ですかね?」
須田「お疲れ様でした。」
第2話「稲津さん、ワーカノードになる」
今日は稲津さんはじめての業務の日です。稲津さんが会社に出勤してきました。
どんな会社でも最初にやることは出勤登録することです。 “Kubernetes ネイティブ”である某Z社における出勤登録は 担当するノードのステータスを更新することです。
稲津「おはようございます、今日もよろしくお願いします!」
須田「おはよう、それでは早速ですが勤怠登録お願いします。」
稲津「はい、わかりました。勤怠はどこのシステムにログインすれば。。。」
須田「うちは、勤怠もKubernetesで行なっているんだよ。」
稲津(何を言ってるんだこの人は。)
須田「はいはい、早速ノードにログインして。フォーマットはこんな感じのJSONで。」
$ STATUS=$(cat <<EOF
{
"status": {
"conditions": [
{
"lastHeartbeatTime": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")",
"message": "今日からよろしくお願いします。",
"reason": "稲津出社",
"status": "True",
"type": "Ready"
}
]
}
}
EOF
)
稲津「これでいいですか。」
須田「それじゃあ、昨日と同じようにステータスサブリソースにパッチを投げてください。」
$ curl -k -X PATCH -H "Content-Type: application/strategic-merge-patch+json" \
--key ~vagrant/secrets/user.key \
--cert ~vagrant/secrets/user.crt \
--data-binary "${STATUS}" "https://192.168.43.101:6443/api/v1/nodes/inajob/status"
稲津「できました、こんな感じですか?」
須田「それj」
稲津「確認ですね。」
$ kubectl get node | grep --color -E "^|inajob.+$"
稲津「お、ステータスがREADYになりました。」
須田「上司の俺は、これで出勤を確認するから。これからじゃんじゃん仕事を振るからよろしくね。」
稲津「ウゲー(はい、よろしくお願いします!)」
Kubernetes ノードはコンテナを実行できる準備ができている、ということをクラスタに対してそれを定期的に伝えています。稲津さんも出社して準備ができたので、それをクラスタに伝えます。Kubernetes はその報告をもとに仕事を各ノードに振り分けます。
また、ノードに障害が発生して、ノードからの定期的な連絡がなくなると、Kubernetes はそのノードで問題が発生したと判断して、そのノード上で実行しているコンテナを他のノード上で代わりに実行するようにします。このようにすることで、Kubernetes 上にデプロイされるサービスは高い可用性を得ることができます。
ちなみに、最近の Kubernetes では Lease オブジェクトを利用して Node のハートビートを行っているため、微妙にこの説明は古くなっています。
第3話「稲津さん、Pod を実行する」
スケジューラ担当 須田
須田「さて、俺もそろそろ自分の仕事をするかな。」
稲津「須田さんは何の仕事をしているんですか?」
須田「俺か?俺は色々な仕事を任せられているが、今日はスケジューラーだ。」
稲津「す、スケジューラー?」
須田「要は調整係だな、この会社で実行しなくちゃいけないプログラム、俺たちはPodと呼んでいるが、それを誰に実行してもらうか決める役割だ。」
稲津「へー、(簡単そうですね。)」
須田「そうだ、お前、俺の仕事を横から見ていろ。(俺もいつまでこの会社にいるかわからないからな。)後任を育てる義務がある。」
稲津「あっ。」
須田「まずは管理者ノードに入るぞ。これ間違えたらできないからな。」
$ vagrant ssh master01
須田「そして、まだノードに割り当てられていないプログラム(Pod)があるかどうかを確認する。」
$ kubectl get pods -o custom-columns=Name:.metadata.name,Node:.spec.nodeName
稲津「お、nginxという名前のPodがありますね。」
須田「そうだ、ここのNodeが <none>
になっているだろう、これはまだ、この nginx がどのノードにも割り当てられていないってことだ。」
須田「次に、出勤している社員を調べる。」
$ kubectl get node
須田「この中でReadyになっていて実行できそうな社員にこのPodの実行を任せることにするんだ。」
$ kubectl describe node inajob | head -n 14
須田「適当にノードを選んで、describe などを駆使して大丈夫かどうかを調べたりする。非常に高度な仕事だ。」
$ cat <<EOL | tee nginx-binding.yaml
apiVersion: v1
kind: Binding
metadata:
name: nginx
target:
apiVersion: v1
kind: Node
name: inajob
EOL
須田「どのPodをどのノード(社員)に割り当てるのかを決める Binding リソースをYAMLで作成する。」
$ curl -k -X POST -H "Content-Type: application/yaml" \
--data-binary @nginx-binding.yaml \
--key /vagrant/kubernetes/secrets/admin.key \
--cert /vagrant/kubernetes/secrets/admin.crt \
"https://192.168.43.101:6443/api/v1/namespaces/default/pods/nginx/binding"
須田「そしてこのYAMLをapiserverにポスト、、、これで先ほどのBindingリソースが、nginx Pod の binding サブリソースとして登録された。」
稲津(わけわからん、これは私に割り当てられたのか…?)
ここでは、須田さんは Kubernetes でいう kube-scheduler の役割を演じていました。kube-scheduler は Node に割り当てられていない Pod を逐一監視し、ノード一覧からそのPodを実行するのに最も適したノードを選択し、Podをノードに割り当てます。
割り当て自体は Pod の binding サブリソースとして表現されいます。
稲津さん、仕事を割り当てられていることを知る
稲津「えーっと、それでは私の日々のルーチンは何をすればいいのでしょうか。」
須田「基本的に稲津くんの仕事はPodを実行することなので、自分にPodが割り当てられているかを確認してください。kubectl で。とりあえず自分のPCにログインしてください。」
$ vagrant ssh inajob
稲津(マニュアルによるとこのコマンドか…。)
$ kubectl get pod \
--field-selector 'status.phase=Pending,spec.nodeName=inajob' -A
稲津「あれ、さっきの nginx Pod が表示されましたよ。」
須田「そのコマンドは自分のノードに割り当てられていて、まだ実行されていないPodが表示されるんだ。」
稲津「それでどうすれば、、」
須田「マニュアル嫁」
Podのネットワークを設定しよう
pause コンテナを起動せよ
稲津「pauseコンテナを起動しろ、とありますね。pauseコンテナって何ですか?」
須田「ああ、そもそもPodとはね、」
稲津(そもそも論が始まった、これは長そうだ。)
稲津「あ、やっぱいいです。」
須田「まあ、簡単にいうとコンテナ同士のネームスペースを共有するため用のコンテナだ。まあ、今回は nginx コンテナ一つを起動するだけなので必要ないんだけどね。」
稲津(必要ないならやらなければいいのに…。)
Pod はコンテナの集合です。某Z社ではコンテナランタイムにDockerを使っているわけなのですが、Dockerには「コンテナの集合」という概念がないため、その機能差を埋めるために 「pause コンテナ」というグルーを利用しています。
The Almighty Pause Container - Ian Lewis
$ docker run -d \
--network none \
--name k8s_POD_default-nginx \
k8s.gcr.io/pause:3.1
稲津「起動しました!」
Pod に IP アドレスを割り当てよう
須田「それじゃあ、その pause コンテナに Pod のアドレスを割り当てようか。この作業は root で行う必要がある。」
稲津「すーどぅーですね。」
$ sudo su
須田「そしてさっき作った pause コンテナのネットワークネームスペースをメモっといてくれ。」
$ PID=$(docker inspect -f '{{ .State.Pid }}' k8s_POD_default-nginx)
$ NETNS=/proc/${PID}/ns/net
稲津「メモりました。」
須田「そしてCNIコマンドを実行する。CNIコマンドは環境変数と設定ファイルを標準入力を実行時のパラメータとして取るんだ。」
稲津「えーっと、環境変数を設定、っと。。何かおまじない感ありますね。」
export CNI_PATH=/opt/cni/bin
export CNI_COMMAND=ADD
export CNI_CONTAINERID=k8s_POD_default-nginx
export CNI_NETNS=${NETNS}
export CNI_IFNAME=eth0
export PATH=$CNI_PATH:$PATH
export POD_SUBNET=$(kubectl get node inajob -o jsonpath="{.spec.podCIDR}")
須田「環境変数を設定したら CNI の bridge コマンドを実行してくれ。」
/opt/cni/bin/bridge <<EOF
{
"cniVersion": "0.3.1",
"name": "bridge",
"type": "bridge",
"bridge": "cni0",
"isGateway": true,
"ipMasq": true,
"ipam": {
"type": "host-local",
"ranges": [
[{"subnet": "${POD_SUBNET}"}]
],
"routes": [{"dst": "0.0.0.0/0"}]
}
}
EOF
稲津「な、何かずらずらと出てきました!」
須田「.ips[0].address
の値が割り当てられたPodのIPだ、忘れると大変だからこれもメモしておいてくれ。あ、以降の作業はまた一般ユーザで行うから exit
してからな。」
$ exit # root 作業終了
$ POD_IP=10.244.1.2
稲津「メモりました!」
POD_IP
の値は環境ごとに違うので、実際の値をメモっておいてください。
須田「ちゃんと設定されたか確認しておこう。」
稲津「えーっと、この場合の確認ってどうすればいいんですかね。」
須田「ping
で良いんじゃね。」
$ ping ${POD_IP}
稲津「応答返ってきました!」
nginx コンテナを起動しよう
須田「それでは実際の nginx コンテナを起動しようか。」
稲津「えーっと、どんな spec かをちゃんと確認してっと。。。」
$ kubectl get pod nginx -o json | jq '.spec.containers'
稲津「nginx:1.14
のイメージを起動すれば良いんですね。そういえば他のフィールドは…。」
須田「。。。」
稲津(時間と尺の都合で割愛かな。)
須田「ネットワークにさっきのコンテナを指定すれば、ネットワークを共有することができるんだ。」
稲津(あ、無視された。)
$ docker run -d \
--network container:k8s_POD_default-nginx \
--name k8s_nginx_nginx_default \
nginx:1.14
稲津「起動しました!」
須田「実際に nginx が起動したか確認してみようか。」
$ curl http://${POD_IP}
稲津「nginx のデフォルトページが返ってきました!」
Pod のステータスを更新しよう
須田「それじゃあ、ちゃんとPodが実行できたことを俺に報告してくれ。」
稲津「なになに、Podのステータスを更新する、とありますね。この会社の報告は大体ステータスの変更で行うんですね。」
須田「Kubernetesネイティブ企業だからな。」
STATUS=$(cat <<EOF
{
"status": {
"conditions": [
{
"lastProbeTime": null,
"lastTransitionTime": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")",
"status": "True",
"type": "Initialized"
},
{
"lastProbeTime": null,
"lastTransitionTime": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")",
"status": "True",
"type": "Ready"
},
{
"lastProbeTime": null,
"lastTransitionTime": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")",
"status": "True",
"type": "ContainersReady"
}
],
"containerStatuses": [
{
"containerID": "human://nginx-0001",
"image": "nginx:1.14",
"imageID": "docker-pullable://nginx@sha256:96fb261b66270b900ea5a2c17a26abbfabe95506e73c3a3c65869a6dbe83223a",
"lastState": {},
"name": "nginx",
"ready": true,
"restartCount": 0,
"state": {
"running": {
"startedAt": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")"
}
}
}
],
"hostIP": "192.168.43.111",
"phase": "Running",
"podIP": "${POD_IP}",
"startTime": "$(date --utc +"%Y-%m-%dT%H:%M:%SZ")"
}
}
EOF
)
稲津「ステータスのJSON書きました。何か妙に長いですね。」
須田「本当はもっと細かく状態が変更されるたびに報告してもらうんだが、新人だから簡略化しました。」
稲津(えらくマイクロマネジメントだな。)
須田「それでは apiserver に登録してください。」
$ curl -k -X PATCH -H "Content-Type: application/strategic-merge-patch+json" \
--key ~vagrant/secrets/user.key \
--cert ~vagrant/secrets/user.crt \
--data-binary "${STATUS}" "https://192.168.43.101:6443/api/v1/namespaces/default/pods/nginx/status"
稲津「登録しました。」
$ kubectl get pods -o wide | grep --color -E "^|Running"
須田「PendingからRunningになりましたね、ちゃんと登録されているようです。」
第4話「吉田さん、ReplicaSet を処理する」
ここで須田さん、稲津さんの上司である吉田さんが初登場します。吉田さんは、コントロールプレーンとしてコントローラマネージャという管理業務を行なっています。
管理業務の1つは Pod の冗長化です。一般にアプリケーションは、複数のインスタンスで構成して可用性を担保します。Kubernetes ではこれを ReplicaSet で実現します。
今日は吉田さんが ReplicaSet コントローラとして活躍するお話です。
吉田さん、処理されていないReplicaSetを見つける
吉田(ゴソゴソ)
$ vagrant ssh master01
稲津(おや、あれは確か上司の吉田さんだ。)
稲津「吉田さん、何をしているんですか。」
吉田「何って、仕事だよ仕事。色々監視してるんだ。」
稲津「何かこの会社って、基本監視で仕事が回ってますよね。」
吉田「そうだな、自分の仕事内容については自分でapierverを監視して自主的に処理するんだ。」
リコンシレーションループと呼ばれる、対象オブジェクトの監視と処理は、Kubernetesの第一原則となっています。
吉田「親会社の人間は、俺たちに何かを処理させたくなると大体、ReplicaSet か Deployment と呼ばれる書式で指示して来ることが多いんだ。」
$ kubectl get replicasets -o wide -A | grep --color -E "^|DESIRED|CURRENT"
吉田「ほら、web
という名前のアプリを二つ処理してくれって来たぞ。」
稲津「DESIREDが2つ、になっていますね。CURRENTがゼロですが。」
吉田「そりゃまだ俺たちが処理していないからな。」
吉田「まあ、一応、本当に処理していないか調べる。このReplicaSetの指示書によると、app=web
というラベルがついたPodが二ついないとダメらしい。」
$ kubectl get pod -l app=web
稲津「ありませんね。」
吉田「よし、それじゃあ Pod を二つ作るか。どんなPodを作るかは ReplicaSet の指示書に書いてある。」
$ kubectl get rs web -o json | jq '.spec.template'
稲津「この spec フィールドのテンプレートのところですね。」
吉田「そうだ、これを使ってチョチョイと kubectl create
っと…。」
$ kubectl get rs web -o json | \
jq -r '.spec.template | .+{"apiVersion": "v1", "kind": "Pod"} | .metadata |= .+ {"name": "web-001"}' | \
kubectl create -f -
$ kubectl get rs web -o json | \
jq -r '.spec.template | .+{"apiVersion": "v1", "kind": "Pod"} | .metadata |= .+ {"name": "web-002"}' | \
kubectl create -f -
稲津「jq がキモいですね。」
吉田「これで Pod が二つ作られたはずだ。」
$ kubectl get pod -l app=web
稲津「どっちもPendingのままですね、担当者に割り当てもされていないみたいです。」
吉田「おう、俺の仕事はReplicaSetからPodの指示書を作るだけだからな、あとはスケジューラの須田とkubelet担当の誰かがやってくれるさ。」
稲津(この人の業務楽そうだな、、早く偉くなりたい。)
吉田「まだ終わりじゃないぞ、一応親会社の誰かさんに処理が進んだことを報告しないといけないからな。」
稲津「ステータスを更新するんですね。」
吉田「そうだ、よくわかっているじゃないか。須田もちゃんと教育しているようだな。」
$ STATUS=$(cat <<EOF
{
"status": {
"availableReplicas": 0,
"fullyLabeledReplicas": 0,
"observedGeneration": 1,
"readyReplicas": 0,
"replicas": 2
}
}
EOF
)
吉田「とりあえず、まだ何も動かないPodを二つ作ったので replicas
に 2 を指定してっと。」
$ curl -k -X PATCH -H "Content-Type: application/strategic-merge-patch+json" \
--key /vagrant/kubernetes/secrets/admin.key \
--cert /vagrant/kubernetes/secrets/admin.crt \
--data-binary "${STATUS}" "https://192.168.43.101:6443/apis/apps/v1/namespaces/default/replicasets/web/status"
吉田「ReplicaSetのステータスサブリソースを更新しておく。」
$ kubectl get replicasets -o wide | grep --color -E "^|DESIRED|CURRENT"
稲津「READYがまだゼロだけど、CURRENTが2になりました。」
吉田「うむ、これで俺の仕事は終わり。基本は自分の担当オブジェクトを kubectl watch
してればいい。」
須田さんによる、いつものスケジューリング業務
$ vagrant ssh master01
須田「む、Podが更新されたな、俺の仕事の出番か。」
稲津「お疲れ様です、どうしたんですか?」
$ kubectl get pods -o custom-columns=Name:.metadata.name,Node:.spec.nodeName
須田「見ろ、ノードが割り当てられてないPodが二つ増えた。スケジューラとしての俺の出番だ。」
稲津「誰かに仕事を割り振るんですね。(自分に割り当てませんように)」
須田「そうだな、最初の1個目はリモート勤務の大塚(yuanying)に任せるか。」
稲津「この会社はリモート勤務もOKなんですね。」
須田「最近の流行りだな。いつも通りに指示書を作るぞ。」
$ cat <<EOL | tee web-yuanying-binding.yaml
apiVersion: v1
kind: Binding
metadata:
name: web-001
target:
apiVersion: v1
kind: Node
name: yuanying
EOL
須田「そしてapiserverに登録する。」
$ curl -k -X POST -H "Content-Type: application/yaml" \
--data-binary @web-yuanying-binding.yaml \
--key /vagrant/kubernetes/secrets/admin.key \
--cert /vagrant/kubernetes/secrets/admin.crt \
"https://192.168.43.101:6443/api/v1/namespaces/default/pods/web-001/binding"
須田「リモートでもちゃんと仕事してるか確認するぞ。」
$ kubectl get pod -o wide
稲津「わ、もう Running
になってますよ!仕事早いですねー。」
須田「大塚はワーカーノード歴が長いからな、このくらいやってもらわないと困る。」
須田「まあ、念の為ちゃんと起動してるか確認するか。」
$ curl http://$(kubectl get pod web-001 -o jsonpath='{.status.podIP}'):8080
稲津「ちゃんと動いてますね。」
須田「それじゃあもう一個は稲津、お前に振るぞ。」
$ cat <<EOL | tee web-inajob-binding.yaml
apiVersion: v1
kind: Binding
metadata:
name: web-002
target:
apiVersion: v1
kind: Node
name: inajob
EOL
$ curl -k -X POST -H "Content-Type: application/yaml" \
--data-binary @web-inajob-binding.yaml \
--key /vagrant/kubernetes/secrets/admin.key \
--cert /vagrant/kubernetes/secrets/admin.crt \
"https://192.168.43.101:6443/api/v1/namespaces/default/pods/web-002/binding"
稲津(他の人に振ってくれてもいいのに)
稲津さん、またまたPodを起動する
稲津「えーっと、ノードにログインしてっと。」
$ vagrant ssh inajob
稲津「さて、何の仕事が私に割り当たっていたんだっけかな。確認しよう。」
$ kubectl get pod \
--field-selector 'status.phase=Pending,spec.nodeName=inajob' -A
稲津「あったあった。人使い荒いなあ、この会社は。」
稲津「といっても優秀な稲津くんなので、すでにこの作業はスクリプトにしているのです。」
須田(何をブツブツ言ってるんだ。)
$ sudo bash /vagrant/scripts/create-pod.sh web-002
稲津(できたできた、もう少し仕事してるフリしてから報告しよう。)
$ kubectl get pod -A
須田「お、稲津くん、キミも仕事が早いじゃないか。」
稲津(しまった、ステータスの更新もスクリプト化してしまっていた。)
吉田さん、ReplicaSet 業務の報告をする
吉田「さてと、そろそろ俺が指示した Pod の作成も終わっている頃かな。親会社への作業完了の報告はコントローラマネージャとしての大事な仕事だからな、ちゃんと監視を怠らないようにしないと。」
$ vagrant ssh master01
吉田「コントロールプレーンノードにログインしてっと、Podを確認するか。」
$ kubectl get pod -l app=web -o wide | \
grep --color -E "^|Running"
吉田「お、ちゃんと二つのPodがReadyになっているな。それではReplicaSetのステータスを更新するか。」
$ STATUS=$(cat <<EOF
{
"status": {
"availableReplicas": 2,
"fullyLabeledReplicas": 2,
"observedGeneration": 1,
"readyReplicas": 2,
"replicas": 2
}
}
EOF
)
吉田「readyReplicas
を2に更新してっと。」
$ curl -k -X PATCH -H "Content-Type: application/strategic-merge-patch+json" \
--key /vagrant/kubernetes/secrets/admin.key \
--cert /vagrant/kubernetes/secrets/admin.crt \
--data-binary "${STATUS}" "https://192.168.43.101:6443/apis/apps/v1/namespaces/default/replicasets/web/status"
吉田「apiserver に登録っと。」
$ kubectl get rs | grep --color -E "^|DESIRED|CURRENT|READY"
吉田「Desired/Current/Ready が一致した。完璧だ。」
稲津(この会社、独り言してる人が多いな。)
Kubernetes コントローラは、API オブジェクトに定義された「望ましい状態」とクラスタの「現在の状態」を一致させるように「継続的に」動作します。これを調整ループ(reconciliation Loop)と呼びます。
第5話「稲津さん、Service を処理する」
ReplicaSet を使って複数の Pod レプリカを作成できましたが、 Pod にアクセスしたいときにそれぞれの IP に直接アクセスするのは不便です。
代表的なアドレスがあっていい感じに ロードバランスして欲しいですよね。
Kubernetes では通常、そのような場合 Service を使います。サービスは ClusterIP と呼ばれる仮想 IP とポートを使って、そこへのアクセスをバックエンドの Pod に分散します。バックエンドの Pod はラベルセレクタで選択されます。"Service" という名前はサービスの入り口となる機能となることから来ています。
この Service のロードバランサとしての機能の実現に責任を持つコンポーネントがkube-proxy
です。
激務な(コントローラー)マネージャー吉田さん、Endpointを処理する
吉田「さてと、そろそろまた新しい指示が来てそうな時間だな、確認するか。」
$ vagrant ssh master01
吉田「うわ、やっぱり。さっきReplicaSetを処理したから来てると思ったんだ。」
稲津「どうしたんですか、吉田さん。」
吉田「お、稲津か。これを見てみろ。」
稲津「どれどれ?」
$ kubectl get service -o wide
稲津「web-service っていう名前の Service がありますね。何ですかこれ?」
吉田「CLUSTER-IP
が 10.254.10.128
で SELECTOR
が app=web
とあるだろ、要するにこれはさっき二つ作ったPodをいい感じにロードバランスしてくれる VIP 10.254.10.128
が欲しい、っていう指示書だ。」
稲津「へ、じゃあ吉田さんはこれからその、ロードバランサの設定をするんですか?
吉田「いや、俺はこの指示をもう少し細かくブレークダウンするだけで、実際の作業は kube-proxy 担当だ。」
稲津「へー、そうなんですね。」
吉田「ひとごとのようだが、kube-proxy はノード担当の仕事だぞ。」
稲津「!?(やっぱりこの人の仕事は楽なんじゃない?!)」
$ kubectl get pod -l app=web -o custom-columns=NAME:.metadata.name,IP:.status.podIP
吉田「まず、やることは、さっきのラベルを持っているPodのIPアドレス一覧を取得する。」
$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Endpoints
metadata:
name: web-service
subsets:
- addresses:
- ip: $(kubectl get pod web-001 -o jsonpath='{.status.podIP}')
- ip: $(kubectl get pod web-002 -o jsonpath='{.status.podIP}')
ports:
- port: 8080
protocol: TCP
EOF
吉田「そしてそのIPアドレスからEndpointsオブジェクトを作ってやる。あとはこのEndpointsオブジェクトを見てノード担当が実際の設定をするんだ。」
稲津さん、kube-proxy の初期設定をする
須田「そういえば稲津、お前のノードにロードバランサの初期設定をするのを忘れていた。」
稲津「なんかめんどくさそうですね。」
須田「そうだな、俺もいまだによくわからん。とりあえず一個ずつ設定していくぞ。ノードにログインして!」
$ vagrant ssh inajob
稲津「はい、ログインしました。」
須田「ついでに sudo が面倒なので root になっておいて。」
$ sudo su
稲津「はい、root になりました!」
さて、稲津くんはこれから kube-proxy の設定を行なっていきます。一つ目は Dummy interface の作成。二つ目は、外部からサービスのリクエストを受けたときのためのiptables のマスカレードの設定及びカーネルパラメータの設定です。これらがないと IPVS のリクエストを外部から受けたとき、パケットがどこかに吸い込まれていってしまいます。
須田「うちは kube-proxy の処理を行うのに、IPVS を使っている。なのでまずは VIP を割り当てる dummy interface を作成する。」
稲津「こうですね!」
$ ip link add kube-ipvs0 type dummy
須田「次に、iptablesの設定をしていくぞ。」
稲津「iptables...」
須田「iptables を触りたくないから IPVS にしたのだが、結局 iptables は必要なのだそうだ。」
$ ipset create KUBE-CLUSTER-IP hash:ip,port
稲津「まずは ipset というのを作るんですね?」
須田「そうだ。これを作ることで iptables のルールを線形探索しなくてすむようになる。」
須田「そして、その ipset を使う iptablesのルールを作っていくぞ。」
iptables -t nat -N KUBE-MARK-MASQ
iptables -t nat -A KUBE-MARK-MASQ -j MARK --set-xmark 0x4000/0x4000
iptables -t nat -N KUBE-POSTROUTING
# kubernetes service traffic requiring SNAT
iptables -t nat -A KUBE-POSTROUTING -m comment --comment "kubernetes service traffic requiring SNAT" -m mark --mark 0x4000/0x4000 -j MASQUERADE
iptables -t nat -N KUBE-SERVICES
# Kubernetes service cluster ip + port for masquerade purpose
iptables -t nat -A KUBE-SERVICES ! -s 10.244.0.0/16 -m comment --comment "Kubernetes service cluster ip + port for masquerade purpose" -m set --match-set KUBE-CLUSTER-IP dst,dst -j KUBE-MARK-MASQ
iptables -t nat -A KUBE-SERVICES -m set --match-set KUBE-CLUSTER-IP dst,dst -j ACCEPT
# kubernetes service portals
iptables -t nat -I PREROUTING -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
iptables -t nat -I OUTPUT -m comment --comment "kubernetes service portals" -j KUBE-SERVICES
# kubernetes postrouting rules
iptables -t nat -I POSTROUTING -m comment --comment "kubernetes postrouting rules" -j KUBE-POSTROUTING
iptables -t filter -N KUBE-FORWARD
# kubernetes forwarding rules
iptables -t filter -A KUBE-FORWARD -m comment --comment "kubernetes forwarding rules" -m mark --mark 0x4000/0x4000 -j ACCEPT
# kubernetes forwarding conntrack pod source rule
iptables -t filter -A KUBE-FORWARD -s 10.244.0.0/16 -m comment --comment "kubernetes forwarding conntrack pod source rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# kubernetes forwarding conntrack pod destination rule
iptables -t filter -A KUBE-FORWARD -d 10.244.0.0/16 -m comment --comment "kubernetes forwarding conntrack pod destination rule" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
# kubernetes forwarding rules
iptables -t filter -I FORWARD -m comment --comment "kubernetes forwarding rules" -j KUBE-FORWARD
稲津「何だかたくさんありますね…。」
須田「これでも昔よりは楽になったんだ。少なくともこれ以上ルールが増えることがないからな。」
Kubernetes の kube-proxy/ipvs パッケージ に、どのような場合に IPVS モードの kube-proxy が iptables にフォールバックするのかが書いてあります。
ちなみに、今回設定した iptables のルールは、ClusterIP に関するルールのみで、type: NodePort
やtype: Loadbalancer
のルールは省略されています。
須田「最後に、カーネルパラメータの設定だ。」
須田「まずは、カーネルモジュールをロードして。」
modprobe -- ip_vs
modprobe -- ip_vs_rr
modprobe -- ip_vs_wrr
modprobe -- ip_vs_sh
modprobe -- nf_conntrack_ipv4
須田「そしてカーネルパラメータを設定する。」
$ echo 1 | sudo tee /proc/sys/net/ipv4/vs/conntrack
稲津「これは一体何をしてるんですか…。」
須田「よく聞く気になったな、えらいえらい。」
須田「詳しくは 「IPVS (LVS/NAT) とiptables DNAT targetの共存について調べた に書いてあるから読むといいぞ。」
稲津「なるほど、IPVS により conntrack の情報が消されてしまってマスカレードができなくなってしまう問題を解決するための設定なんですね!」
須田(なんだ、いきなり賢くなったぞ。)
稲津さん、IPVS の設定をする
須田「それじゃあ、まずは設定するVIPの値を調べよう、kubectl get svc
して。」
$ kubectl get svc
稲津「はい、しました。どうやらVIPのアドレスは 10.254.10.128
のようですね。」
須田「そのアドレスを dummy interface に設定する。」
$ sudo ip addr add 10.254.10.128 dev kube-ipvs0
須田「さらに、さっき作った ipset にも登録する。」
$ sudo ipset add KUBE-CLUSTER-IP 10.254.10.128,tcp:80
須田「そして、さらにさらに、ipvs の Virtual Server そのものを作成する。」
$ sudo ipvsadm -A -t 10.254.10.128:80 -s rr
稲津「何か、、いろんなところに同じ情報が散らばっていて気持ち悪いですね、、自動化したい気分…。」
kube-proxy
はこれらの作業を自動化しています。
須田「何か言ったか?それじゃあ ipvs の Real Server を設定するぞ、endpoint を取得して!」
$ kubectl get ep web-service -o yaml
稲津「.subsets[0].addresses
の値ですね。」
須田「その値で Real Server を二つ作る。」
$ sudo ipvsadm -a -t 10.254.10.128:80 -r $(kubectl get ep web-service -o jsonpath='{.subsets[0].addresses[0].ip}'):8080 -m
$ sudo ipvsadm -a -t 10.254.10.128:80 -r $(kubectl get ep web-service -o jsonpath='{.subsets[0].addresses[1].ip}'):8080 -m
稲津「設定しました!」
須田「それじゃあ確認して!」
稲津(何か投げやりになってきたな、面倒になってきたのか?)
$ sudo ipvsadm -Ln
稲津「設定できているみたいです。」
須田「実際に curl
してみようか。」
$ curl http://10.254.10.128
$ curl http://10.254.10.128
稲津「ちゃんとラウンドロビンされているようです!」
須田「よくやった、、もうお前は立派なくべれっとだ。」
稲津「何かスタッフロールが流れてきました。」
その後、須田さんは別会社へと旅立ち、稲津さんがスケジューラに昇進したとかしないとか。しかし、その後も某Z社はKubernetesネイティブ企業として長く繁栄したとのことです。
まとめ
Kubernetes はコントロールプレーンと呼ばれるマスタコンポーネントとノードコンポーネントで構成されています。今回いくつかの Kubernetes のコンポーネントの代わりに手作業で一通りの Kubernetes での作業を実施することで、そのコンポーネントの責任と何をやっているのかをあきらかにしました。
Kubernetes はブラックボックスでよくわからないものではなく、もちろん人間が実施できる作業です。Kubernetes がなにをやっているかを把握しておくことはなにか問題があったときの解決に役立つでしょう。
それでは、良い Kubernetes ライフを!
キャスト
kubectl kubectl
kube-apiserver kube-apiserver
etcd etcd
kube-scheduler 須田一輝
kube-controller-manager 吉田龍馬
kubelet1 稲津和磨(あなた)
kubelet2 kubelet
kube-proxy1 稲津和磨(あなた)
kube-proxy2 kube-proxy
container-runtime Docker
スライド
https://speakerdeck.com/superbrothers/
動画
https://www.youtube.com/watch?v=_Bve-nkBr2E
GitHub
https://github.com/zlabjp/k8s-the-heartful-way
おわり