
搞了大半天的 k8s 集群部署中间踩了不少坑趁周末把整个过程重新整理了一遍。这篇主要记录我用 kubeadm 方式部署一套三节点 Kubernetes 集群的全过程从环境准备、容器运行时安装、控制平面初始化到网络插件部署、工作节点加入、验证集群可用性全部按实际执行顺序走下来每步都有命令和输出参考。如果你正准备接触 Kubernetes想自己搭一套环境练手或者工作中要在测试环境快速拉起一套集群这篇可以作为直接参考的操作手册。我会把部署过程中“为什么要这样配置”“遇到问题从哪里查”也一并写清楚少走弯路。1. 环境准备三台机器怎么规划才不会在集群部署时返工1.1 节点规划与版本选择我这边用的是三台虚拟机做实验角色分别是控制平面节点也就是常说的 Master新版本里叫 Control Plane和两台工作节点。这种“一个控制平面 两个工作节点”的组合是练手最舒服的配置能体验完整的调度、网络、服务暴露流程又不需要为高可用付出太多成本。角色主机名IP 地址配置要求操作系统控制平面节点master01192.168.1.102核 4G 50GUbuntu 22.04工作节点worker01192.168.1.112核 2G 50GUbuntu 22.04工作节点worker02192.168.1.122核 2G 50GUbuntu 22.04版本这里我选的 Kubernetes 1.28.2这个版本不算最新但相当稳定网上资料也多各种问题基本都有现成答案。如果你完全按这篇来建议保持同样的版本号避免因为版本差异导致参数变更。顺带说一句很多人刚开始会想用单机 all-in-one 的方式部署所有角色塞在一台机器上。这种方案只适合感受一下 kubectl 命令长什么样真要测试调度、跨节点通信、NodePort 访问完全体验不到集群和单机的区别。所以别偷懒三台机器是最低配置。1.2 系统初始化清单每个节点的必备配置不管哪台节点部署前都应该做一轮系统初始化。这些操作看似琐碎但每一项都对应着一个常见的部署失败原因。第一项是设置主机名和 hosts 解析。三个节点分别设置成 master01、worker01、worker02然后分别在 /etc/hosts 里加上三行记录。Kubernetes 组件之间通信会用到主机名如果解析不正确kubelet 可能会因为无法访问 apiserver 一直报错。第二项是关闭 swap。这个很多新手不理解明明内存不够为什么还要关交换分区因为 kubelet 的资源管理和内存 QoS 依赖 cgroup 提供的准确内存使用统计一旦启用 swap内存回收行为就不受 kubelet 控制Pod 的内存上限也就形同虚设了。执行 swapoff -a 并注释掉 /etc/fstab 里的 swap 行即可。第三项是确认内核模块。Kubernetes 网络转发依赖 br_netfilter 和 overlay 模块可以预先加载cat EOF | tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilter第四项是内核参数。桥接流量需要经过 iptables 规则否则集群内 Pod 之间的通信可能被防火墙规则挡住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 --system第五项是时间同步。Kubernetes 集群的证书验证和日志时间戳都依赖各节点时间一致偏差太大时kubelet 和 apiserver 之间的 TLS 握手容易直接失败。Ubuntu 下默认有 systemd-timesyncd确认它处于运行状态即可如果是 CentOS 环境就装一下 chrony。第六项是防火墙。我这个实验环境直接关闭了 firewalld因为端口放行规则在复杂网络环境里很容易漏掉。但生产环境千万别这么干应该按端口清单精细化放行。Kubernetes 主要需要放行 6443、2379/2380、10250、10251/10252、30000-32767 这几个端口。这些初始化步骤虽然多但你把这些命令在三个节点上都跑一遍后面部署会顺畅非常多。1.3 容器运行时的选择为什么我用 containerd 而不是 Docker这里要说明一个很多人容易搞混的点新版 Kubernetes 已经不直接使用 Docker 作为容器运行时了而是通过 CRIContainer Runtime Interface对接 containerd。自 Kubernetes 1.24 起dockershim 从 kubelet 中移除你用 Docker 也最终会在底层调用 containerd所以与其绕一圈不如直接安装 containerd。安装 containerd 在 Ubuntu 上可以用 apt也可以从二进制包装。我直接用的官方 apt 源apt-get update apt-get install -y containerd安装完成后需要生成一个配置文件默认情况下 containerd 没有 /etc/containerd/config.toml 这个文件需要手动生成mkdir -p /etc/containerd containerd config default /etc/containerd/config.toml关键的一步是修改 SystemdCgroup 这个配置项。默认生成的配置里它是 false也就是使用 cgroupfs而 kubelet 通常期望使用 systemd 驱动。两者不一致会导致节点上的资源统计错乱甚至 kubelet 无法启动。把配置文件里 SystemdCgroup false 改成 truesed -i s/SystemdCgroup false/SystemdCgroup true/g /etc/containerd/config.toml另外containerd 默认拉取 pause 镜像的源是 registry.k8s.io在国内访问经常超时建议改一下 sandbox_image 的配置把它替换成国内可访问的镜像仓库地址。这一步我在后面问题排查部分会详细说。配置改好后重启 containerd 并设置开机自启systemctl daemon-reload systemctl restart containerd systemctl enable containerd到这里三个节点的系统层面准备就完成了接下来进入 Kubernetes 组件的部署阶段。2. kubeadm 部署的核心思路先搞懂它帮你做了什么2.1 控制面组件各自的作用一张表讲清楚直接用 kubeadm 初始化之前有必要先了解一下 Kubernetes 控制平面有哪些组件各自干什么活。这样后续看到相关 Pod、日志的时候心里有底排查问题也不至于抓瞎。组件职责生活类比kube-apiserver所有请求的唯一入口提供 REST API读写 etcd前台的业务办理窗口etcd集群所有状态数据的存储中心合同档案室kube-controller-manager维持集群期望状态处理节点、副本、Endpoint 等各类控制器纠察队kube-scheduler决定新 Pod 调度到哪个节点房产中介kubelet每个节点上的代理负责启动、停止、健康检查容器现场执行人员kube-proxy维护节点上的网络规则实现 Service 的负载转发快递分发站搞清楚这些组件的分工之后再去看部署日志就不会一头雾水。比如 etcd 启动失败那么 apiserver 大概率也起不来因为 apiserver 依赖 etcd 做数据存储。2.2 kubeadm init 的执行过程拆解用二进制方式手动部署 Kubernetes 需要自己管理证书、kubeconfig、systemd 服务、静态 Pod 定义步骤非常多容易漏。而 kubeadm 把这些工作自动化了它的好处在于标准化流程和可预期的失败位置。简单拆解一下 kubeadm init 主要做的事情第一步是预检也就是 preflight checks。它会检查内核参数、swap 状态、端口占用、容器运行时可用性等发现问题就直接报错提示。所以如果 kubeadm init 刚执行就失败99% 的报错信息会明确告诉你缺什么。第二步是生成 CA 证书以及各组件证书包括 apiserver、etcd、kubelet、controller-manager、scheduler。这些证书默认存放在 /etc/kubernetes/pki 下有效期通常是一年社区也提供了证书轮换机制但实验环境下一般不用操心。第三步是生成一下 kubeconfig 文件放在 /etc/kubernetes 目录下分别是 admin.conf、kubelet.conf、controller-manager.conf 和 scheduler.conf。第四步是把控制平面组件的静态 Pod 定义写入 /etc/kubernetes/manifests 目录。这些组件不是通过 kubeadm 直接启动的而是由 kubelet 以静态 Pod 的方式拉起。如果你进到 /etc/kubernetes/manifests 目录下会看到 apiserver.yaml、controller-manager.yaml、scheduler.yaml、etcd.yaml 几个文件。第五步是为集群生成 bootstrap token并输出 kubeadm join 命令供工作节点加入时使用。这个 token 默认有效期 24 小时过期后需要重新生成。2.3 版本一致性控制平面和工作节点为什么要保持同版本kubeadm、kubelet、kubectl 和 kubernetes 服务器版本之间最好保持一致。虽然 Kubernetes 官方允许 kubelet 版本略低于 apiserver 版本但偏差过大时会出现兼容性问题例如部分 API 资源无法识别调度逻辑异常等。我统一选择 1.28.2所有节点执行同样的安装命令避免出现版本错配这种难以排查的怪问题。这也是实验环境必须坚持的原则别在某一个节点上单独升级或降级。3. 控制平面安装实操从安装工具到集群初始化3.1 安装 kubeadm、kubelet、kubectl在三个节点上统一安装 kubeadm、kubelet、kubectl。国内环境直接用 Google 的 apt 源肯定不行可以改用阿里云的 Kubernetes 软件源。先把源配置好apt-get update apt-get install -y apt-transport-https ca-certificates curl gpg curl -fsSL https://mirrors.aliyun.com/kubernetes/apt/doc/apt-key.gpg | gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo deb [signed-by/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://mirrors.aliyun.com/kubernetes/apt/ kubernetes-xenial main | tee /etc/apt/sources.list.d/kubernetes.list这里有个细节阿里云源里的源码库叫 kubernetes-xenial但它在 Ubuntu 22.04 上也能用不用特意改成 jammy。接下来执行安装apt-get update apt-get install -y kubelet1.28.2-00 kubeadm1.28.2-00 kubectl1.28.2-00安装完成后先执行 systemctl enable kubelet但暂时不要 start。因为 kubelet 现在还没有任何配置强行启动会失败这是正常的。3.2 编写 kubeadm-init.yaml 初始化配置在 master01 上用下面这条命令生成默认配置作为模板kubeadm config print init-defaults kubeadm-init.yaml然后编辑这个 YAML 文件我需要修改这几个关键位置advertiseAddress改成 master01 的 IP也就是 192.168.1.10imageRepository改成一个国内可访问的镜像仓库否则后面拉取组件镜像会很痛苦kubernetesVersion改成 1.28.2networking.podSubnet改成 10.244.0.0/16 或者你后续网络插件要求的值修改后的核心片段类似下面这样apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.10 bindPort: 6443 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: 1.28.2 imageRepository: registry.aliyuncs.com/google_containers networking: podSubnet: 10.244.0.0/16有人会问podSubnet 为什么写成 10.244.0.0/16不能随便写吗这要看你后面选什么网络插件。Flannel 默认就使用 10.244.0.0/16 这个网段保持一致可以避免网段冲突。3.3 执行 kubeadm init 并处理输出配置写好后在 master01 上执行初始化kubeadm init --config kubeadm-init.yaml这一步会花一些时间主要卡在镜像拉取和控制平面容器启动上。如果一切正常最后会看到类似下面的输出Your Kubernetes control-plane has initialized successfully! To start using your cluster, you need to run the following as a regular user: mkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config Then you can join any number of worker nodes by running the following on each as root: kubeadm join 192.168.1.10:6443 --token abc123.def456xyz789 --discovery-token-ca-cert-hash sha256:xxxxxx这段输出非常关键务必复制保存下来尤其是最后那行 kubeadm join 命令后面添加工作节点要用的。如果忘记保存也没关系可以用 kubeadm token create --print-join-command 重新生成一条这个我在问题排查部分会再讲。3.4 配置 kubectl 并验证控制平面组件按输出提示在 master01 上配置 kubectlmkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config然后验证集群状态kubectl get nodes此时 master01 应该显示为 NotReady这是正常现象控制平面的 Pod 虽然起来了但整个集群还没有网络插件节点状态无法变为 Ready。可以再看看控制平面组件的运行情况kubectl get pods -n kube-system你会发现 kube-apiserver、etcd、kube-controller-manager、kube-scheduler 这几个 Pod 都在 Running 状态。如果它们出现了 CrashLoopBackOff多半是静态 Pod 定义有问题可以直接查看对应容器的日志比较常见的诱因是证书目录权限、参数写错或者 etcd 端口占用。这里还有一个新版 Kubernetes 很容易让人误解的地方执行 kubectl get cs 会显示连接错误。这个命令在新版本里已经不准了因为健康检查改用了单独的 healthz 端点返回错误不代表集群有问题可以直接忽略。4. CNI 网络插件与工作节点的接入4.1 选 Flannel 还是 Calico控制平面跑起来之后接下来就要解决集群网络的问题。CNIContainer Network Interface插件负责为每个 Pod 分配 IP并打通跨节点通信。常见的两个选择是 Flannel 和 Calico。Flannel 使用 VXLAN 技术简单、轻量、性能够用适合学习和中小规模集群。Calico 基于 BGP 协议支持网络策略性能更好配置也更复杂适合生产环境有细粒度安全策略需求的场景。我这次实验用的是 Flannel原因很简单先用最小成本让集群跑起来把整体流程跑通。等你熟悉了基础操作再上 Calico 也不迟。4.2 部署 Flannel 网络插件部署 Flannel 其实就是应用一份 Manifest 文件。官方提供的文件是一个包含了很多资源定义的 YAML里面定义了 DaemonSet、RBAC、ConfigMap 等。在 master01 上执行kubectl apply -f kube-flannel.yml这里需要留意一个问题默认的 Flannel 仓库地址在国内可能下载不通导致镜像拉取失败。可以在部署前先把 kube-flannel.yml 文件里镜像地址改成国内可访问的或者提前把 flannel 和 flannel-cni 相关镜像加载到 containerd 里。部署完成后查看 kube-system 命名空间下面的 Podkubectl get pods -n kube-system你会看到 flannel 的 Pod 会出现在每个节点上当前也就是 master01 上有一个。因为 DaemonSet 会确保每个节点运行一个网络插件的 Pod。如果这个 Pod 一直处于 ImagePullBackOff多半是镜像地址的问题按上面提到的思路处理。Flannel 正常后master01 会变成 Ready 状态kubectl get nodes输出里的状态应该是 master01 Ready这里 Ready 的含义是 kubelet 健康、容器运行时正常、CNI 网络可用。4.3 工作节点加入集群的完整流程工作节点加入集群的流程其实比控制面简单得多。worker01 和 worker02 上已经完成了系统初始化和 containerd 安装现在只需要补上 kubeadm 和 kubelet 组件。apt-get install -y kubelet1.28.2-00 kubeadm1.28.2-00 systemctl enable kubelet然后直接在两个节点上执行之前保存的 join 命令kubeadm join 192.168.1.10:6443 --token abc123.def456xyz789 --discovery-token-ca-cert-hash sha256:xxxxxx如果 join 过程卡住或者报容器运行时找不到先检查 containerd 是否启动、服务是否在监听。Kubelet 启动后会自动向 apiserver 注册节点并上报状态。回到 master01 上查看节点状态kubectl get nodes正常情况下会看到三个节点但 worker01 和 worker02 大概率是 NotReady。不用着急它们需要等 kubelet 把状态上报上去Flannel 的 DaemonSet 也会自动调度到新节点并启动。等一两分钟再执行一次你会发现三个节点全部变成 Ready。4.4 用 Nginx 快速验证集群可用性节点全部 Ready 只是架构层面的成功真正能跑业务才叫部署完成。我习惯用 Nginx 做一个最简单的验证。先创建一个 Deploymentkubectl create deployment nginx --imagenginx检查 Pod 调度情况kubectl get pods -o wide你会看到 nginx Pod 被调度到某一个工作节点上比如 worker01。此时如果你想从外部访问这个 Nginx需要创建一个 Service类型选 NodePortkubectl expose deployment nginx --port80 --typeNodePort查看服务分配到的 NodePortkubectl get svc假设 NodePort 是 30567那么在你自己电脑的浏览器里访问 http://192.168.1.11:30567如果看到 Nginx 的欢迎页说明集群从部署到内部网络到外部访问整条链路都是通的。你可以把访问不同节点的同一个 NodePort 试一下就会体会到 kube-proxy 的转发效果。5. 部署过程中的常见问题与排查笔记5.1 镜像拉不动换源与预加载kubeadm init 阶段最常见的失败原因就是镜像拉取超时。老版本中默认的镜像仓库是 k8s.gcr.io后来换成了 registry.k8s.io这两个地址在部分网络环境下访问速度都很慢甚至直接超时。解决方案就是在 kubeadm-init.yaml 里指定 imageRepository 为国内可访问的仓库地址例如imageRepository: registry.aliyuncs.com/google_containers但这里要注意一个细节并不是所有镜像都在这个仓库里齐全尤其是 pause、coredns 这几个。如果 init 阶段还是报某个镜像找不到可以用kubeadm config images list --config kubeadm-init.yaml查看完整的镜像列表然后逐一手动 pull 并打上原仓库的 tag再用 kubeadm init 时跳过拉取。这种方式虽然笨但非常可靠。另外 containerd 的 sandbox_image 也需要同步改成可拉取的 pause 镜像地址否则 Kubernetes 运行时在创建 Pod 时会卡在 SandBox 阶段。5.2 节点 NotReady 的排查顺序节点一直是 NotReady可以从这几个方向依次排查第一看 kubelet 服务状态systemctl status kubelet journalctl -u kubelet -fkubelet 的日志信息非常丰富几乎每个具体问题都会直接扫一眼就能看到关键词。比如没装网络插件时日志里出现 CNI 相关报错或网络未就绪。第二看节点详情kubectl describe node worker01描述信息底部有 Conditions 列能直接看到 Ready、MemoryPressure、PIDPressure 等具体状态以及最近发生的事件。这部分信息能帮你快速判断是网络、存储还是资源问题。第三确认 CNI 是否正常。最简单的方式就是看 kube-system 下的 flannel Pod 是否 Running如果它一直报镜像拉取或宿主机网络配置失败节点就不可能 Ready。这个排查顺序基本可以覆盖 90% 的 NotReady 场景。5.3 cgroup 驱动不匹配导致 kubelet 起不来在 containerd 配置时我已经特意提到过 SystemdCgroup原因就在这个坑上。如果 containerd 的 SystemdCgroup 保持 false而 kubelet 默认使用 systemd 驱动kubelet 启动时就会报类似这样的错误kubelet cgroup driver: systemd is different from docker cgroup driver: cgroupfs不过新版 kubelet 大多会直接失败提示你检查容器运行时。解决办法就是改 containerd 配置里的 SystemdCgroup 为 true然后重启 containerdsystemctl restart containerd切记修改后最好在三个节点上统一检查一遍因为这种错误通常是一台台地冒出来不是同时出现。还有一个相关联的坑如果你的节点上同时装了 Docker 和 containerd而 Docker 的 cgroup 驱动是 cgroupfs也可能出现类似报错。这里面的经验是保持整个节点环境里“所有容器运行时 kubelet”的 cgroup 驱动一致就能避开大部分资源问题。5.4 token 过期、kubectl get cs 报错等琐碎问题kubeadm init 输出的 token 默认有效期是 24 小时。如果第二天再来加节点join 会报 token 无效。解决办法是重新生成一条kubeadm token create --print-join-command这条命令会直接输出完整的 join 命令复制粘贴到工作节点上执行就行。还有一个问题是 CA 证书 hash 不匹配。如果 join 时报 hash 校验失败可以在 join 命令里加上 --discovery-token-ca-cert-hash 参数来指定不过要注意这个 hash 是固定的它对应控制平面的 CA 证书不要随意绕过校验除非是实验环境临时使用。再有就是 kubectl get cs 这个命令之前提过了新版里它返回 connection refused 不代表集群有问题直接改用 kubectl get componentstatuses 或检查核心 Pod 状态即可避免被这种历史遗留命令误导。5.5 实验环境一条命令重置集群反复练习时你可能经常会想从头来一遍。这时候不需要重装系统直接在当前节点上执行kubeadm reset -f然后清理网络相关的残留配置rm -rf /etc/cni/net.d iptables -F iptables -t nat -F如果你想彻底一点可以把 /etc/kubernetes 也备份后清掉再把 containerd 数据目录清空。重置后重新执行 kubeadm init 或 join 流程即可。这一套流程多走几遍你会对 kubeadm 内部做了什么有更直观的认识。最后说点实际经验。环境从零部署到三节点 Ready理想情况下 40 分钟到一个小时但对第一次操作的人来说两三个小时也很正常。最容易出问题的永远是镜像拉取和 cgroup 驱动其次是网络插件的安装。我现在的习惯是先把所有节点的系统初始化脚本整理好跑完一遍再折腾 Kubernetes 相关的内容这样整个流程的稳定性会高很多。部署完集群不等于完事后面的 Pod 调度、持久化存储、Ingress 接入都还有得折腾。这套集群如果你还想继续用建议尽早把证书续期、组件升级这些运维事项纳入计划不然过一年再回来会发现集群内部不少证书都过期了。