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

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

init は成功したのにノードが NotReady だ

TT Labで続きを見る

目標

何もない状態のUbuntu VMでkubeadmを使ってコントロールプレーンを1台構築し、NotReadyの原因がCNIであることを証拠で確認した後、Flannelを導入してDNSまで復旧させます。その過程で、静的Pod・テイント・証明書の有効期間・join用トークンがそれぞれどんな役割なのかを、手を動かして確かめます。

なぜ重要なのか

k3sとk0sは、ランタイム・CNI・データストアをディストリビューションが代わりに選んでくれます。kubeadmはそれらの判断をすべて人に残す標準の組み立てキットなので、initが成功したという言葉は「クラスターが使える」という意味ではありません。ネットワークアドオンを選び、そのアドオンが要求するカーネル設定を整え、CIDR範囲が周囲のネットワークと重ならないようにすることは、すべて運用者の仕事です。特に、ノードがReadyになったら終わりではないという点を、このラボで自分で体験します。CNIの設定ファイルができるだけでReadyにはなりますが、デーモンが止まっているとCoreDNSは起動しません。証明書が1年で期限切れになることと、join用トークンのCAハッシュは、インストール当日には何の問題も起こさず、数か月後に事故になる部分なので、あらかじめ読み方を身につけます。

ステップ

  1. net.ipv4.ip_forward = 1を/etc/sysctl.d/k8s.confに書いて適用し、containerd config defaultで/etc/containerd/config.tomlを作成してから、runcのSystemdCgroupをtrueに変更してcontainerdを再起動してください。そのあと次のフィールドを書いてください(書き込み先: /root/kubeadm/runtime.json)。containerd_version(containerd --versionの3番目の列)、systemd_cgroup(実行中のcontainerdが報告する値、ブール値)、ip_forward(現在のカーネルの値、数値)、swap_total_kb(/proc/meminfoのSwapTotal、数値)です。
  2. kubeadm initを--kubernetes-version v1.36.4 --pod-network-cidr 172.20.0.0/16 --service-cidr 172.21.0.0/16で実行し、出力の全文を保存してください(保存先: /root/kubeadm/init.log)。/etc/kubernetes/admin.confを/root/.kube/configにコピーして、kubectlが新しいクラスターを見るようにします。
  3. CNIをインストールする前に現在の状態を記録してください(記録先: /root/kubeadm/notready.json)。フィールドは次のとおりです。node_uid、ready_status(Ready条件のstatus)、ready_message(Ready条件のmessage)、taints(ノードのテイントを키:효과の形式の文字列で入れた配列。プレースホルダーはキーと効果です)、coredns_phase(CoreDNSのPod1つのphase)、cni_conf_count(/etc/cni/net.dにあるドットで始まらないファイルの数)、observed_at(記録したUTC時刻、2026-01-01T00:00:00Z形式)です。
  4. Flannel v0.28.9リリースのkube-flannel.ymlをダウンロードして保存し(保存先: /root/kubeadm/kube-flannel.yml)、net-conf.jsonのNetworkをこのクラスターのPodのCIDR範囲に変更して適用してください(br_netfilterはまだ読み込みません)。30秒ほど様子を見たら、次のフィールドを書いてください(書き込み先: /root/kubeadm/cni-symptom.json)。node_ready(Ready条件のstatus)、coredns_ready_replicas(corednsのDeploymentのreadyReplicas、なければ0)、flannel_pod_uid、flannel_restarts(kube-flannelコンテナのrestartCount)、flannel_error(flannelのログから原因を示す1行)です。
  5. br_netfilterモジュールを/etc/modules-load.d/k8s.confに書いて起動時に読み込まれるようにし、今も読み込んでください。net.bridge.bridge-nf-call-iptables = 1を/etc/sysctl.d/k8s.confに追加して適用します。Flannel DaemonSetの準備ができ、CoreDNSの2つがAvailableになり、ノードでdig @<kube-dns 서비스 IP> kubernetes.default.svc.cluster.localを実行するとkubernetes ServiceのIPが返る必要があります(プレースホルダーはkube-dns ServiceのIPです)。
  6. 次のフィールドを書いてください(書き込み先: /root/kubeadm/static-pods.json)。manifests(/etc/kubernetes/manifestsのファイル名をソートした配列)、owner_kind(kube-scheduler PodのownerReferencesの種類)、etcd_data_dir(etcdのマニフェストがhostPathでマウントしたデータパス)、service_cluster_ip_range(kube-apiserverのマニフェストにある同名のフラグの値)です。そのうえで、kube-scheduler Podをkubectl deleteで削除し、戻ってきたことを確認して、deleted_uid、new_uid、container_id_before、container_id_after(containerStatuses[0].containerID)を同じファイルに追加します。
  7. defaultネームスペースにPodprobe(イメージはregistry.k8s.io/pause:3.10.2、restartPolicyはNever)を作成してPendingであることを確認し、次のフィールドを書いてください(書き込み先: /root/kubeadm/pending.json)。pod_uid、phase、message(PodScheduled条件のmessage)、observed_at(UTC、2026-01-01T00:00:00Z形式)です。そのあと、ノードからnode-role.kubernetes.io/control-plane:NoScheduleテイントを外して、probeがRunningになるようにします。Podは削除して作り直さないでください。
  8. kubeadm certs check-expirationとopensslで確認し、次のフィールドを書いてください(書き込み先: /root/kubeadm/certs.json)。apiserver_not_after、ca_not_after(どちらもUTCで2026-01-01T00:00:00Z形式)、apiserver_valid_days、ca_valid_days(各証明書のnotBeforeからnotAfterまでの日数、整数)です。そしてkubeadm token create --ttl 3h --description worker-joinで新しいトークンを作成し、CA公開鍵のハッシュを自分で計算して、1行で書いてください(書き込み先: /root/kubeadm/join.txt)。書く内容はkubeadm join <API 엔드포인트> --token <토큰> --discovery-token-ca-cert-hash sha256:<해시>です(プレースホルダーはAPIエンドポイント、トークン、ハッシュです)。
  9. 次のフィールドを書いてください(書き込み先: /root/kubeadm/report.json)。kubernetes_version(サーバーのgitVersion)、pod_cidr、service_cidr、dns_service_ip(kube-dns ServiceのIP)、cgroup_driver(kubeletが使っているドライバー)、notready_cause(ステップ3のNotReadyの原因。cni、runtime、certificateのいずれか)、flannel_blocker(ステップ4でflannelを止めたカーネルモジュールの名前)、join_token_id(ステップ8のトークンの先頭6桁)です。

参考

initの前にランタイムを整える

net.ipv4.ip_forward = 1を/etc/sysctl.d/k8s.confに書いて適用し、containerd config defaultで/etc/containerd/config.tomlを作成してから、runcのSystemdCgroupをtrueに変更してcontainerdを再起動してください。そのあと次のフィールドを書いてください(書き込み先: /root/kubeadm/runtime.json)。containerd_version(containerd --versionの3番目の列)、systemd_cgroup(実行中のcontainerdが報告する値、ブール値)、ip_forward(現在のカーネルの値、数値)、swap_total_kb(/proc/meminfoのSwapTotal、数値)です。

ファイルに書いた内容と、実行中のデーモンが使っている内容は別物です。containerd config dumpは設定ファイルを合成して見せるだけで、再起動していないデーモンが何を使っているかはCRIに尋ねる必要があります(crictl info)。ip_forwardは、sysctl --systemでファイルを読み直させないと現在の値が変わりません。

CIDR範囲を避けてinitする

kubeadm initを--kubernetes-version v1.36.4 --pod-network-cidr 172.20.0.0/16 --service-cidr 172.21.0.0/16で実行し、出力の全文を保存してください(保存先: /root/kubeadm/init.log)。/etc/kubernetes/admin.confを/root/.kube/configにコピーして、kubectlが新しいクラスターを見るようにします。

このVMはホストのKubernetesの中で動いていて、ホストはPodに10.244.0.0/16、Serviceに10.96.0.0/12を使っています。2つのCIDR範囲が重なると、内側のkube-proxyが外側のDNSアドレスを横取りします。--kubernetes-versionを省略すると、kubeadmはインターネットから最新のバージョン番号を探しに行きます。

initは成功したのにNotReadyのノード

CNIをインストールする前に現在の状態を記録してください(記録先: /root/kubeadm/notready.json)。フィールドは次のとおりです。node_uid、ready_status(Ready条件のstatus)、ready_message(Ready条件のmessage)、taints(ノードのテイントを키:효과の形式の文字列で入れた配列。プレースホルダーはキーと効果です)、coredns_phase(CoreDNSのPod1つのphase)、cni_conf_count(/etc/cni/net.dにあるドットで始まらないファイルの数)、observed_at(記録したUTC時刻、2026-01-01T00:00:00Z形式)です。

ノードのReady条件のmessageが、原因をそのまま述べています。CoreDNSがPendingである理由はPodのPodScheduled条件にあるので、その理由がノードに付いているどのテイントなのかを照らし合わせてみてください。採点ツールは、この記録がCNIのインストールより先に書かれたかどうかを、時刻で照合します。

CNIを導入してもノードだけがReadyになる

Flannel v0.28.9リリースのkube-flannel.ymlをダウンロードして保存し(保存先: /root/kubeadm/kube-flannel.yml)、net-conf.jsonのNetworkをこのクラスターのPodのCIDR範囲に変更して適用してください(br_netfilterはまだ読み込みません)。30秒ほど様子を見たら、次のフィールドを書いてください(書き込み先: /root/kubeadm/cni-symptom.json)。node_ready(Ready条件のstatus)、coredns_ready_replicas(corednsのDeploymentのreadyReplicas、なければ0)、flannel_pod_uid、flannel_restarts(kube-flannelコンテナのrestartCount)、flannel_error(flannelのログから原因を示す1行)です。

リリースアセットのアドレスはhttps://github.com/flannel-io/flannel/releases/download/v0.28.9/kube-flannel.ymlです。マニフェストのデフォルトのNetworkは10.244.0.0/16なので、このVMではそのまま使えません。ノードがReadyになったのはCNIの設定ファイルができたという意味にすぎず、デーモンが生きているという意味ではありません。再起動を繰り返しているコンテナのログは、--previousで見られます。

br_netfilterを読み込んでCNIを復旧する

br_netfilterモジュールを/etc/modules-load.d/k8s.confに書いて起動時に読み込まれるようにし、今も読み込んでください。net.bridge.bridge-nf-call-iptables = 1を/etc/sysctl.d/k8s.confに追加して適用します。Flannel DaemonSetの準備ができ、CoreDNSの2つがAvailableになり、ノードでdig @<kube-dns 서비스 IP> kubernetes.default.svc.cluster.localを実行するとkubernetes ServiceのIPが返る必要があります(プレースホルダーはkube-dns ServiceのIPです)。

CrashLoopBackOffは再起動の間隔を少しずつ延ばすので、原因を直した後もしばらく待つことがあります。DaemonSetが管理するPodは、削除してもすぐに作り直されます。モジュールを読み込んだ後にsysctlを読み直さないと、bridgeの項目ができません。

削除しても戻ってくるコントロールプレーンのPod

次のフィールドを書いてください(書き込み先: /root/kubeadm/static-pods.json)。manifests(/etc/kubernetes/manifestsのファイル名をソートした配列)、owner_kind(kube-scheduler PodのownerReferencesの種類)、etcd_data_dir(etcdのマニフェストがhostPathでマウントしたデータパス)、service_cluster_ip_range(kube-apiserverのマニフェストにある同名のフラグの値)です。そのうえで、kube-scheduler Podをkubectl deleteで削除し、戻ってきたことを確認して、deleted_uid、new_uid、container_id_before、container_id_after(containerStatuses[0].containerID)を同じファイルに追加します。

静的Podは、APIサーバーではなくkubeletがディレクトリを見て起動します。APIサーバーに見えているのはkubeletが作ったミラー(mirror)Podなので、削除してもkubeletがミラーだけを再び登録します。コンテナが再起動したかどうかは、containerIDで判断してください。

1台構成のクラスターにPodがスケジュールされない

defaultネームスペースにPodprobe(イメージはregistry.k8s.io/pause:3.10.2、restartPolicyはNever)を作成してPendingであることを確認し、次のフィールドを書いてください(書き込み先: /root/kubeadm/pending.json)。pod_uid、phase、message(PodScheduled条件のmessage)、observed_at(UTC、2026-01-01T00:00:00Z形式)です。そのあと、ノードからnode-role.kubernetes.io/control-plane:NoScheduleテイントを外して、probeがRunningになるようにします。Podは削除して作り直さないでください。

kubeadmは、コントロールプレーンのノードに一般のPodが来ないようにテイントを付けます。ノードが1台だけだと、その判断がそのまま「どこにも行けない」になります。テイントを外す書式は、キー:効果の末尾にマイナス記号を付けるものです。スケジューラーは、テイントが変わるとPendingのPodを再び試します。

1年後と2台目のノードに備える

kubeadm certs check-expirationとopensslで確認し、次のフィールドを書いてください(書き込み先: /root/kubeadm/certs.json)。apiserver_not_after、ca_not_after(どちらもUTCで2026-01-01T00:00:00Z形式)、apiserver_valid_days、ca_valid_days(各証明書のnotBeforeからnotAfterまでの日数、整数)です。そしてkubeadm token create --ttl 3h --description worker-joinで新しいトークンを作成し、CA公開鍵のハッシュを自分で計算して、1行で書いてください(書き込み先: /root/kubeadm/join.txt)。書く内容はkubeadm join <API 엔드포인트> --token <토큰> --discovery-token-ca-cert-hash sha256:<해시>です(プレースホルダーはAPIエンドポイント、トークン、ハッシュです)。

リーフ証明書とCAのデフォルトの有効期間は、kubeadmの設定のcertificateValidityPeriodとcaCertificateValidityPeriodで決まります。ハッシュは、CA証明書の公開鍵をDERで取り出してsha256にかけた値です。エンドポイントは、kube-publicのcluster-info ConfigMapに書かれているserverのアドレスと同じである必要があります。

自分で選んだものを報告する

次のフィールドを書いてください(書き込み先: /root/kubeadm/report.json)。kubernetes_version(サーバーのgitVersion)、pod_cidr、service_cidr、dns_service_ip(kube-dns ServiceのIP)、cgroup_driver(kubeletが使っているドライバー)、notready_cause(ステップ3のNotReadyの原因。cni、runtime、certificateのいずれか)、flannel_blocker(ステップ4でflannelを止めたカーネルモジュールの名前)、join_token_id(ステップ8のトークンの先頭6桁)です。

前のステップで残した記録と、現在のクラスターを根拠にして書きます。採点ツールは、同じ値をクラスターと前のステップの記録からもう一度計算します。kubeletのドライバーは、kubeletのログにある'cgroup driver setting received from the CRI runtime'の行や、/var/lib/kubelet/config.yamlで確認できます。