TT Lab
开始
学习 学习路径 课程

Kubernetes 发行版 — 自己搭

init 成功了,节点却是 NotReady

在 TT Lab 中继续学习

目标

在一台空白的 Ubuntu VM 上用 kubeadm 搭建一台控制平面,用证据确认 NotReady 的原因是 CNI,然后 接入 Flannel 并让 DNS 也恢复工作。在这个过程中,亲手查看静态 Pod、污点、证书有效期、join 令牌各自起什么作用。

为什么重要

k3s 和 k0s 由发行版替你选好运行时、CNI 和存储。kubeadm 是把这些决定全部留给人的标准组装套件, 所以 init 成功并不意味着“集群可以使用”。选择网络附加组件、配好该附加组件所要求的内核配置、 让网段不与周边网络重叠,这些全都是运维人员的工作。尤其是,节点变成 Ready 并不意味着结束, 你会在本实验中亲身经历这一点——只要 CNI 配置文件出现,节点就会变成 Ready,但如果守护进程已经崩溃,CoreDNS 就不会启动。 证书一年到期和 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 的第三列)、systemd_cgroup(正在运行的 containerd 所报告的值,布尔值)、ip_forward(当前内核值,数字)、swap_total_kb(/proc/meminfo 的 SwapTotal,数字)。
  2. 用 --kubernetes-version v1.36.4 --pod-network-cidr 172.20.0.0/16 --service-cidr 172.21.0.0/16 运行 kubeadm init,并把完整输出保存到 /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 Pod 的 phase)、cni_conf_count(/etc/cni/net.d 中不以点开头的文件数)、observed_at(记录时的 UTC 时刻,2026-01-01T00:00:00Z 格式)。
  4. 把 Flannel v0.28.9 release 的 kube-flannel.yml 下载到 /root/kubeadm/kube-flannel.yml,把 net-conf.json 中的 Network 改成本集群的 Pod 网段并应用(此时先不要加载 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 日志中说明原因的一行)。
  5. 把 br_netfilter 模块写入 /etc/modules-load.d/k8s.conf,使其在开机时加载,并且现在也加载它。把 net.bridge.bridge-nf-call-iptables = 1 加入 /etc/sysctl.d/k8s.conf 并应用。Flannel DaemonSet 要就绪,两个 CoreDNS 要变为 Available,并且在节点上 dig @<kube-dns 서비스 IP> kubernetes.default.svc.cluster.local(占位符为 kube-dns Service 的 IP)要能返回 kubernetes 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 清单中同名标志的值)。然后用 kubectl delete 删除 kube-scheduler Pod,确认它又回来了,并把 deleted_uid、new_uid、container_id_before、container_id_after(containerStatuses[0].containerID)追加到同一个文件中。
  7. 在 default 命名空间中创建 Pod probe(镜像 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 公钥哈希,并在 /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 的第三列)、systemd_cgroup(正在运行的 containerd 所报告的值,布尔值)、ip_forward(当前内核值,数字)、swap_total_kb(/proc/meminfo 的 SwapTotal,数字)。

写在文件中的值与正在运行的守护进程所使用的值并不相同。containerd config dump 只是把配置文件合并后显示出来,没有重启的守护进程实际在用什么,必须去问 CRI(crictl info)。ip_forward 必须用 sysctl --system 让文件重新读取,当前值才会改变。

避开网段后再 init

用 --kubernetes-version v1.36.4 --pod-network-cidr 172.20.0.0/16 --service-cidr 172.21.0.0/16 运行 kubeadm init,并把完整输出保存到 /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。如果两个网段重叠,内层的 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 Pod 的 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 release 的 kube-flannel.yml 下载到 /root/kubeadm/kube-flannel.yml,把 net-conf.json 中的 Network 改成本集群的 Pod 网段并应用(此时先不要加载 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 日志中说明原因的一行)。

release 资源地址是 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 要变为 Available,并且在节点上 dig @<kube-dns 서비스 IP> kubernetes.default.svc.cluster.local(占位符为 kube-dns Service 的 IP)要能返回 kubernetes 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 清单中同名标志的值)。然后用 kubectl delete 删除 kube-scheduler Pod,确认它又回来了,并把 deleted_uid、new_uid、container_id_before、container_id_after(containerStatuses[0].containerID)追加到同一个文件中。

静态 Pod 不是由 API 服务器,而是由 kubelet 查看目录后启动的。在 API 服务器中看到的是 kubelet 创建的镜像 Pod(mirror Pod),所以即使删除,kubelet 也只会重新注册这个镜像 Pod。容器是否重启过,请根据 containerID 来判断。

单节点集群上 Pod 调度不上去

在 default 命名空间中创建 Pod probe(镜像 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 过来。如果只有一个节点,这个决定就意味着“哪儿也去不了”。去掉污点的语法,是在“键:效果”之后加上减号。污点变化后,调度器会重新尝试 Pending 的 Pod。

为一年之后和第二个节点做准备

用 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 公钥哈希,并在 /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 中确认。