Kubeadm部署Kubernetes 1.21.1实践:从环境初始化到集群排错
1. 为什么还在折腾 kubeadm说一说 V1.21.1 这个版本先说个多数人容易忽略的事实Kubernetes 官方一直把 kubeadm 定位成“生产可用的集群安装工具”而不是只给学习环境用的玩具。哪怕到了今天很多生产集群的初始化也依然走的是 kubeadm区别只是前面套了一层自动化封装而已。我这次选择 V1.21.1一方面是因为项目正好要跑在这个版本上另一方面也是想验证一下 kubeadm 在跨小版本升级路径上的表现——1.21 的 API 兼容性做得不错像 StatefulSet、Deployment 这类常用资源都处于稳定版本后续升级到 1.22、1.23 的阻力相对可控。为什么单独写部署过程因为网上关于 kubeadm 的教程大多针对旧版本拿 1.18、1.19 的配置直接套到 1.21 上会出各种莫名其妙的问题。最常见的例子是 1.20 之后 kubeadm 默认不再使用 Docker 作为运行时依赖而是推荐 containerd同时 etcd 的版本、CoreDNS 的配置方式都有调整。如果你还是照搬老教程里“装完 Docker 再 init”那套逻辑多半会在 kubeadm init 阶段遇到 kubelet 起不来的问题。这一篇我把从零开始用 kubeadm 部署 V1.21.1 的完整链路捋一遍包括环境初始化、运行时选型、master 节点初始化、worker 节点加入、网络插件安装、常见排错。适合已经有一点 Linux 和容器基础、想亲手搭一套多节点集群的读者。如果你之前只玩过单节点的 minikube这套流程也能帮你把“集群是怎么把节点黏在一起的”这件事彻底搞明白。1.1 Kubeadm 在 V1.21.1 中解决的“一致性”问题kubeadm 的价值在于把很多容易出错的分散步骤变成了可验证的清单。从底层看它做的事包括生成证书和 kubeconfig、初始化 control plane 组件kube-apiserver、kube-controller-manager、kube-scheduler、启动 etcd然后通过 DaemonSet 的形式把 kube-proxy 等组件装好。V1.21.1 新增了几个值得注意的点比如kubeadm init --dry-run可以提前输出将要执行的动作方便你检查参数另外 kubeadm 对 kubelet 配置的同步机制更严格如果你在kubeadm init之前手动改过/etc/kubernetes/kubelet.conf很可能因为哈希不一致直接被拒绝。还有一点是关于“每个节点上的 kubelet 版本必须和 kubeadm 版本匹配”的提示。在实际操作中我建议所有节点安装同一个版本的 kubelet、kubeadm 和 kubectl也就是 1.21.1。这样做不是为了省事而是避免出现 node 版本偏差导致 kubelet 向 apiserver 上报的 version 信息不兼容进而引发调度异常。很多人初始化失败排到最后才发现是版本不一致。1.2 为什么不用云厂商托管集群有人肯定会问直接买托管集群不香吗香但你要分场景。如果只是跑业务托管集群确实省心可如果你面临的是内网隔离环境、特定合规要求或者想深入理解 k8s 内部机制那么自己用 kubeadm 搭一套还是很有必要的。我这次折腾的节点就是模拟内网环境没有外网容器镜像仓库依赖需要提前把所有镜像导入到私有仓库。这个过程中暴露出的网络、DNS、镜像拉取策略问题都是云厂商托管集群不会让你碰到的。再说学习收益。kubeadm 部署能让你真正摸到/etc/kubernetes/manifests下的静态 Pod 定义理解 apiserver、controller-manager、scheduler 是怎么被 kubelet 拉起来的。这比看一百遍架构图都管用。2. 环境准备节点规划、系统初始化与运行时选型部署 k8s 集群不是装上 kubeadm 就直接 init 这么简单。我见过太多人忽略前置调优结果集群起来了过几天 kubelet 莫名其妙失联。所以环境准备这一步千万不能跳。2.1 节点规划与操作系统要求先交代一下本次部署的环境配置。我这边准备了三台机器一台作为 master两台作为 worker具体规格如下角色主机名配置系统masterk8s-master4C8G / 100G SSDCentOS 7.9worker1k8s-node14C8G / 100G SSDCentOS 7.9worker2k8s-node24C8G / 100G SSDCentOS 7.9这里的配置是够用的。V1.21.1 的 control plane 组件加 etcd2C4G 也能跑但生产环境我建议至少 4C8G因为后面还要装各种监控组件和网络插件。操作系统我用了 CentOS 7.9虽然已经是老版本了但兼容性最好。如果你是 Ubuntu 20.04 或 Debian 11流程类似只是包管理命令不同。三台机器的 hostname 必须提前设好且不能重复。集群内部通过 hostname 进行节点通信所以还要把所有节点的 IP 和 hostname 写入/etc/hosts。这一步很基础但很多人会漏导致 kubeadm join 时证书里的 IP 对不上。2.2 内核参数调整与防火墙处理Kubernetes 依赖 iptables 和网络桥接功能需要做一些内核参数调整。我在所有节点上执行了下面这些操作cat EOF /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第一项net.bridge.bridge-nf-call-iptables是为了让桥接的流量也能被 iptables 规则处理。如果你用的网络插件是 Calico 或者 Weave这个参数尤其重要不然跨节点 Pod 通信会直接不通。第二项net.ipv4.ip_forward必须为 1否则 kube-proxy 的转发功能会失效。还要把防火墙关掉。我知道有人喜欢不关防火墙用安全组规则来控制流量。但在内网测试环境我建议直接systemctl stop firewalld和systemctl disable firewalld省得排查网络问题时找不到方向。生产环境如果必须保留防火墙记得放行 6443、2379-2380、10250 等端口但一般在物理机层面做白名单会更合适。2.3 Swap 关闭与 cgroup 驱动选择这一步是很多 init 失败的根源。Kubernetes 默认不支持 Swap如果系统开启了 Swapkubelet 会拒绝启动。所以务必关闭swapoff -a sed -i / swap / s/^/#/ /etc/fstabswapoff立即生效改/etc/fstab是为了重启后不再自动挂载。这里提醒一句云服务器如果用了带磁盘的镜像可能没有 swap但物理机一般都会开所以这步不能省。接下来是 cgroup 驱动。V1.21.1 里 kubelet 和容器运行时的 cgroup 驱动必须保持一致。如果你用 systemd 作为 init 系统推荐 kubelet 和运行时都把cgroupDriver配置成systemd因为 systemd 作为 cgroup 管理器时如果再用 cgroupfs 会出现双重管理的问题。我在后面配置 containerd 时会显式写SystemdCgroup true这一步后面再说。2.4 容器运行时为什么我选了 containerdV1.21.x 时代 Docker 还活着但它已经不再是 kubelet 直接对话的运行时。kubelet 通过 CRIContainer Runtime Interface与运行时通信而 Docker 需要通过一个中间层dockershim来转换。从 1.20 开始 Kubernetes 社区就不断强调未来要移除 dockershim所以在 1.21.1 上直接使用 containerd 是更面向未来的选择。安装 containerd 的方式有很多种我这次用的是yum install containerd.io在 CentOS 上来自 docker 官方源。装完之后需要修改配置containerd config default /etc/containerd/config.toml然后编辑/etc/containerd/config.toml找到[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options]把SystemdCgroup设为true。另外因为环境内网无法直接访问外网镜像仓库我还在配置里把sandbox_image改成了内网镜像地址例如sandbox_image registry.example.com/pause:3.6。这一步如果不改kubelet 在创建 Pod 时会去拉k8s.gcr.io/pause:3.6在内网环境很容易失败。这个pause镜像虽然不起眼但它每个 Pod 都会用到是最基础的镜像。很多人只关注业务镜像提前导入忘了 pause 镜像结果 Pod 一直 ContainerCreating。还有一点CentOS 7 自带的是 iptables 1.4.21和 Calico 默认要求的较新特性可能存在兼容问题。如果你的网络插件选了 Calico建议把 iptables 升级到 1.6 以上或者干脆用 Flannel这个后面再对比。3. kubeadm init 主节点部署关键参数、网络插件与常见坑环境准备完毕接下来就是重头戏kubeadm init。这一步我看过无数人反复失败原因五花八门但大部分集中在上文说的 cgroup 驱动、镜像拉取、或者证书 IP 配置。3.1 提前拉取镜像与版本对齐在 init 之前先确认你安装的 kubeadm、kubelet、kubectl 版本都是 1.21.1。我用的安装方式是 yumcat EOF /etc/yum.repos.d/kubernetes.repo [kubernetes] nameKubernetes baseurlhttps://mirrors.example.com/kubernetes/yum/repos/kubernetes-el7-x86_64 enabled1 gpgcheck0 EOF yum install -y kubelet-1.21.1 kubeadm-1.21.1 kubectl-1.21.1 systemctl enable kubelet注意这里我用了自定义的镜像源地址大家根据自己网络环境替换。gpgcheck0是内网环境方便生产环境建议开。然后可以用kubeadm config images list查看需要的镜像列表kubeadm config images list它会列出类似k8s.gcr.io/kube-apiserver:v1.21.1、k8s.gcr.io/kube-controller-manager:v1.21.1、k8s.gcr.io/kube-scheduler:v1.21.1、k8s.gcr.io/kube-proxy:v1.21.1、k8s.gcr.io/pause:3.4.1、k8s.gcr.io/etcd:3.4.13-0、k8s.gcr.io/coredns:1.8.0等。你需要手动把这些镜像 pull 到本地然后 tag 成私有仓库地址推上去或者直接在 containerd 配置里设置registry.mirrors指向内部代理。推荐后者因为配置一次之后后来加入的 worker 节点也能复用。3.2 编写 kubeadm 初始化配置我不建议直接裸跑kubeadm init加上一长串参数因为可维护性太差。更好的做法是写一个配置文件比如kubeadm-config.yamlapiVersion: kubeadm.k8s.io/v1beta2 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.1.10 # master内网IP bindPort: 6443 nodeRegistration: criSocket: /run/containerd/containerd.sock name: k8s-master --- apiVersion: kubeadm.k8s.io/v1beta2 kind: ClusterConfiguration kubernetesVersion: v1.21.1 controlPlaneEndpoint: 192.168.1.10:6443 networking: podSubnet: 10.244.0.0/16 # 与网络插件匹配 serviceSubnet: 10.96.0.0/12 imageRepository: registry.example.com/k8s.gcr.io # 内网镜像仓库默认是 k8s.gcr.io --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd注意几个字段criSocket必须指向 containerd 的 socket也就是/run/containerd/containerd.sock而不是 Docker。imageRepository改成私有镜像仓库地址这里假设你已经把需要的镜像都打到了registry.example.com/k8s.gcr.io下面。podSubnet选10.244.0.0/16是 Flannel 默认网段。如果你计划用 Calico可以改成192.168.0.0/16。总之要和后面安装的网络插件要求的网段一致否则插件启动后路由规则错乱Pod 会互相 ping 不通。controlPlaneEndpoint需要写 master 的内网 IP或者如果有负载均衡器写负载均衡器的 VIP。这样做的好处是后续 worker 节点 join 时都走这个统一入口即使 master 挂了只要 VIP 能漂移集群控制面就不会中断。3.3 执行 init 并处理输出配置写好后执行kubeadm init --config kubeadm-config.yaml --upload-certs如果一切顺利会看到一段让你配置 kubectl 的提示。我一般直接按提示执行mkdir -p $HOME/.kube cp /etc/kubernetes/admin.conf $HOME/.kube/config然后把 admin.conf 拷贝到所有需要的管理机上。这里有个小经验admin.conf里的 server 地址默认是controlPlaneEndpoint如果你的管理机访问不到这个地址需要手动替换成实际可达的 IP。init 完成后还会输出一条kubeadm join命令里面包含 token 和 ca cert 哈希。务必把这行命令存下来worker 节点加入要用。如果忘记也没关系后面可以通过kubeadm token create --print-join-command重新生成。3.4 安装网络插件Flannel 还是 Calico容器网络是集群能工作的前提这一步不做所有 Pod 会一直 Pending 或 CrashLoopBackOff。我优先推荐根据你的网络环境选择插件Flannel简单基于 VXLAN 或 host-gw 模式适合规模不大、对性能要求不极致的场景。配置也简单一个 yaml 就搞定了。Calico支持网络策略BGP 路由模式性能更好但组件更多配置复杂一些。在 V1.21.1 上我这次选用的是 Flannel因为它的网络模式不需要额外依赖比如 BGP 支持只要保证net.bridge.bridge-nf-call-iptables1就能跑。安装 Flannelkubectl apply -f https://example.com/flannel.yaml注意 flannel 的配置文件里net-conf.json中的Network要和 kubeadm 的podSubnet一致。如果从网上下载的默认文件写的是10.244.0.0/16而你 kubeadm 里用了别的网段记得修改。装完之后你可以观察kubectl get pods -n kube-system会看到flannelPod 在陆续起来。这一步耗时一两分钟是正常的如果超过五分钟还没 Ready基本上就是网络问题我们第五节专门讲。3.5 将 worker 节点加入集群在 worker 节点上你需要做和 master 节点一样的准备工作关闭 swap、调整内核参数、安装 kubelet/kubeadm、配置 containerd。然后执行之前保存的 join 命令kubeadm join 192.168.1.10:6443 --token xxxx --discovery-token-ca-cert-hash sha256:xxxx如果 token 过期了可以到 master 上重新生成kubeadm token create --print-join-command加入后等一小会儿在 master 上执行kubectl get nodes能看到 worker 节点状态为Ready就成功了。4. worker 节点加入与验证从 token 到集群自检很多教程写到kubeadm join成功就结束了但实际上后面的验证部分才是决定集群是否真正可用的关键。我见过 join 成功、节点状态却是NotReady的情况这通常不是加入动作的问题而是网络或运行时配置的问题。4.1 token 管理与重新生成kubeadm 创建的 token 默认有效期是 24 小时如果你在第二天才想起来加节点之前的 token 肯定用不了。这时候直接执行kubeadm token create --ttl 0 --print-join-command--ttl 0表示 token 永不过期适合长期需要加节点的场景但生产环境不建议这么干应该用--ttl 2h这种临时有效的方式。另外还有一个容易踩的坑--discovery-token-ca-cert-hash的值是 master 的 CA 证书哈希。在之前保存的 join 命令里已经包含了这个值如果重新生成 token 后想用--print-join-command它也会自动带上。注意不要自己手动算直接使用输出即可。4.2 节点标签与角色确认节点加入后k8s 会默认给它一个空的角色标签。你可以通过给节点打标签来区分用途kubectl label node k8s-node1 node-role.kubernetes.io/workerworker不过这一步纯属方便管理不是必须。真正需要关注的是节点状态。4.3 集群自检清单下面是我在每次部署后必做的自检步骤大家可以照着过一遍节点状态kubectl get nodes确认所有节点都是Ready。核心组件kubectl get pods -n kube-system确认 coredns、kube-proxy、flannel 都处于Running且READY为 1/1。DNS 测试创建一个临时 Podkubectl run test-dns --imagebusybox --rm -it -- nslookup kubernetes.default能解析出 cluster IP 就说明 DNS 正常。跨节点通信在两个不同节点的 namespace 中分别创建 Pod互相 ping 一下。如果 ping 不通优先检查网络插件的路由规则和 iptables。存储卷挂载如果你的业务有 PVC随便创建一个 PVC 试试能否绑定到 PV。这一步不是必须的但提前做能避免后续部署应用时被卡住。我在实际测试中发现有时候corednsPod 会处于CrashLoopBackOff日志里会看到network not ready。这通常是因为 flannel 还没装好或者kubeproxy的 iptables 模式不对。需要多等一会儿如果一直不恢复请看下一节的排错思路。4.4 安装并访问 Dashboard可选虽然不是必须但很多人会想装个 Dashboard 来可视化查看集群状态。V1.21.1 对应的 Dashboard 版本建议选 v2.4.0 或者更新。安装命令一般是kubectl apply -f https://example.com/kubernetes-dashboard.yaml由于 Dashboard 默认只允许 HTTPS 访问并且需要 token 登录这里不展开细节。我只提醒一点Dashboard 是管理工具建议只在内网使用并且不要直接用kubectl proxy暴露到公网。5. 排错实战初始化失败、coredns pending、节点 NotReady 等问题链路这一节我把部署 V1.21.1 时最容易遇到的三类问题拿出来完整复盘一下排查思路而不是只丢出答案。因为下一次你遇到的报错可能不一样但排查链路是通用的。5.1 kubeadm init 卡在 “failed to pull image”这个问题典型原因是镜像仓库地址不可达。我遇到过一种情况imageRepository配置成了内网地址但pause镜像还在走默认的k8s.gcr.io导致总是卡在拉取 pause。排查链路先看kubeadm init输出中卡住的镜像名称。在节点上手动crictl pull 镜像测试是否能拉取。检查 containerd 的 registry mirrors 配置确认sandbox_image是否已经指向内网。如果网络没问题可能是镜像 tag 不对。V1.21.1 中 pause 默认是pause:3.4.1有的私有仓库导入时把 tag 搞丢了就会 404。解决建议提前在 worker 节点也把 pause 镜像拉好并且确认所有节点都有同样版本的 pause。这个问题可以避免大半。5.2 coredns 一直 Pending 或 CrashLoopBackOffcoredns Pending 通常是因为没有网络插件coredns 需要分配 Pod IP。你可以先检查kubectl describe pod -n kube-system coredns-xxx如果 Events 里提示Failed to create pod sandbox: rpc error: ... network plugin is not ready,那基本就是网络插件没起来。再看 CrashLoopBackOff日志常常是network is not ready。原因是 kubelet 在启动 Pod 前会检查网络是否就绪如果 CNI 插件不在就会一直重试。处理方法有两个维度确认你安装了网络插件Flannel 或 Calico。确认插件要求的 iptables 参数没问题。比如 Flannel 在启动时如果检测不到 bridge-nf-call-iptables1会直接报错退出。5.3 节点状态 NotReady 且 kubelet 报错比较常见的是 kubelet 日志里出现failed to get sandbox image k8s.gcr.io/pause:3.4.1: failed to pull image这就能顺藤摸瓜找到 containerd 的 sandbox_image 还没改或者镜像仓库里根本没有这个 tag。另一种可能是 kubelet 和 containerd 的 cgroup 驱动不一致启动后kubelet报了Failed to run kubelet ... cgroup driver cgroupfs is different from systemd修改KubeletConfiguration里的cgroupDriver: systemd同时确保 containerd 配置里SystemdCgroup true然后分别重启。注意改了 containerd 配置后必须重启 containerdsystemctl restart containerd重启 kubeletsystemctl restart kubelet假如节点依然 NotReady再看一下journalctl -u kubelet -f的日志。不要凭感觉猜直接看日志是最快的。5.4 Pod 之间通信失败这种情况网络插件装好了节点也是 Ready但 Pod IP 之间 ping 不通。我遇到的最常见原因是不同节点上的 Pod 网段冲突。Flannel 的 host-gw 模式要求所有节点在同一个二层网络里如果节点分布在不同网段host-gw 就不起作用了需要改用 VXLAN 模式或者直接用 Calico。非要给一个建议的话网络环境复杂的场景优先选 Calico 而不是 Flannel它的跨网段能力更强。还有一个隐蔽的坑有些节点有多个网卡Flannel 默认会挑选第一个网卡的 IP 作为节点 IP。如果选错了网卡那么它对外通告的 IP 就无法互通。解决办法是在 Flannel 的 DaemonSet 中添加环境变量env: - name: POD_NAME valueFrom: fieldRef: fieldPath: metadata.name - name: POD_NAMESPACE valueFrom: fieldRef: fieldPath: metadata.namespace - name: NODE_NAME valueFrom: fieldRef: fieldPath: spec.nodeName - name: FLANNEL_INTERFACE value: eth1 # 改成实际使用的网卡名6. 从部署到维护证书续期、备份与升级关注点集群搭起来只是开始真正考验人的是后续维护。V1.21.1 的证书有效期默认是一年很多人集群跑着跑着突然崩溃查到最后是证书过期了。所以在部署完成后我强烈建议你立刻把证书续期机制和备份方案做好。6.1 kubeadm 证书管理与自动续期kubeadm 的证书存放路径是/etc/kubernetes/pki。其中包括apiserver.crt、apiserver-kubelet-client.crt、front-proxy-client.crt等。你可以通过下面命令查看当前剩余时间kubeadm certs check-expirationV1.21.1 中如果你用的是kubeadm init且没有指定额外的证书管理工具那证书是不过期自动轮换的其实不是。kubeadm 只会在升级控制平面时自动更新证书不会每天自动续期。所以你需要一个定时任务或者手动定期执行kubeadm certs renew all如果只有一个 master执行完后需要手动更新admin.conf和 kubelet 的 kubeconfigkubeadm init phase kubeconfig admin --config kubeadm-config.yaml如果是多 master 高可用集群证书续期会更复杂建议换用 cert-manager 之类的方案或者在规划阶段就把证书有效期参数调整成长时间。6.2 etcd 备份是最容易被忽略的一环etcd 是集群状态的唯一来源一旦挂了整个集群就“失忆”了。我见过不少新手只备份了/etc/kubernetes却忘了 etcd 数据。备份 etcd 的简单方式ETCDCTL_API3 etcdctl --endpointshttps://127.0.0.1:2379 \ --cacert/etc/kubernetes/pki/etcd/ca.crt \ --cert/etc/kubernetes/pki/etcd/server.crt \ --key/etc/kubernetes/pki/etcd/server.key \ snapshot save /backup/etcd-snapshot.db恢复时要小心先停止 kube-apiserver再执行 snapshot restore最后重启 kubelet。不要在集群正常运行的时候随意恢复否则会把集群状态回滚到旧时间点。6.3 升级到更高版本时的留意点虽然这篇文章讲的是 V1.21.1 部署但很可能你在半年后就想升级。这里给你一个安全升级步骤的顺序参考先升级 kubeadmyum install kubeadm-next-version执行kubeadm upgrade plan它会提示你当前版本到目标版本之间的中间版本。然后升级 kubeletyum install kubelet-next-version。最后执行kubeadm upgrade apply。注意 Kubernetes 的版本跃迁不能跨太多比如从 1.21 直接升 1.24官方不一定支持。最好逐版本升级每次升级后确认集群健康。7. 最后再分享两个我踩过的细节第一个是镜像导入的问题。内网环境部署时我习惯把所有需要的镜像先导到一个清单文件然后用脚本批量拉取和推送。具体做法是在一台有外网的机器上执行kubeadm config images list然后循环docker pull再docker tag推送到私有仓库。这台中转机和目标集群不一定有联系只要保证目标集群的 containerd 配置了私有仓库的访问凭证即可。不要小看这一步很多团队在内网部署时都卡在镜像这里有的甚至连 pause 镜像都没有导入导致所有 Pod 一直 ContainerCreating。第二个是 kubelet 的--node-ip参数。如果你的机器有多个内网 IP比如有办公网和业务网kubelet 默认会选择默认路由对应的网卡 IP。但 k8s 集群节点之间通信可能希望走业务网这时候你需要在/etc/sysconfig/kubelet中设置KUBELET_EXTRA_ARGS--node-ip10.10.1.11然后重启 kubelet。如果不设节点注册到集群里的 IP 可能是错误的导致 kubectl exec、日志查看等操作全部失败。这个问题排查起来很隐蔽因为节点状态可能是 Ready但功能却不对。说了这么多其实最有用的经验就一条不要急于执行命令先把节点配置、镜像源、网络插件这三个基础打牢再走 kubeadm init。只要前置条件正确V1.21.1 的部署流程本身是相当丝滑的。这套方法我也用在了后来的 1.22、1.23 版本上步骤几乎没有变化唯一要留意的就是 kubeadm 的配置文件 API 版本可能跟着升级到时候记得更新你的kubeadm-config.yaml。