本文基于一次完整的 K8s v1.35 集群部署实践,梳理从私有镜像仓库、节点初始化、容器运行时到集群网络的全链路流程,并穿插每个环节背后的设计原理与个人理解。希望能帮你少走弯路。
一、为什么要"从零"搭集群?
现在云厂商都提供托管 K8s(EKS、ACK、GKE),点几下就能用。但在以下场景里,自建集群仍然不可替代:
- 离线 / 内网环境:生产机房不能直连公网,所有镜像和 RPM 包都要本地化;
- 成本控制:小规模测试集群自己用虚拟机搭,比托管服务便宜得多;
- 学习与排障:只有亲手把每个组件装一遍,才能真正理解
NotReady、CrashLoopBackOff背后到底发生了什么。
本次部署的整体架构是1 个 Harbor 私有仓库节点 + 1 个 Master 控制节点 + 2 个 Worker 工作节点,操作系统为 RHEL 9,K8s 版本 v1.35.7。
| 主机名 | IP | 配置 | 角色 |
|---|---|---|---|
| harbor reg.timinglee.org | 172.25.254.250 | 1C1G | Harbor 镜像仓库 |
| k8s-master | 172.25.254.100 | 4C4G+ | 控制平面(control-plane) |
| k8s-node1 | 172.25.254.10 | 2C2G | 工作节点 |
| k8s-node2 | 172.25.254.20 | 2C2G | 工作节点 |
下面按实际部署顺序逐层展开。
二、第一步:搭建 Harbor 私有镜像仓库
2.1 为什么需要私有仓库?
K8s 集群启动时需要拉取大量镜像:kube-apiserver、etcd、coredns、pause……这些镜像默认来自registry.k8s.io,在国内网络环境下要么极慢要么直接超时。把所有镜像提前推到内网 Harbor,集群节点从本地拉取,速度快、可离线、可审计,这是企业级部署的标准做法。
2.2 本地化 Docker YUM 源
Harbor 本身依赖 Docker,而内网环境连 Docker 官方源都访问不了。这里用了一个巧妙的办法:
- 在 Harbor 节点上用
dnf install docker-ce --downloadonly把所有 RPM 包下载到本地; - 用
createrepo把这些包做成一个本地 YUM 仓库; - 用
httpd把仓库目录通过 HTTP 暴露出去(监听 4444 端口); - 其他节点把
baseurl指向http://172.25.254.250:4444/docker即可安装。
个人理解:这一步本质上是"把公网资源快照到内网"。在完全离线的生产环境中,不仅 Docker,K8s、操作系统补丁、甚至 Python 包都需要做类似的本地化镜像。
createrepo+httpd是最简单可靠的方案。
2.3 Docker 安装与内核参数调优
安装 Docker 后,必须做几个关键的内核配置:
# 加载桥接网络过滤模块echobr_netfilter>/etc/modules-load.d/docker_mod.conf modprobe-abr_netfilter# 开启桥接流量经过 iptables、开启 IP 转发cat>/etc/sysctl.d/docker.conf<<EOF net.bridge.bridge-nf-call-iptables = 1 net.bridge.bridge-nf-call-ip6tables = 1 net.ipv4.ip_forward = 1 EOFsysctl--system原理说明:
br_netfilter模块让 Linux 网桥(bridge)上的流量也能被 iptables 规则处理。K8s 的 Service 转发、NetworkPolicy 都依赖这个能力,不加载的话 Pod 网络会异常;net.ipv4.ip_forward = 1开启内核 IP 转发,这是容器跨节点通信、SNAT 出网的基础;- Docker 启动参数加
--iptables=true,让 Docker 自己管理 iptables 规则,保证容器能访问外网。
2.4 Harbor 安装与 HTTPS 证书
Harbor 离线包解压后,核心是编辑harbor.yml:
hostname:reg.timinglee.orgcertificate:/data/certs/timinglee.org.crtprivate_key:/data/certs/timinglee.org.keyharbor_admin_password:lee证书用openssl自签名生成,注意必须加subjectAltName = DNS:reg.timinglee.org,否则 Docker 客户端会因为证书 SAN 不匹配而拒绝登录。
踩坑提醒:自签名证书必须分发到所有需要登录 Harbor 的节点,放到
/etc/docker/certs.d/reg.timinglee.org/ca.crt,同时建议也放进系统信任库/etc/pki/ca-trust/source/anchors/并执行update-ca-trust extract。只放 Docker 目录、不放系统信任库,某些工具(如cri-dockerd)仍然会报证书错误。
Harbor 用docker compose启动,为了开机自启,写了一个 systemd service 包裹docker compose up -d,Type=oneshot+RemainAfterExit=yes是这种"命令型服务"的标准写法。
三、第二步:所有节点的基础环境准备
这一步是最容易被忽视、但出问题最多的环节。
3.1 关闭 Swap
systemctl disable--nowswap.target systemctl mask swap.targetsed'/swap/s/^/#/g'-i/etc/fstab为什么必须关 Swap?K8s 的 kubelet 默认要求关闭交换分区。原因是:
- K8s 通过
requests和limits做资源保证,Pod 的内存使用是可预测的; - 如果开启 Swap,内核可能把 Pod 内存换出到磁盘,导致性能严重下降,且 QoS 等级(Guaranteed/Burstable/BestEffort)的语义被破坏;
- 虽然 1.22 之后引入了
NodeSwap特性门控支持有限的 Swap,但生产环境仍然建议直接关闭。
验证:swapon -s无任何输出即为成功。
3.2 主机名解析与时间同步
所有节点的/etc/hosts都要写上彼此的主机名和 IP:
172.25.254.100 k8s-master 172.25.254.10 k8s-node1 172.25.254.20 k8s-node2 172.25.254.250 reg.timinglee.org个人理解:K8s 组件之间大量通过主机名或证书 CN 互相通信,
/etc/hosts是最朴素但最可靠的 DNS 方案。生产环境建议用内部 DNS 服务器,但实验环境/etc/hosts足够。另外别忘了配置 NTP 时间同步,etcd 对时钟偏移非常敏感。
3.3 Docker 加速器与 Harbor 证书分发
在所有节点上配置daemon.json:
{"registry-mirrors":["https://reg.timinglee.org"]}这里把 Harbor 设为 Docker 的镜像加速器。这样即使docker pull nginx没有写完整仓库地址,Docker 也会优先从 Harbor 拉取。配合 Harbor 的代理缓存功能,可以实现"首次拉取走公网、后续走本地"的透明加速。
四、第三步:容器运行时 cri-dockerd
这是整个部署中最需要理解原理的一环。
4.1 为什么是 cri-dockerd 而不是直接用 Docker?
K8s 在v1.24 版本正式移除了 dockershim。在此之前,kubelet 内部有一个叫 dockershim 的组件,专门把 CRI(Container Runtime Interface)调用翻译成 Docker API。移除后,kubelet 不再原生支持 Docker。
但很多团队已经深度依赖 Docker(镜像构建、调试工具、私有仓库生态),不想切换到 containerd。于是 Mirantis 公司把 dockershim 的代码抽出来,维护成了独立项目cri-dockerd。
它的工作模式是:
kubelet ←—CRI(gRPC)—→ cri-dockerd ←—Docker API—→ dockerd ←—containerd—→ runccri-dockerd 作为一个"翻译层",对外暴露标准 CRI 接口,对内调用 Docker API。这样 kubelet 以为自己在跟一个标准 CRI 运行时通信,而实际跑容器的还是 Docker。
4.2 两种安装方式
- RPM 包安装:简单,但版本较老(0.3.14);
- 二进制包安装:从 GitHub 下载最新版(0.4.4),手动
install到/usr/local/bin,再复制 systemd 单元文件。
生产环境建议用二进制包,版本新、bug 少。
4.3 关键启动参数
ExecStart=/usr/local/bin/cri-dockerd \ --network-plugin=cni \ --pod-infra-container-image=reg.timinglee.org/k8s/pause:3.10.1 \ --container-runtime-endpoint fd://逐个解释:
--network-plugin=cni:使用 CNI 网络插件,这是 K8s 集群网络的标准;--pod-infra-container-image:指定 Pod 基础设施容器(pause 容器)的镜像。每个 Pod 启动时都会先跑一个 pause 容器来持有网络命名空间,这个镜像必须从私有仓库拉取;--container-runtime-endpoint fd://:通过 systemd 传递的 socket 文件描述符监听 CRI 请求。
启动后验证:ls /var/run/cri-dockerd.sock存在即正常。
个人理解:cri-dockerd 是一个"历史包袱兼容层"。如果是全新部署,我更推荐直接用containerd作为运行时——它更轻量、性能更好、是 K8s 官方推荐的默认运行时。但如果你的团队已经有大量 Docker 镜像构建脚本和运维习惯,cri-dockerd 是平滑过渡的最佳选择。
五、第四步:kubeadm 初始化集群
5.1 安装 K8s 组件
Master 节点安装kubelet、kubeadm、kubectl;Worker 节点只需要kubelet、kubeadm(kubectl 是客户端工具,Worker 上不需要)。
kubelet安装后要systemctl enable --now,但注意:在集群初始化之前,kubelet 会不断重启并报错,这是正常的——因为它还没有拿到 kubeconfig 配置文件。kubeadm init会生成配置并写入/var/lib/kubelet/,之后 kubelet 才会正常运行。
5.2 预拉取集群镜像
kubeadm config images list# 查看需要哪些镜像kubeadm config images pull\--image-repository registry.aliyuncs.com/google_containers\--kubernetes-version v1.35.7\--cri-socket=unix:///var/run/cri-dockerd.sock需要的镜像包括:
kube-apiserver:集群 API 入口,所有操作都经过它;kube-controller-manager:运行各种控制器(Deployment、ReplicaSet 等);kube-scheduler:Pod 调度决策;kube-proxy:Service 网络代理,每个节点都有;etcd:分布式键值存储,保存集群所有状态;coredns:集群内部 DNS;pause:Pod 基础设施容器。
从阿里云镜像源拉取后,用docker tag改名再docker push到 Harbor,这样后续所有节点都能从本地仓库拉取。
5.3 初始化 Master
kubeadm init\--pod-network-cidr=10.244.0.0/16\--image-repository reg.timinglee.org/k8s\--kubernetes-version v1.35.7\--cri-socket=unix:///var/run/cri-dockerd.sock参数说明:
--pod-network-cidr=10.244.0.0/16:指定 Pod 网络的 CIDR 段。这个值必须和后续安装的网络插件(Flannel 默认就是 10.244.0.0/16)一致,否则会出现 Pod 跨节点不通;--image-repository:指定从哪个仓库拉取控制平面镜像;--cri-socket:明确告诉 kubeadm 使用 cri-dockerd 的 socket,避免它自动检测到其他运行时。
kubeadm init 背后做了什么?这是理解 K8s 部署的关键:
- 预检查:检测内核版本、cgroup 驱动、Swap 是否关闭、端口是否占用等;
- 生成证书:在
/etc/kubernetes/pki/下生成 CA、apiserver、etcd 等所有组件的证书和密钥; - 生成 kubeconfig:为 kubelet、controller-manager、scheduler 生成连接 apiserver 的配置文件;
- 生成静态 Pod 清单:把 apiserver、controller-manager、scheduler、etcd 的 YAML 清单写到
/etc/kubernetes/manifests/,kubelet 会自动拉起这些静态 Pod; - 启动控制平面:等待 apiserver 就绪;
- 安装附加组件:通过 ConfigMap 配置 CoreDNS 和 kube-proxy;
- 生成 join 命令:输出带 token 和 CA 证书哈希的
kubeadm join命令。
初始化成功后,配置环境变量:
echo"export KUBECONFIG=/etc/kubernetes/admin.conf">>~/.bash_profilesource~/.bash_profile此时kubectl get nodes能看到 Master 节点,但状态是NotReady——因为还没装网络插件。
5.4 Worker 节点加入
kubeadmjoin172.25.254.100:6443\--tokenjl4ztx.cax3iysvu7onsh5s\--discovery-token-ca-cert-hash sha256:6b5950ef...\--cri-socket=unix:///var/run/cri-dockerd.sockjoin 的原理:
--token:一个短期有效的身份凭证,默认 24 小时过期。Worker 用它向 apiserver 证明自己是被允许加入的;--discovery-token-ca-cert-hash:Master 的 CA 证书哈希。Worker 用它验证连接到的 apiserver 是真实的、不是中间人伪造的;- join 成功后,Master 会为该节点签发证书,kubelet 开始正常工作。
如果 token 过期了,在 Master 上执行kubeadm token create --print-join-command可以重新生成。
如果初始化出错想重来,kubeadm reset --cri-socket=unix:///var/run/cri-dockerd.sock可以清理所有配置和证书。
六、第五步:安装网络插件 Flannel
6.1 为什么节点是 NotReady?
集群初始化后所有节点都是NotReady,这是因为K8s 本身不提供容器网络,它只定义了 CNI(Container Network Interface)标准,具体实现由第三方插件完成。没有网络插件,Pod 之间无法通信,节点就被标记为 NotReady。
常见的 CNI 插件有:
- Flannel:最简单,适合学习和小规模集群;
- Calico:功能强大,支持 NetworkPolicy,生产环境首选;
- Cilium:基于 eBPF,性能好、可观测性强,新兴方案。
本次用 Flannel,因为它配置最简单。
6.2 Flannel 的工作原理
Flannel 为每个节点分配一个子网(从10.244.0.0/16中划分),节点上的 Pod 都从该节点的子网中获取 IP。跨节点通信时,Flannel 通过VXLAN把 Pod 网络包封装到 UDP 包中,通过宿主机网络传输,到达目标节点后再解封装。
简单来说:
Pod A (10.244.1.5) → flannel.1 (VXLAN封装) → 宿主机网络 → flannel.1 (解封装) → Pod B (10.244.2.8)6.3 部署步骤
- 加载 Flannel 镜像到 Docker,tag 后推到 Harbor;
- 修改
kube-flannel.yml中的镜像地址,指向私有仓库; kubectl apply -f kube-flannel.yml。
Flannel 以DaemonSet形式部署,这意味着每个节点上都会自动运行一个 Flannel Pod,这正是网络插件需要的——每个节点都必须有网络代理。
部署后等待几十秒,kubectl get nodes所有节点状态变为Ready,集群就真正可用了。
七、进阶:用 Ansible 自动化部署
手动在 3 个节点上重复执行命令既繁琐又容易出错。文档最后给出了 Ansible 自动化方案,这是生产级部署的必经之路。
7.1 Ansible 的核心价值
- 幂等性:同一个 Playbook 执行多次结果一致,不会重复安装或重复配置;
- 声明式:描述"目标状态"而不是"执行步骤",Ansible 自己判断需不需要改;
- 批量执行:一次配置,所有节点同步生效。
7.2 Playbook 结构解析
整个 Playbook 按模块组织,每个 task 对应一个配置动作:
| Task | 模块 | 作用 |
|---|---|---|
| setup docker repo | yum_repository | 配置 Docker YUM 源 |
| install docker | dnf | 安装 Docker |
| setup docker.service | replace | 修改 Docker 启动参数 |
| setup docker registry | copy | 写入 daemon.json |
| setup docker certs | file | 创建证书目录 |
| cp certs file | copy+loop | 分发证书 |
| load module | copy+loop | 写入内核模块和 sysctl 配置 |
| stup cri-dockerd | copy+loop | 部署 cri-dockerd 二进制和 service |
| start services | service+loop | 启动并启用服务 |
几个值得注意的写法:
when条件判断:Master 和 Worker 安装的包不同,用when: inventory_hostname == "172.25.254.100"区分;loop循环:多个相似操作(如分发多个文件、启动多个服务)用循环精简代码;become提权:用普通用户devops登录,通过 sudo 提权到 root 执行,这是安全最佳实践;lineinfile:确保某一行存在于文件中,适合追加环境变量配置。
个人理解:Ansible Playbook 的编写思路是"把手动步骤翻译成声明式配置"。初学者容易犯的错是用
shell模块执行所有命令,这样就失去了幂等性。应该尽量用专用模块(yum、copy、service、lineinfile),只有模块覆盖不到的场景才用shell。
八、常见问题与排障思路
部署过程中最容易遇到的几个问题:
8.1 节点一直 NotReady
- 检查网络插件 Pod 是否正常运行:
kubectl get pods -n kube-flannel; - 检查 Pod 网络 CIDR 是否和 Flannel 配置一致;
- 查看 kubelet 日志:
journalctl -u kubelet -f。
8.2 kubeadm init 超时
- 通常是镜像拉不下来:检查
--image-repository是否正确、Harbor 是否可访问、证书是否分发; - 检查 cri-dockerd 是否正常运行:
systemctl status cri-docker; - 查看具体哪个静态 Pod 没起来:
crictl ps -a或docker ps -a。
8.3 Worker 节点 join 失败
- 检查网络连通性:
telnet 172.25.254.100 6443; - 检查 token 是否过期:
kubeadm token list; - 检查防火墙和 SELinux:生产环境建议统一关闭或正确配置。
8.4 Harbor 登录失败
- 检查证书 SAN 是否包含访问域名;
- 检查证书是否分发到客户端节点的
/etc/docker/certs.d/; - 检查
/etc/hosts域名解析是否正确; - 检查 Harbor 容器是否全部正常:
docker compose ps。
九、总结
回顾整个部署流程,K8s 集群的搭建可以归纳为五层架构:
┌─────────────────────────────────────────┐ │ 第五层:网络插件(Flannel / Calico) │ ← Pod 通信基础 ├─────────────────────────────────────────┤ │ 第四层:K8s 组件(kubeadm 初始化) │ ← 控制平面 + kubelet ├─────────────────────────────────────────┤ │ 第三层:容器运行时(cri-dockerd) │ ← CRI 接口适配 ├─────────────────────────────────────────┤ │ 第二层:容器引擎(Docker) │ ← 镜像与容器管理 ├─────────────────────────────────────────┤ │ 第一层:基础设施(内核参数 / Swap / 网络)│ ← 系统级准备 └─────────────────────────────────────────┘每一层都为上一层提供支撑,任何一层出问题,上面的所有层都无法正常工作。这也是为什么 K8s 部署看起来"步骤很多"——因为它把一个复杂的分布式系统拆解成了清晰的层次。
最后,关于部署方式的建议:
- 学习实验:手动部署一遍,理解每个组件的作用;
- 小规模生产:用 Ansible Playbook 自动化,保证一致性;
- 中大规模生产:考虑用 Kubespray(基于 Ansible 的成熟方案)或 kOps;
- 超大规模 / 多云:托管 K8s 服务仍然是最省心的选择。
希望这篇文章能帮你建立起对 K8s 集群部署的整体认知。动手搭一遍,你会发现那些曾经看起来高深莫测的概念,其实都有清晰的逻辑和实现路径。
本文基于 K8s v1.35.7 + cri-dockerd 0.4.4 + Harbor v2.5.4 + Flannel v0.28.9 实践整理。