先说个题外话:很多人会把标题里的“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 通过 CRI(Container 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-address=192.168.1.11 \ --pod-network-cidr=10.244.0.0/16 \ --kubernetes-version=v1.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 配置
初始化完成后,控制面节点上可以用如下命令启用 kubectl:
mkdir -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、kubectl(kubectl 其实工作节点可以不装,但为了方便排查我一般也装上),然后执行控制面节点输出里的 join 命令:
kubeadm join 192.168.1.11:6443 \ --token <token> \ --discovery-token-ca-cert-hash sha256:<hash>join 过程比 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 containerd - cgroup driver 不一致,报
failed to run kubelet: "systemd" cgroup driver is required - kubelet 启动时找不到集群配置,报
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 --image=nginx:latest kubectl expose deployment test-nginx --type=NodePort --port=80 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、装网络插件、验证这几个步骤来,每一步都明确自己在做什么,集群就能稳定跑起来。