TT Lab
はじめる
学ぶ 学習パス コース

Kubernetesディストリビューション — 自分で立てる

kubeadm がわざと空けておくもの

TT Labで続きを見る

一言でいうと

kubeadmは「Kubernetesをインストールしてくれるツール」ではなく、コントロールプレーンをベストプラクティスどおりに組み立ててくれるツールであり、ランタイム・ネットワーク・CIDR範囲・カーネル設定は最後まで運用者の判断として残ります。

なぜ必要なのか

k3sやk0sを先に使った人は、kubeadm initが成功した直後に戸惑います。緑色の成功メッセージが出たのに、ノードはNotReadyで、CoreDNSはPendingです。故障ではありません。kubeadmは、あえてネットワークアドオンを選びません。Creating a cluster with kubeadmのドキュメントが強調して書いている一文が、まさに「CNIベースのPodネットワークをデプロイする必要があり、ネットワークをインストールするまではクラスターDNSが起動しない」です。

このように設計した理由は、kubeadmの位置づけが「他のインストールツールの部品」だからです。ドキュメントは、kubeadmがAnsibleやTerraformのようなプロビジョニングシステムや、より大きなインストールツールのビルディングブロックとして使われることを想定していると述べています。部品がCNI・ストレージ・Ingressを勝手に選んでしまうと、その上に載るツールがそれを取り除き直さなければなりません。そのためkubeadmは、証明書・kubeconfig・静的Podのマニフェスト・ブートストラップトークンのようにどの環境でも同じでなければならないものだけを作り、環境ごとに変わるものは空けておきます。その代わり、人が知っておくべきことが増えます。

どう動くのか

インストールは3つの層に分かれます。

호스트 준비      swap · ip_forward · 컨테이너 런타임(CRI) · cgroup 드라이버
kubeadm init    preflight → 인증서(/etc/kubernetes/pki) → kubeconfig → 정적 파드 매니페스트
                → kubelet 이 매니페스트를 보고 etcd·apiserver·controller-manager·scheduler 를 띄움
                → CoreDNS·kube-proxy 애드온, 부트스트랩 토큰, control-plane 테인트
사람의 몫        CNI 설치(대역을 init 과 맞춰서) · 테인트 정리 · 워커 조인 · 인증서 갱신 계획

ホストの準備。Installing kubeadmによると、kubeletはデフォルトでswapがあると起動を拒否します。パッケージリポジトリはpkgs.k8s.ioにマイナーバージョンごとに別々にあり、ドキュメントはインストール後にapt-mark holdでkubelet・kubeadm・kubectlを通常のアップグレードの対象から外すよう求めています。アップグレードはkubeadmの手順に従う必要があるからです。Container Runtimesのドキュメントは、Linuxカーネルがデフォルトではインターフェース間のIPv4転送を止めること、そしてsystemdがinitのホストでcgroupfsドライバーを使うとcgroupマネージャーが2つになり、リソースが逼迫したときにノードが不安定になることがあると説明しています。containerd 2.xでは、plugins.'io.containerd.cri.v1.runtime'の下にあるruncオプションのSystemdCgroup = trueがそのスイッチです。

ここでバージョンが重要になります。KubeletCgroupDriverFromCRI機能は1.34でstableになり、CRIがRuntimeConfig呼び出しをサポートしていれば、kubeletは自身の設定のcgroupDriverを無視して、ランタイムが知らせる値を使います。containerdは2.0からサポートしています。このラボのVM(containerd 2.3.5、kubelet 1.36.4)で実測すると、kubeletのログにUsing cgroup driver setting received from the CRI runtimeが出力されます。つまり、今はcontainerdの設定が事実上の唯一の真実です。

init。preflightには、処理を止めるものと警告だけを出すものが混在しています。1.36.4で実測すると、ip_forwardが0のときは[ERROR FileContent--proc-sys-net-ipv4-ip_forward]で止まりますが、br_netfilterは確認しません。通過すると、kubeadmは/etc/kubernetes/manifestsにマニフェストを4つ書き込み、kubeletがそのディレクトリを直接読んでコントロールプレーンを起動します。APIサーバーがまだないのにAPIサーバーを起動しなければならないという鶏と卵の問題を、静的Podで解いているのです。APIサーバーに見えるコントロールプレーンのPodは、kubeletが登録したミラー(mirror)Podなので所有者はNodeであり、削除してもkubeletが再び登録します。

CIDR範囲。--pod-network-cidrは、controller-managerがノードごとにpodCIDRを割り当てるようにし、--service-cidrはAPIサーバーの--service-cluster-ip-rangeになります。ドキュメントは、Podネットワークがホストネットワークと重なると問題が起きるので、重ならないCIDR範囲を選び、initとネットワークプラグインのYAMLの両方に同じ値を入れるよう求めています。

現場での姿

このラボのVMでそのまま再現した場面です。Flannel v0.28.9のマニフェストを適用すると、8秒でノードがReadyに変わります。ところがCoreDNSは、しばらくContainerCreatingのままです。理由は順序にあります。Flannel Podの初期化コンテナが/etc/cni/net.d/10-flannel.conflistを先にコピーし、kubeletは設定ファイルができたことだけを見てネットワークの準備ができたと判断します。肝心のflanneld本体は、次のように終了して再起動を繰り返します。

E0914 22:51:43.944688  1 main.go:292] Failed to check br_netfilter:
  stat /proc/sys/net/bridge/bridge-nf-call-iptables: no such file or directory

Flannel READMEにも、Flannelはbr_netfilterがないと起動せず、kubeadmは1.30からそのモジュールを確認しないと書かれています。モジュールを読み込んだ後も、CrashLoopBackOffの再起動間隔のせいで、実測でさらに74秒待ちました。Readyは「設定ファイルがある」ことであって、「データパスが動いている」ことではありません。判定は、CoreDNSが実際に名前を返すかどうかで行う必要があります。

2つ目の場面はCIDR範囲です。このVMはホストのKubernetesの中で動いていて、VMのPodのアドレスはホストのPodのCIDR範囲である10.244.xです。FlannelのマニフェストのデフォルトのNetworkがちょうど10.244.0.0/16なので、マニフェストを直さずに適用すると、内側のPodのCIDR範囲が外側と重なります。そのためこのラボでは172.20.0.0/16と172.21.0.0/16を使い、マニフェストの1行をinitの値に合わせます。

3つ目はcontainerdのドロップインです。筆者のGPUノード(containerd 1.7.27)では、conf.dのドロップイン1つがCRIプラグインの設定を丸ごと上書きし、ランタイム設定がデフォルト値に戻ったことがありました。同じ実験をこのVMのcontainerd 2.3.5でやってみると、ドロップインがフィールド単位でマージされ、SystemdCgroup = trueが残りました。バージョンによってマージのルールが変わることがあるので、ファイルを目で読んで信じるのではなく、containerd config dumpとcrictl infoで確認する習慣が答えです。

実務で本当に大切なこと

証明書は1年間のものです。Certificate Management with kubeadmによると、kubeadmが作ったクライアント証明書は1年後に期限切れになり、デフォルト値はリーフ証明書が8760h、CAが87600hです。このVMでも、kubeadm certs check-expirationがリーフ364日、CA 9年と表示します。kubelet証明書は自動でローテーションされるので、一覧にはありません。1年間一度もアップグレードしなかったクラスターが、ある日すべてのAPI呼び出しを拒否される事故は、ここから起きます。

joinコマンドのハッシュはセキュリティの仕組みです。--discovery-token-ca-cert-hashはCA公開鍵のsha256で、joinするノードが「このAPIサーバーは本当に自分たちのCAで署名されているか」を確認するための値です。トークンはkube-systemのbootstrap-token-<id>というSecretとして保存され、BootstrapSignerがkube-publicのcluster-info ConfigMapにトークンごとのHMAC署名を付けます。ハッシュ検証を無効にするオプションもありますが、ドキュメントは可能なら別の方法を使うよう勧めています。

1台構成のクラスターなら、テイントを覚えておきます。kubeadmはセキュリティ上、コントロールプレーンのノードにnode-role.kubernetes.io/control-plane:NoScheduleを付け、一般のPodを受け入れないようにします。ノードが1つだけだと、すべてのワークロードがPendingになります。

バージョンを固定します。--kubernetes-versionを省略すると、kubeadmはインターネットから最新のバージョン番号を探しに行きます(実測: remote version is much newer: v1.37.0; falling back to: stable-1.36)。エアギャップ環境なら、その1行で止まります。

次のラボですること

containerdの設定から始めて、kubeadm initでコントロールプレーンを構築し、NotReadyを証拠として残してからFlannelを導入します。ノードはReadyなのにDNSが動かない状態を実際に体験し、br_netfilterで復旧させます。最後に、静的Podを削除してみて、テイントを外し、証明書の有効期限とjoinコマンドを自分で計算して、レポートにまとめます。