Kubespray と Terraform でクラスターを構築する
同じプレイブックを再実行することが運用だ
一言でいうと
cluster.ymlは、ホストの準備 → ランタイム → ダウンロード → etcd → kubelet → コントロールプレーン → CNI → アドオンの順で動く15個のplayで、同じインベントリで何度実行し直しても、同じクラスターになるように作られています。
なぜ必要なのか
kubeadmで1台を構築することは、コマンド数行で終わります。難しいのは、その後です。3か月後に誰かがcontainerdの設定を1つ変更する必要があるとき、その人は、最初に何をどの順序で行ったかを知っている必要があり、ノードごとに同じ結果を出す必要があります。シェルスクリプトは、最初の1回はうまく動きますが、2回目にはすでにあるファイルや、すでに起動しているデーモンにぶつかります。kubesprayは、インストールを「現在の状態を見て、足りないものだけを補う」Ansibleのタスクとして書いたので、インストールのコマンドと運用のコマンドが同じです。設定を変えるときも、ノードを追加するときも、同じcluster.ymlを再実行します。
どう動くのか
--list-tasksで展開すると、v2.32.0のcluster.ymlは15個のplayです(実測4秒、何も変更しません)。順序には理由があります。
#1-2 Ansible 판 확인, 인벤토리 검사(boilerplate) — 틀린 인벤토리는 여기서 1초 만에 멈춘다
#4-5 호스트 부트스트랩, 사실(facts) 수집
#6 etcd 준비: preinstall(스왑·sysctl·패키지) → 컨테이너 런타임 → 받기(download)
#8 etcd 설치 — 기본은 컨테이너가 아니라 호스트의 systemd 서비스(etcd_deployment_type: host)
#9 kubelet 설치
#10 컨트롤 플레인 — 안에서 kubeadm init 을 부른다
#11 kubeadm 마무리, 노드 라벨·테인트, CNI(기본 calico)
#14 애드온 — CoreDNS, nodelocaldns, (켰다면) metrics-server 등
#15 클러스터 DNS 가 뜬 뒤 resolv.conf 정리
このコードブロックの韓国語の部分は、#1-2がAnsibleのバージョン確認とインベントリの検査(boilerplate)で、間違ったインベントリはここで1秒で止まること、#4-5がホストのブートストラップとファクト(facts)の収集、#6がetcdの準備でpreinstall(スワップ・sysctl・パッケージ)からコンテナランタイム、ダウンロード(download)の順であること、#8がetcdのインストールで、デフォルトはコンテナではなくホストのsystemdサービスであること、#9がkubeletのインストール、#10がコントロールプレーンで、内部でkubeadm initを呼び出すこと、#11がkubeadmの仕上げ、ノードのラベルとテイント、CNI(デフォルトはcalico)であること、#14がアドオンで、CoreDNS、nodelocaldns、有効にした場合はmetrics-serverなどであること、#15がクラスターDNSが起動した後のresolv.confの整理であることを述べています。
etcdがコントロールプレーンより先に来るのは、APIサーバーがデータストアなしでは起動しないからで、CNIがアドオンより先に来るのは、CoreDNSがPodネットワークなしでは起動しないからです。この順序を知っていれば、失敗した地点を見るだけで、何がすでにあり、何がないかを推測できます。
ログは末尾から読みます。PLAY RECAPの1行が判定です。ok、changed、unreachable、failed、skippedがあります。リポジトリのansible.cfgがprofile_tasksコールバックを有効にしているので、その下にTASKS RECAPが付きますが、見出しの行の累積時間が全体の時間で、下の一覧が時間のかかったタスクの順です。途中のfatal:は判定ではありません。たとえば、最初のインストール時のGet currently-deployed etcd versionは、etcdがまだないので失敗するのが正常で、kubesprayはその結果を...ignoringとして受け流します。
冪等はchanged=0ではありません。Ansibleのモジュールのほとんどは、「望ましい状態と現在の状態を比較して、異なるときだけ変更する」という動作をするので、2回目の実行では、ほとんどがokで終わります。ところが、commandやshellで呼び出すタスクは、比較する方法がないので、実行のたびにchangedを出すか、あえてchanged_whenで抑えるか、どちらかです。そのため、2回目の実行のchangedの一覧は「毎回変わるもの」の一覧であり、それを知っていて初めて、3回目の実行で新しく出たchangedが、自分の変更によるものかどうかを見分けられます。
タグで一部だけを実行できます。kubesprayのドキュメントのタグの表には、containerd、etcd、network、apps、coredns、metrics_serverのような名前があり、--tags containerdを指定すると、そのタグが付いたタスクとalwaysタグ(インベントリの検査・facts)だけが動きます。ドキュメントは、タグは「何をするか100%確信があるときにだけ」使うよう警告しています。ロール間の依存(例: containerdの設定を変えると再起動ハンドラーが呼ばれる)がタグの外にあると、抜け落ちるからです。
現場での姿
このコースを作りながら、medium VM(4 vCPU・4 GiB)で実測した値です。きれいなVMでのcluster.ymlの最初の実行は341秒(changed 132)、同じコマンドの2回目の実行は183秒(changed 27)でした。インストール中のVM全体のメモリ使用量(MemTotal − MemAvailable)は最大1.52GiBで、Ansibleのプロセスがそのうち最大345MBを占めました。4 GiBで十分という意味です。ダウンロードしたものは、github.comのリリース・dl.k8s.io・registry.k8s.io・quay.ioとUbuntuのaptリポジトリで、すべてHTTPS(443)またはHTTP(80)だけで取得しました。
実測中に2回つまずき、どちらも現場でそのまま出会う形です。1つ目、ルートを節約しようと/usr/localを丸ごと別のディスクにバインドしたところ、/usr/local/share/ca-certificatesが隠れて、etcd証明書のステップがDestination directory ... does not existで止まりました。2つ目、プレイブックをHOMEが空の環境(systemdユニット)で実行したところ、kubesprayのkubeモジュールが--kubeconfigなしでkubectlを呼び出してlocalhost:8080に向かい、cluster_rolesのステップが1分間リトライして止まりました。この2つ目の失敗はCNIのインストール前でしたが、その状態でcluster.ymlを再実行したところ、今度は「Wait for new control plane nodes to be Ready」が130秒待って失敗しました。kubeadmがすでに動いたノードでは、CNIなしでReadyを待つタスクが先に来るからです。中途半端に構築されたクラスターは、再実行しても直らないことがあり、その場合は、reset.ymlで削除して最初から構築するほうが速いです(resetは実測78秒)。
次のラボですること
playの順序を先に展開して見てから、cluster.ymlを実行してクラスターを構築します。ログの末尾で結果と時間を読み取り、同じコマンドをもう一度実行して、2回目の実行が変更したタスクを集めます。最後に、containerdの設定値を1つgroup_varsで変更し、--tags containerdでその部分だけを再適用して、設定ファイルとデーモンの両方が変わったかを確認します。