Ubuntu集群部署最佳实践:用kubeadm搭建Kubernetes集群全流程指南
1. 为什么我仍然推荐kubeadm做Ubuntu集群部署1.1 几种部署路线对比“Ubuntu部署k8s”这个话题我每次回答都会先反问你是想快速得到一个环境还是想真正理解Kubernetes是怎么运转的这两种目标对应的部署路线完全不同。如果你手上是几台Ubuntu服务器想要一个多节点、可扩展、以后还能往生产靠拢的集群我优先推荐kubeadm而不是minikube也不是纯二进制手工部署。先看常见的几条路线。minikube本质上是在一台机器里跑一个伪集群适合本地学习和跑Demo但你要理解“多节点之间怎么通信”“证书是怎么签发的”这些核心问题它帮不上忙。kind基于Docker节点多用于CI流水线和真实物理机集群的差距比较大。二进制部署虽然能让你把每个组件都摸一遍但光是维护apiserver、etcd、kubelet之间的证书关系和启动参数就足以让新手劝退。kubeadm是Kubernetes社区官方维护的集群引导工具它的定位很有趣不隐藏核心概念但把重复性的证书签发、组件配置、节点令牌管理都自动化了。你在Ubuntu上跑kubeadm得到的是一个和官方架构一致的集群同时还能看到每一步到底发生了什么。对于1到3台服务器起步、以后可能扩展到十几台的场景kubeadm在可理解性和可维护性之间是最平衡的选项。另外可以对比容器化部署工具链这类工具上手快一条命令就能装出集群但它们把太多细节封装成了“黑盒”。一旦集群出问题你想定位是哪个组件坏了光看抽象层就够折腾半天。我的建议很直接学习阶段用kubeadm生产环境如果运维能力不够可以后期再迁移到更顺手的工具但底层原理不能靠工具替你理解。部署方式多节点生产可用性学习价值维护成本kubeadm支持较高高能看清组件关系中等纯二进制部署支持可以最高高minikube单机不适合中低kind伪多节点不适合中低容器化全家桶支持可以低中1.2 选择kubeadm的边界条件与适用场景kubeadm并不是银弹。如果集群规模很大比如上千节点、需要多控制平面负载均衡、需要和外部OpenStack或云环境强集成kubeadm虽然能做但手动的部分会变多这时候更合理的方案是借助成熟发行版或云托管集群。如果只有单机而且你只想快速出一个环境写点业务代码minikube反而更省心。那kubeadm真正适合谁第一类是准备考认证或者正在系统学习容器编排的人因为考试环境里就是kubeadm部署平时用熟了考试不慌。第二类是公司内部有多台Ubuntu物理机或虚拟机想搭一个自建集群跑内部系统这类场景规模通常不大也没有太复杂的网络要求kubeadm完全扛得住。第三类是想理解“部署”本身的人你会看到kubelet是怎么通过令牌找到控制平面的etcd是怎么在静态Pod里运行的CNI插件是在什么时机被安装的。还有一个容易被忽略的点kubeadm在Ubuntu这种systemd体系下跑得非常顺。Ubuntu的kubelet和containerd都天然适配systemd管理cgroup驱动也能统一到systemd不会出现某些发行版里常见的初始化管理器冲突。这也是为什么网上几乎所有k8s教程都喜欢拿Ubuntu做演示——不是巧合而是这个组合确实顺。2. 动手前的网络与系统规划2.1 主机、网段、软件版本的三张表跳过规划直接敲命令是部署踩坑最容易发生的地方。我给你一份可以直接抄的三张表这只是最小示例你可以按自己环境调整。第一张是主机规划表。建议至少两台机器一台做控制平面另外一两台做工作节点配置尽量不低于2核4GB否则kubelet和系统组件跑起来会很吃力。角色主机名IP地址配置控制平面master01192.168.50.1014核8GB工作节点node01192.168.50.1024核8GB工作节点node02192.168.50.1034核8GB第二张是网段规划表。这组参数必须在初始化之前定好因为网络插件安装后不能随便改改错了只能重建集群。网段类型取值备注Pod网段10.244.0.0/16与flannel默认值一致Service网段10.96.0.0/12kubeadm默认值宿主机网段192.168.50.0/24避免与Pod/Service网段重叠第三张是软件版本表。我这次用的是Ubuntu 22.04 LTS容器运行时用containerdKubernetes用1.28系列网络插件用flannel或calico二选一。版本固定很重要因为Kubernetes组件之间对于版本差异比较敏感尤其是kubeadm、kubelet、kubectl三个二进制建议保持完全相同版本。2.2 系统层必须处理的三件事swap、cgroup、内核模块在安装k8s之前每台Ubuntu机器都建议执行同一套系统初始化。这套操作是被问得最多的也是网上教程容易一笔带过的地方。# 关闭swap sudo swapoff -a sudo sed -i / swap / s/^/#/ /etc/fstab # 加载内核模块 cat EOF | sudo tee /etc/modules-load.d/k8s.conf overlay br_netfilter EOF sudo modprobe overlay sudo modprobe br_netfilter # 调整内核参数 cat EOF | sudo 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 sudo sysctl --system为什么必须关闭swap因为kubelet在远古版本就对swap有明确限制认为swap会让Pod的内存隔离失去可靠性。虽然新版本允许通过配置保留swap但对新手来说最省事的做法就是全部关掉。如果你跳过这一步kubelet会反复报错节点永远无法Ready。为什么cgroup驱动必须统一这是很多老手都会踩的坑。Kubernetes的容器运行时和kubelet都依赖cgroup来限制Pod的资源使用。在Ubuntu上systemd是1号进程它本身也在管理cgroup如果containerd用cgroupfs驱动、kubelet用systemd驱动就会出现两套cgroup管理机制互相干扰。所以推荐统一到systemd驱动具体配置我在下一节说。关于overlay和br_netfilter这两个内核模块第一个是为了让containerd能以overlayfs的方式高效管理镜像层第二个是为了让Linux网桥上的流量能被iptables规则处理。如果Bridge netfilter不开启Pod和Service的通信会出现奇怪的路由现象比如某些Pod能通、某些不通。2.3 hosts与nameserver的坑在所有节点上把hosts文件写好是很多教程默认你“已经会了”的步骤。实际部署中这一步漏掉或者写错会让集群出现很有迷惑性的报错。sudo vi /etc/hosts 192.168.50.101 master01 192.168.50.102 node01 192.168.50.103 node02hosts文件为什么这么关键Kubernetes的组件之间经常用主机名互相访问apiserver连接kubelet时也会走主机名。如果DNS解析不到或者解析出来的IP不对就会触发证书校验失败一类的错误。证书里签了master01这个名字解析结果却变成另一个IP握手自然过不去。另外不要在每台机器上乱改/etc/resolv.conf。kubelet在创建Pod时会依赖集群内部的CoreDNS做服务发现宿主机nameserver只负责解析外部域名。如果你把宿主机DNS指向一个不存在的地址集群内部Pod拉取镜像或者访问外部服务都会失败而且你很难第一时间想到是DNS的问题。3. 从零到master的部署实操3.1 容器运行时安装与配置containerdKubernetes从1.24版本开始移除了对Docker的直接内置支持底层用符合CRI接口的容器运行时。最通用也最轻量的选择是containerd。安装步骤非常简单sudo apt update sudo apt install -y containerd sudo mkdir -p /etc/containerd containerd config default | sudo tee /etc/containerd/config.toml生成默认配置后必须检查一个关键项SystemdCgroup。打开/etc/containerd/config.toml找到类似下面这一段[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup false把它改成true然后重启containerdsudo sed -i s/SystemdCgroup false/SystemdCgroup true/ /etc/containerd/config.toml sudo systemctl restart containerd sudo systemctl status containerd这里如果不改kubelet启动后会报告cgroup驱动不一致节点就一直卡在NotReady。另外如果你的网络环境无法直接访问外部镜像仓库还得顺手修改配置里的sandbox_image把pause容器镜像换成可访问的地址否则后续初始化的关键镜像拉取阶段就会卡死。这个细节看起来不起眼但至少有一半的新手第一次部署都栽在这。3.2 安装kubeadm/kubelet/kubectl并锁定版本接下来安装三个核心二进制。先添加官方APT源再安装并锁定版本。sudo apt-get install -y apt-transport-https ca-certificates curl gpg sudo mkdir -p /etc/apt/keyrings curl -fsSL https://pkgs.k8s.io/core:/stable:/v1.28/deb/Release.key | sudo gpg --dearmor -o /etc/apt/keyrings/kubernetes-apt-keyring.gpg echo deb [signed-by/etc/apt/keyrings/kubernetes-apt-keyring.gpg] https://pkgs.k8s.io/core:/stable:/v1.28/deb/ / | sudo tee /etc/apt/sources.list.d/kubernetes.list sudo apt-get update sudo apt-get install -y kubelet kubeadm kubectl sudo apt-mark hold kubelet kubeadm kubectl注意现在使用的是官方声明的独立APT仓库而不是以前混杂在系统源里的旧方案。安装完立刻hold住版本防止系统升级时自动更新。为什么必须锁定Kubernetes的版本升级通常伴随API变更控制平面和节点之间的版本兼容范围也有限制一旦某台机器被自动升级到新版本而其他机器还停留在旧版本整个集群可能瞬间失效。安装完成后分别执行kubeadm version和kubectl version确认三个二进制版本完全一致。这一步花不了十秒钟但能省掉后面一大半的玄学问题。3.3 生成kubeadm初始化配置并完成master初始化很多人直接用kubeadm init命令加参数初始化这样不是不行但可复用性很差。我建议先导出一份初始化模板再改这样以后重建集群或者加第二个控制平面节点都有一份可追溯的配置。kubeadm config print init-defaults kubeadm-config.yaml打开这份文件至少需要修改三个地方。第一是localAPIEndpoint的advertiseAddress改成控制平面节点的真实IP填127.0.0.1会让你之后的工作节点根本连不上apiserver。第二是nodeRegistration的criSocket默认虽然是/var/run/containerd/containerd.sock但某些系统里路径不同建议显式写为/run/containerd/containerd.sock。第三是networking的podSubnet填10.244.0.0/16和flannel默认网段保持一致。准备一份最简配置文件apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.50.101 bindPort: 6443 nodeRegistration: criSocket: /run/containerd/containerd.sock name: master01 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: 1.28.2 networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12然后执行初始化sudo kubeadm init --config kubeadm-config.yaml初始化的日志很长我会重点关注最后几行。如果看到“Your Kubernetes control-plane has initialized successfully”基本就成功了大半。接下来按照提示执行这几条让当前用户可以使用kubectlmkdir -p $HOME/.kube sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config sudo chown $(id -u):$(id -g) $HOME/.kube/config初始化输出里还有一段kubeadm join命令这是工作节点入集群的钥匙建议立刻复制保存到一个临时文件里。如果忘了后面可以用kubeadm token create重新生成。3.4 两个最容易忽略的初始化参数很多教程会把kubeadm init的完整参数表丢给你但我觉得最值得单独拿出来讲的是两个高频出错点。第一个是apiserver的advertiseAddress和Pod网段的配合。advertiseAddress必须是控制平面节点上真实存在的网卡IP而且最好是静态IP。如果你在DHCP环境里部署IP一变apiserver的证书和访问地址全部作废整个控制平面就变成了一座孤岛。多网卡机器还需要在初始化时通过--apiserver-advertise-address显式指定否则kubeadm可能选错网卡导致节点之间连不上。第二个是controlPlaneEndpoint。单控制平面集群不用填但如果你规划未来要加多个控制平面或者前面有一台负载均衡器最好在初始化时就把它写上。controlPlaneEndpoint会作为统一入口写进证书之后再改非常麻烦。我的经验是哪怕现在只有一台控制平面只要目录里已经有负载均衡的规划就先把域名或VIP填好不改比改容易。初始化失败也别慌kubeadm reset可以清理现场。但要注意reset清掉的是通过kubeadm生成的内容之前手动写的系统配置和网络插件要自己检查。4. 网络插件选型与安装顺序4.1 flannel与calico的核心取舍控制平面初始化完成后集群还是“瘫痪”的因为Pod之间没有互通网络。这一步由CNI网络插件完成。你在社区里最常看到的就是flannel和calico这两个名字它们不是二选一的随机选择而是两种不同设计哲学。维度flannelcalico数据面VXLAN/UDP隧道为主BGP路由为主支持多种模式NetworkPolicy不支持支持完整策略推荐规模中小型集群中大型集群运维门槛低几乎零配置高需要理解BGP和IP池资源占用更少相对更多flannel的思路是把所有容器的网络包塞进VXLAN隧道里实现跨节点通信。它简单、稳定、资源消耗小但如果你想限制某个命名空间之间的访问它做不了因为NetworkPolicy需要额外组件支持。calico则用BGP协议在节点之间直接交换路由网络转发路径更短性能更好还内置了NetworkPolicy能力。代价是概念更多配置更复杂出问题时排查链路过深。对于第一次在Ubuntu上部署k8s的读者我的建议很明确先选flannel。因为它能让你以最小代价把集群跑通当你理解了“节点NotReady”和“网络插件未生效”之间的关系以后再考虑换calico不迟。如果从一开始就明确需要网络隔离策略那直接上calico。4.2 安装网络插件时的CIDR对齐问题这里有个隐藏很深的坑网络插件的默认网段和初始化时的Pod网段必须完全一致。flannel默认使用10.244.0.0/16所以如果你在kubeadm init时配置了同样的podSubnet安装flannel几乎不会遇到网段冲突。安装命令通常是kubectl apply -f https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml但如果你在初始化时把Pod网段改成了其他值就要修改flannel清单里的net-conf.json配置把Network改成一模一样的网段然后再apply。否则flannel管理的是10.244.0.0/16而kubelet分给Pod的是另一个网段Pod能创建但无法通信现象非常诡异。calico的默认IP池则是192.168.0.0/16如果你不想用这个网段要么在初始化时把podSubnet改成192.168.0.0/16要么安装calico后修改IPPool资源再apply。我的习惯是先决定网络插件再决定Pod网段最后写初始化配置。顺序反了就只能重新reset集群。4.3 网络插件没装好时的排查思路安装完网络插件后最常见的现象有三个coredns一直Pending、coredns疯狂重启、节点仍然NotReady。这三个本质上都是同一个问题网络插件没有正常工作。排查时不要急着瞎猜。先执行kubectl get pods -n kube-system -o wide看看coredns和flannel的Pod是什么状态。如果flannel Pod是CrashLoopBackOff马上看日志kubectl logs -n kube-system 。日志里最常见的报错是网段冲突、缺少权限、无法读取kubelet的CNI配置。如果flannel Pod是Running但节点还是NotReady建议在节点上检查CNI配置目录是否存在。Kubelet在创建Pod时会到/etc/cni/net.d里找网络配置如果这个目录是空的或者权限不对Pod始终无法获得IP节点自然也就Ready不了。还有一种容易被忽略的情况Node的kubernetes.io/os标签或提供器的污点不匹配导致flannel Pod被调度到其他节点上这个要结合kubectl describe node来看。5. 工作节点加入与集群验证5.1 生成并保留join token的正确姿势集群的网络通起来之后接下来就是把工作节点拉进集群。工作节点上其实不需要也不应该运行kubectl你只需要在master上生成一条join命令然后在工作节点上以root权限执行。如果你把初始化时的join输出弄丢了可以用下面的命令重新生成kubeadm token create --print-join-command这条命令会输出类似这样的内容kubeadm join 192.168.50.101:6443 --token abc123.abc123abc123 --discovery-token-ca-cert-hash sha256:xxxxxxxxxxxxxxxx执行之前先保证工作节点已经完成系统初始化关闭swap、加载内核模块、安装containerd并配置SystemdCgroup、安装kubeadm和kubelet。这些步骤在master上怎么做的节点上就怎么做一条都不能少。然后执行join命令。成功的话控制台会显示“This node has joined the cluster”。如果显示错误先检查token是否过期、ca-cert-hash是否匹配、以及控制平面IP是否可达。token默认有效期是24小时过期了就重新生成一条不要凭记忆手工拼写。5.2 节点加入后如何快速确认状态回到master节点执行kubectl get nodes正常情况下一两个节点会先显示NotReady再等几十秒变成Ready。这中间kubelet要完成启动、连接apiserver、注册节点、初始化CNI插件有一个不算短的过程。如果需要更详细的信息用kubectl get nodes -o wide查看节点IP、系统信息、容器运行时版本。如果想看每个节点上运行了哪些系统组件用kubectl get pods -n kube-system -o wide。注意componentstatuses这个命令在最新版本里基本不可靠不要再依赖它。如果节点超过几分钟还是NotReady别急着把节点删了重加。按照“kubectl describe node 节点名”里的Conditions看KubeletReady是不是FalseFalse就说明kubelet进程本身有问题日志才是定位的关键。5.3 运行一个demo负载验证数据面集群组建完毕我们用一个最小负载验证控制平面和数据面是否真的健康。kubectl create deployment nginx-demo --imagenginx:1.25 kubectl expose deployment nginx-demo --typeNodePort --port80 kubectl get svc nginx-demo拿到NodePort端口后在任意节点上访问curl http://192.168.50.102:nodeport能返回nginx默认页面说明Service转发链路是通的。如果你想把验证做深一点再起一个临时Pod用它的shell去curl nginx-demo Pod的IPkubectl run test --imagebusybox --rm -it --restartNever -- sh wget -O- http://pod-ip:80这一步能通说明跨节点的Pod网络真正工作正常。如果到这里通不了那问题大概率不是Service层而是CNI的转发规则有遗漏。6. 预判常见故障我的排查链路与修复方案6.1 镜像拉取失败与重新打tag镜像拉取是部署k8s时最大的拦路虎。控制平面组件、pause容器、coredns、网络插件每个都要从远端仓库拉镜像。如果你的网络环境访问不了默认镜像仓库初始化阶段就会看到“failed to pull image”之类的错误。最干净的解法是在初始化时换一个可访问的镜像仓库sudo kubeadm init --config kubeadm-config.yaml --image-repository你的可用仓库地址kubeadm会从这个仓库拉取列表里的所有官方镜像。如果初始化已经失败先执行kubeadm reset再用新配置重新init。还有一个更灵活的办法从别处提前拿到镜像然后把它们重新打上本地标签。比如我们已经有了一个nginx镜像但缺少coredns镜像就先把源镜像pull下来再用docker tag或ctr i tag命令改成kubeadm期望的镜像名。这种方法适合离线环境或者内网镜像仓库也是很多公司在生产环境里的标准做法。6.2 cgroup driver不一致导致kubelet无法启动这条我几乎每次部署都会碰到所以单独拿出来说。如果你把containerd的SystemdCgroup保持默认false而kubelet使用的是systemd驱动kubelet启动时会直接报错退出。典型日志Failed to run kubelet: failed to validate kubelet flags: cgroup driver cgroupfs is different from systemd修复方法就是前面第3.1节提到的把containerd配置里的SystemdCgroup改为true重启containerd。改完以后不要忘记到工作节点上做同样的事因为节点的kubelet同样会校验。验证方法很简单在节点上执行kubelet --version确认组件已装好然后查看/var/log/syslog里的kubelet日志如果不再报错说明驱动已经统一。如果集群已经初始化再改containerd配置建议对节点执行kubeadm reset后重新加入否则kubelet可能还残留旧状态反而更难排查。6.3 节点NotReady的通用排查顺序节点NotReady是最常见的“模糊故障”因为它的原因太多了。我总结一条固定的排查链路按照这个顺序走通常十分钟内能定位。第一步看进程。在目标节点执行systemctl status kubelet如果服务本身没起来直接journalctl -u kubelet -f看实时日志。第二步看事件。回到master执行kubectl describe node 节点名重点看Conditions和Events段。事件里通常会直接告诉你原因比如“NetworkUnavailable”。这是最值得相信的信息来源比自己猜要高效得多。第三步看CNI。如果一个节点Ready后网络又消失问题基本都在CNI插件上。检查/opt/cni/bin目录下有没有可执行文件检查/etc/cni/net.d目录下有没有配置文件再看网络插件Pod的日志。第四步看证书和权限。Kubelet注册节点需要读取/etc/kubernetes/kubelet.conf如果这个文件缺失或者里面记录的集群地址过期节点会反复报错。此时最快的办法是把节点重置后再加入一次。我这里有一个小习惯不在NotReady的节点上反复改配置文件而是先看事件和日志带着证据再动手。因为NotReady的原因七成在kubelet与CNI的交互层两成在证书和token剩下的才是配置文件的锅。先诊断后处理步骤越少越不容易把集群弄得更坏。最后分享一点我自己的实操体会部署k8s的过程中最大的成本不是命令本身而是“理解每一步为什么这样做”。所以在初始化前我会把kubeadm-config.yaml按需求整理好几份模板比如单控制平面模板、高可用模板、calico网段模板需要哪个直接改IP就能用。初始化完成后会在所有节点上拍一个系统级快照这样如果后面网络插件或者集群配置出现无法收拾的局面直接回滚到可用状态比在坏掉的集群里反复排查要痛快得多。