
先说个题外话很多人会把标题里的“kubeadmin”看成正主其实Kubernetes官方那个用来初始化集群的工具叫kubeadm不是 kubeadmin。kubeadmin这个拼写在交接文档、工单截图里见过太多次了甚至有同事把某个集群管理后台的登录账号名当成工具名来搜教程绕了不少弯路。这篇内容就是围绕 kubeadm 从零搭一套“能用、敢用、出问题知道怎么救”的 Kubernetes 集群覆盖环境准备、组件安装、控制平面初始化、工作节点接入、网络插件落地以及我实际部署中踩过的坑和处理链路。适合刚上手 K8s、想在自己机器或公司内网环境里搭一套实验集群的运维和开发同学。1. 为什么是 kubeadm和二进制部署、托管集群的取舍1.1 三种部署方式横评搭建 Kubernetes 集群主流路径大致有三条二进制部署、kubeadm 部署、托管集群云厂商的容器服务。很多教程喜欢把三条路都讲一遍但落到实际选择上我认为关键不在于谁“更高级”而在于你要拿集群做什么、有没有长期维护的精力。二进制部署的好处是每个组件都由你自己拉起来etcd、kube-apiserver、kube-controller-manager、kube-scheduler、kubelet、kube-proxy逐个配置能让你把 K8s 的骨架看得很透。代价也很现实一次升级等于重新折腾一遍etcd 集群和 apiserver 的证书体系一旦没捋清楚排查起来非常耗时。适合学习原理不太适合做需要长期跟随版本更新的环境。托管集群省心控制面由服务商维护你只需要关心节点和业务。但如果你在内网环境、离线环境或者需要自定义 apiserver 参数、审计策略、准入控制器托管集群往往会给你设一层隐性的边界有些参数不开放给你调。kubeadm 正好卡在中间它把控制面组件的静态 Pod 清单自动生成把证书签发给到你面前把升级路径固定成一条相对成熟的操作链同时保留了底层节点的完全控制权。这意味着你既能拥有接近生产环境的部署形态又不用把手动搭 etcd 集群的那套繁琐流程全走一遍。1.2 kubeadm 的适用边界我的判断是这样的如果你需要一套单命令可控、升级路径稳定、组件可自定义的集群kubeadm 是最均衡的选项。现在很多内网生产环境、边缘机柜、离线交付项目里kubeadm 其实是事实标准官方也把 kubeadm 定义为“生产级集群安装工具”。不过它也有自己的适用边界。单控制面节点的 kubeadm 集群适合测试和开发环境生产环境至少需要三台控制面节点走高可用还要加负载均衡把 6443 端口分发到多台 apiserver。kubeadm 支持这种方式但需要额外准备负载均衡器排障复杂度会明显上升。所以本文先讲基础版的单控制面部署把流程跑通高可用是在这个基础上再加一层的逻辑。2. 环境规划与系统初始化版本、网络、内核参数的硬性要求2.1 节点规划与版本选型我这次规划的集群是三台节点一台控制面两台工作节点。操作系统选的是主流 Linux 发行版内核版本需要支持运行容器建议 5.x 以上。硬件方面控制面节点给 2 核 4G 起步工作节点 2 核 2G 起步磁盘最好用 SSD因为 etcd 对 IO 延迟比较敏感。版本选型是整个部署里最值得多花两分钟的事。kubeadm、kubelet、kubectl 这三个组件的版本必须保持严格一致比如都选 1.28.x 或者都选 1.31.x。Kubernetes 的版本支持策略是各组件之间的版本差不超过两个小版本但我建议直接锁同一版本省得出现“APF 参数不识别”“kubelet 证书轮换异常”之类说不清的问题。容器运行时我选containerd。Kubernetes 从 1.24 版本开始移除了对 Docker 的直接支持虽然可以通过 cri-dockerd 适配器继续用 Docker但没必要给自己埋这种坑。containerd 本身是 CNCF 孵化的项目和 K8s 的 CRI 接口贴合度最好部署时直接把它作为 kubelet 的下层依赖来对待。提示一定要先确认好你要安装的 Kubernetes 版本对应的 containerd 版本。有些 containerd 版本和特定 K8s 版本存在兼容性问题装之前去官方 compatibility matrix 查一眼比出问题了再排查快得多。2.2 系统预配置清单节点的 hostname 必须规范我用的是k8s-master01、k8s-node01、k8s-node02这种命名。为什么单独强调 hostname因为 kubeadm 初始化时会把节点的 hostname 写入证书的 SAN 或者 kubelet 的 Node 对象里如果 hostname 重复工作节点加入时会直接报 Node xxx already exists。另外 hostname 里尽量别带下划线某些 DNS 实现下会解析不正常。三台节点的/etc/hosts要写清楚彼此的解析记录确保节点之间能用主机名互通。这一步不能省因为 apiserver 生成的证书里包含了节点名你在节点上用主机名访问 apiserver 时解析走不通就会出现 x509 证书校验不匹配的报错。系统层面有几项硬性配置必须在装 kubeadm 之前处理完关闭交换分区swapoff -a同时注释掉/etc/fstab里的 swap 条目。kubelet 默认要求 swap 处于关闭状态否则节点启动会报错。关闭防火墙或放行必要端口测试环境和内网环境我习惯直接关闭 firewalld生产环境则在防火墙上放行 6443、2379-2380、10250、10259、10257、30000-32767 这几个端口段。加载 br_netfilter 内核模块K8s 的网络模型依赖 iptables 对桥接流量的处理不加载这个模块会导致 Service 的 ClusterIP 转发异常。2.3 内核模块与 sysctl 参数我每次装新节点时都会写一段固定的初始化脚本把内核模块和系统参数一次性配好你直接拿去用就行cat EOF | tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilter cat EOF | tee /etc/sysctl.d/k8s.conf net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sysctl --systemnet.bridge.bridge-nf-call-iptables这个参数的作用是让桥接的 IPv4 流量经过 iptables 规则处理。K8s 的 Service 本质上是一组 iptables 规则Pod 里的流量要访问 Service IP必须经过宿主机 netfilter 层如果这个参数没开表现通常是 Pod 之间能 ping 通但 Service 的 ClusterIP 怎么都访问不了而且不太好排查。net.ipv4.ip_forward是容器网络通信的基础Pod 网卡上的流量要路由出去宿主机必须先开启转发。这两个参数加一行说明它们的作用能帮你省掉后面大量的网络排障时间。3. 容器运行时选型与配置containerd 的坑比想象中多3.1 为什么默认选 containerd之前提到过 K8s 1.24 之后移除了 Docker但还有一个容易被忽略的点kubelet 通过 CRIContainer Runtime Interface和容器运行时通信containerd 原生实现了 CRI 插件而 Docker 需要额外通过 cri-dockerd 做一层转换。多一个中间层不仅增加故障点还会带来性能损耗。最重要的是containerd 的配置结构比 Docker 清晰很多它只有一份/etc/containerd/config.toml没有 client/server 那套模型。这对后面调试镜像拉取、配置私有仓库、调整 cgroup 驱动来说都更直接。3.2 安装与配置步骤以我常用的发行版为例安装 containerd 时我一般不用系统自带的旧版本而是从官方源安装。装完先执行一次mkdir -p /etc/containerd containerd config default | tee /etc/containerd/config.toml这条命令会生成一份默认配置。注意默认配置里的SystemdCgroup通常是false但你的节点如果是用 systemd 作为 init 系统kubelet 的 cgroup driver 一般也是 systemd。这两边必须对齐如果 containerd 用的是 cgroupfs、kubelet 用的是 systemd节点在运行容器时就会因为 cgroup 管理器不一致而报错最常见的表现是 Pod 一直处于 ContainerCreating 状态看 kubelet 日志会有 Failed to start container 之类的信息。所以生成默认配置后我第一时间会改两个关键项[plugins.io.containerd.grpc.v1.cri] # 沙箱镜像后面细说 sandbox_image registry.k8s.io/pause:3.9 [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc] runtime_type io.containerd.runc.v2 [plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup true改完用systemctl restart containerd重启然后systemctl status containerd确认状态。3.3 SystemdCgroup 与 sandbox_image 到底在干嘛SystemdCgroup true意思是让 runc 通过 systemd 来管理容器的 cgroup 目录而不是直接写 cgroupfs。为什么必须和 kubelet 保持一致因为 kubelet 会把 Pod 的 cgroup 配置通过 CRI 传给运行时如果运行时这边用的 cgroup 驱动和 kubelet 那边不是同一个管理器节点上就会同时存在两套 cgroup 管理逻辑产生资源统计错乱、驱逐异常这类难查的问题。sandbox_image是 pause 容器的镜像地址。每个 Pod 里都会先启动一个 pause 容器负责持有 Pod 的网络命名空间和资源配额业务容器再共享这个命名空间。如果这个镜像拉不下来所有 Pod 都起不来。默认配置里指向registry.k8s.io/pause:3.9在内网环境经常拉不到建议提前在镜像仓库里准备一份 pause 镜像然后把地址改到本地仓库。这个坑基本所有离线部署都会遇到我后面在踩坑清单里还会再提。注意containerd 的配置修改之后不只是 restart 一下那么简单config.toml某些版本版本如果格式不合法containerd 会启动失败。改完可以用containerd config check做一次配置校验再重启。4. 集群控制平面初始化kubeadm init 的完整参数解析4.1 初始化前的最后检查在跑kubeadm init之前我把所有节点的准备命令汇总一下避免中途发现环境缺东西。kubeadm、kubelet、kubectl 这三个二进制用官方软件源安装版本必须一致。安装完 kubelet 后注意先别急着启动它因为此时集群还不存在kubelet 会因为找不到 apiserver 而反复重启报错这是正常现象等 join 或 init 完成后再让它接管。初始化前我把检查项列成一个清单各节点时间同步是否正常时间不同步会导致证书校验时由于时间差过大而失败/etc/hosts是否写全三台节点之间能否通过主机名相互 ping 通swap 是否已经关闭fstab 里的 swap 条目是否注释掉内核模块和 sysctl 参数是否已加载containerd 是否为 running 状态ctr version能正常输出它有一个内置的预检流程哪个检查项没过会直接报出来大多数情况是 swap、内核参数、cgroup 驱动不匹配这几个问题。预检失败就一条条解决不要加--ignore-preflight-errors硬跳过去除非你明确知道自己在做什么。4.2 kubeadm init 完整命令控制面节点执行初始化命令时我习惯显式指定三个参数kubeadm init \ --apiserver-advertise-address192.168.1.11 \ --pod-network-cidr10.244.0.0/16 \ --kubernetes-versionv1.31.0--apiserver-advertise-address指定 apiserver 对外通告的 IP 地址一般填控制面节点的内网 IP。如果你有多网卡且默认网卡不是你想让集群用的那张这个参数必须写否则 apiserver 会选择错误的 IP导致工作节点 join 后连不上控制面。--pod-network-cidr是 Pod 网段的 CIDR它决定了后面网络插件分配给每个 Pod 的 IP 范围。这里选的10.244.0.0/16是 Flannel 的常见默认网段。如果你要用 Calico可以选192.168.0.0/16这一段其它地址池。重点注意这个网段绝对不能和你的宿主机所在局域网网段冲突否则 Pod 访问外部网络时会直接路由混乱。我见过有人把 Pod 网段设成和公司办公网同一个网段结果一堆服务互相串网排查得很痛苦。--kubernetes-version指定版本号必须和你安装的 kubeadm 版本一致。有时候你装的 kubeadm 是 1.31.0但默认初始化会去拉取最新版本镜像不指定这个参数就会出现本地 kubeadm 和远端镜像版本对不上的情况不推荐依赖这种隐式行为。初始化过程会持续几分钟期间 kubeadm 会依次完成生成 CA 证书、生成组件证书、生成 kubeconfig、生成静态 Pod 清单、拉取控制面组件镜像、启动 etcd 和 apiserver 等静态 Pod。最终输出会在结尾处显示三行后续操作所需的信息kubeconfig配置命令、kubeadm join加入命令、以及部署网络插件的提醒。4.3 初始化输出的含义与 kubectl 配置初始化完成后控制面节点上可以用如下命令启用 kubectlmkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config/etc/kubernetes/admin.conf是 kubeadm 生成的集群管理凭证把它拷贝为 kubectl 的默认配置后你才能通过 kubectl 访问集群。之后用kubectl get nodes能看到控制面节点状态但此时节点一般是 NotReady因为网络插件还没装。初始化输出里那句kubeadm join 控制面IP:6443 --token xxx --discovery-token-ca-cert-hash sha256:xxx要保存好工作节点加入集群需要用到。token 是有时效的默认大概是 24 小时。如果你日后把这条命令丢了或者 token 过期了可以用kubeadm token create --print-join-command重新生成一条。提示discovery-token-ca-cert-hash是用于校验控制面证书链的散列值工作节点在加入时用这个散列验证 apiserver 的身份避免中间人攻击。别为图省事在 join 时加--discovery-token-unsafe-skip-ca-verification这在公网和跨网络环境尤其危险。5. 工作节点加入与网络插件落地5.1 生成的 join 命令怎么用工作节点只需要安装好 containerd、kubeadm、kubelet、kubectlkubectl 其实工作节点可以不装但为了方便排查我一般也装上然后执行控制面节点输出里的 join 命令kubeadm join 192.168.1.11:6443 \ --token token \ --discovery-token-ca-cert-hash sha256:hashjoin 过程比 init 短很多工作节点先从 apiserver 拉取集群配置生成节点证书然后启动 kubelet 并把自己的信息注册到集群。join 完成后到控制面节点执行kubectl get nodes正常情况下三台节点都会出现在列表里。刚 join 的节点状态会是 NotReady直到网络插件给它分配 Pod 网络。这里有一个容易踩的情况如果 join 时报错说节点已经存在通常是你之前清理不彻底需要在该节点上执行kubeadm reset然后删/etc/kubernetes目录清理残留再重新 join。5.2 网络插件Calico 的安装与选择原因网络插件我这次选 Calico。它的优势在于集成了 BGP 组网和网络策略NetworkPolicy你要给 Pod 做网络隔离、做跨节点通信控制Calico 比 Flannel 的能力上限高得多。Flannel 胜在简单VXLAN 模式下几乎不需要额外配置但在理解个性和长期演进之间我更倾向于 Calico。安装 Calico 时用的是官方提供的 manifest 文件但我更习惯把 manifest 先下载到本地然后修改镜像地址为企业内网仓库再 apply。因为你直接kubectl apply -f在线装如果节点无法访问公共仓库Calico 的 Pod 会一直拉镜像失败整个集群的网络一直不 ready。Calico 的 manifest 里有个关键配置项是CALICO_IPV4POOL_CIDR这个值必须和你 init 时传入的--pod-network-cidr一致。比如 init 时写了10.244.0.0/16这里就要改成10.244.0.0/16否则 Calico 创建的 Pod 会落到它自己的默认网段和 kubelet 期望的 Pod 网段不一致后果是跨节点 Pod 通信直接断裂。安装完成后kubectl get pods -n kube-system | grep calico等所有 calico-node Pod 变为 Running再看节点状态kubectl get nodes此时节点应该全部变为 Ready。5.3 节点状态验证节点 Ready 不等于一切正常我一般会额外看几个细节。kubectl describe node能看到节点的 Condition、Addresses 和系统信息排查节点异常首选它。然后我会检查 CoreDNS 是否正常因为内部域名解析依赖它kubectl get pods -n kube-system | grep corednsCoreDNS 问题是集群初始化后最容易暴露的隐患节点成为了 Ready但 Pod 之间用 Service 域名访问不通而用 ClusterIP 访问却正常。这种情况下优先排查 CoreDNS 的副本数、日志和集群网络连通性。6. 实测踩坑清单镜像拉取失败、kubelet 启动异常、证书过期6.1 镜像拉取失败好几个“解法”都不是真的解法kubeadm init 时最常见的失败原因就是镜像拉取失败。报错信息形如failed to pull image registry.k8s.io/etcd:3.5.7-0先说一个容易误导的操作很多人看到拉取失败立刻去配置全局代理或改 docker 配置但 kubeadm 用的是 containerd不认 Docker 的配置。你要改的是 containerd 的config.toml给它配置镜像仓库的加速地址或者在离线环境下预先在镜像仓库里准备好所有组件镜像再用kubeadm init --image-repository指向内网仓库。我第二次搭集群时踩过一个更隐蔽的坑明明本地已经有了 pause 镜像kubeadm init 时依然报拉取失败。后来查了才发现containerd 默认配置里的sandbox_image指向的是公共仓库地址kubelet 通过 CRI 调 containerd 拉取时永远是去公共仓库拉而不会用本地已有的镜像。所以如果你的环境是离线或半离线的镜像准备到位之后别忘了一处改config.toml一处改 kubeadm 的--image-repository两边配合才能绕开这个坑。6.2 kubelet 启动失败的完整排查链路初始化完成后 kubelet 一直报启动失败这个问题的排查链路值得展开说。我先说症状systemctl status kubelet显示 active (running)但kubectl get nodes里看不到节点或者节点状态总是 NotReady。有经验的同事会说“看 kubelet 日志”但 kubelet 的日志默认会打到 journald而且错误信息经常被刷屏掩盖需要精确过滤。我的排查顺序是这样journalctl -u kubelet -f --since 5 min ago观察实时日志在日志里找Unable to update cni config、Failed to run kubelet这类关键行用kubectl describe node name查看节点上的Ready条件是否为 False最常见的根因有几种containerd 未启动或重启后没恢复kubelet 无法连接 CRI socket报failed to connect to containerdcgroup driver 不一致报failed to run kubelet: systemd cgroup driver is requiredkubelet 启动时找不到集群配置报kubelet config file not found说明节点之前没有正常 join 或 init 过遇到 cgroup driver 报错时解决办法不是在 kubelet 里硬改而是要把 containerd 的SystemdCgroup设为 true并保证 kubelet 的配置指向/etc/kubernetes/kubelet.conf。我踩过最深的一个坑是containerd 已经配好了但 kubelet 因为旧配置缓存还指向了 cgroupfs重启了几次都不行最后删掉/etc/kubernetes下的残留 kubelet 配置重新 init 才解决。所以节点加入失败时优先kubeadm reset干净重来往往比原地补丁更省时间。6.3 证书过期kubeadm 集群里最容易忽视的定时炸弹kubeadm 生成的证书默认有效期是一年一年之后 apiserver 会开始报证书过期错误。症状是 kubelet 能启动kubectl 执行命令时提示 x509 certificate has expired 或证书校验失败。这个问题在生产集群中很常见因为很多团队第一次搭完集群后就把证书这事忘干净了。处理方式有两种一是手动续期kubeadm certs renew all之后重启 kubelet 和控制面组件二是设置定时任务定期检查证书有效期比如每个月跑一次kubeadm certs check-expiration这个命令会列出所有证书的剩余时间一眼能看到哪些快过期了。我建议把证书检查纳入集群日常巡检脚本因为证书过期是少数几个“不提前发现一到时间全线瘫痪”的故障。注意kubeadm certs renew all只会更新证书不会管理证书生命周期里关联的 kubeconfig 文件比如 admin.conf。续期之后如果你现有的 kubectl 凭证仍然要求重新生成可以重新执行kubeadm init phase kubeconfig admin这类命令但我更建议在测试环境试一遍再上生产别临场再摸索。6.4 其他小坑hostname 冲突、swap 回弹、NodePort 被占用除了上面几个大头还有一些出现频率不低的小问题。hostname 冲突我在前面提过这里单独说一下场景有两台节点使用了相同的 hostname可能因为从虚拟机模板克隆没有改主机名join 时 apiserver 只认 hostname 不认 IP新节点会一直注册不上表现为 join 命令执行成功但kubectl get nodes里始终只有旧节点。解决方式是改 hostname 后重新 join。swap 回弹swapoff只是临时的如果不处理/etc/fstab重启后 swap 又会挂载回来。有些教程只在部署时跑了一遍命令没做持久化结果节点重启后 kubelet 直接崩溃。建议用系统化的方式如 swap 设备的注释或 mask 服务来彻底禁用。NodePort 占用K8s 默认的 NodePort 端口范围是 30000-32767但 NodePort 的端口是全局的一个端口只能被一个 Service 使用。如果你想快速部署多个对外暴露的服务端口可能会撞车。处理方式是在 apiserver 配置里调整--service-node-port-range比如改成 20000-32767扩宽端口范围。7. 可用性验证与日常巡检部署一个测试应用并建立检查习惯7.1 部署测试应用验证集群可用集群装完不要直接丢业务上去先跑一个简单的测试应用验证端到端链路。我习惯用一个简单的 HTTP 服务镜像来做验证kubectl create deployment test-nginx --imagenginx:latest kubectl expose deployment test-nginx --typeNodePort --port80 kubectl get pods -o wide等 Pod 变成 Running 后用任意节点的 NodePort 访问测试服务。如果访问通了说明 Pod 调度、kubelet、kube-proxy 的 iptables 转发链路都是通的。如果访问不通按这样的思路排查Pod 是否 Running不是则看 kubelet 日志和kubectl describe pod节点上 30000-32767 端口是否被防火墙拦截kubectl get svc里END POINTS是否已经有 Pod 的 IP没有则说明 Service 的后端列表没填充节点上用curl ClusterIP:port看通不通通但 NodePort 不通重点查 kube-proxy 和防火墙这个测试应用同时也能验证跨节点通信是否正常。把副本数扩到多副本让它调度到不同的节点上然后从控制面节点访问 Service IP如果都能通说明节点间的 Pod 网络和 kube-proxy 转发没有问题。7.2 日常巡检命令与习惯集群进入稳定运行后我建议在控制面节点上把这几条命令固化到日常巡检脚本里kubectl get nodes -o wide # 节点状态和 IP 信息 kubectl get pods -A | grep -v Running | grep -v Completed # 非正常运行状态的 Pod kubectl get events -A --sort-by.lastTimestamp | tail -20 # 最近的集群事件 kubeadm certs check-expiration # 证书剩余时间最后再补充一个操作习惯任何涉及 kubeadm 的大变更升级、证书续期、配置调整都建议先在测试环境完整跑一遍。kubeadm 的最大价值是提供了确定性和可重复性让变更可以在另一套环境里被完整复现这个特性在生产场景里远比“初始化很快”更重要。我个人在一次升级中得到的教训是升级前不仅备份了/etc/kubernetes还在另一个节点上完整搭了同版本环境做演练。真正操作时发现网上教程里轻描淡写的“升级 kubeadm、kubelet 二进制后重启即可”这句话在实际环境中会牵扯到镜像版本、CNI 兼容性、kubelet 配置迁移等多个变量。只有亲手完整演练过一遍才敢在生产环境点下那个回车。写这篇东西时我把整个部署链路再过了一遍发现绝大多数问题都集中“容器运行时配置”“网络插件网段”“证书生命周期”这三件事上。提前把这三件事理解清楚kubeadm 搭集群其实是件很确定的事我只要按照预检、初始化、join、装网络插件、验证这几个步骤来每一步都明确自己在做什么集群就能稳定跑起来。