
前段时间团队让我负责搭建一套给内部开发和测试用的 Kubernetes 环境需求很直接机房是严格隔离的专网环境网络安全策略要求外部资源不可直接访问因此所有软件依赖、容器镜像、安装包都得提前准备好一次带入环境还要支持反复重建不能每次部署都靠人肉搭。这套集群要支撑好几个业务组的日常联调和验收稳定性要求不低但也不需要生产级别的高可用。说白了就是一句话离线可控、快速复现、能落地。这篇文章就是我从零开始做完这件事整理的完整思路和步骤。从离线资源包里放什么到私有镜像仓库怎么搭再到 kubeadm 初始化、工作节点接入、网络插件安装和后期问题排查基本把一条路走通会遇到的事情都写了一遍。如果你正在做类似的“内网环境建 K8s 集群”项目或者是运维、DevOps、后端开发可以参考这份流程直接套用。1. 整体设计与方案选型1.1 开发测试环境的真实诉求开发测试环境部署 Kubernetes和生产环境最大的区别在于你要的不是“极致稳定”而是“快速交付 随时重置”。业务同学今天要联调一个接口明天要验证一个新版本后天可能还要模拟故障场景所以集群最好能在半天内重新部署一遍。这就决定了方案选择上不能太复杂但也不能过于随意。我当时的诉求概括下来有四条支持多租户隔离至少能容纳 3 到 5 个业务组并行使用环境重建速度要快尽量脚本化、制品化减少手工操作所有软件来源可追溯版本清单要能拿得出给审计看不依赖外部网络网络插件、存储插件、常用中间件镜像都要提前备好这些诉求直接影响后面的每一个选择。如果只是图省事直接在每台机器上装 Docker 再手动起容器短期能用但长期会变成维护陷阱后面加节点、改网络、升级组件都会非常痛苦。所以从一开始就决定用标准 K8s 路线kubeadm 做集群初始化containerd 做容器运行时Calico 做网络插件镜像统一走私有仓库分发。1.2 离线部署的三大难点很多人觉得离线部署就是把安装包拖过去装一遍真正动手才发现坑比想象中多。我梳理下来离线部署 Kubernetes 的核心难点集中在三个方面第一是版本匹配。Kubernetes 生态里每个组件都有版本要求kubeadm、kubelet、kubectl 必须保持小版本一致containerd 和 K8s 之间的兼容性也有明确的版本矩阵etcd、coredns、pause 这些镜像版本在 kubeadm 初始化时会从默认仓库拉取版本不匹配就会直接初始化失败。第二是镜像关联。K8s 集群启动依赖一组系统镜像包括 kube-apiserver、kube-controller-manager、kube-scheduler、kube-proxy、pause、etcd、coredns。网络插件 Calico 又有自己的 cni、node、kube-controllers 等镜像。单独准备一个两个容易漏全量拉下来再一个个导入又容易乱。这个环节不处理干净后面全是 ImagePullBackOff。第三是配置一致性。离线环境下节点上的容器运行时如果不能从固定仓库拉镜像就无法工作。你需要提前把所有镜像推到私有仓库再让 containerd 的 registry mirror 指向它。同时 cgroup driver 必须统一为 systemd否则 kubelet 和 containerd 各管一套节点状态会一直 NotReady。1.3 技术选型与版本确定下面是我最终使用的版本组合这个组合经过生产环境验证稳定性不错也适合开发测试环境使用组件版本说明Kubernetesv1.28.2稳定版本社区生态成熟kubeadm / kubelet / kubectlv1.28.2三个工具必须同版本containerd1.7.11K8s 1.24 后默认运行时Calicov3.26.1网络插件 网络策略Registry2.8.3私有镜像仓库Etcd3.5.9随 kubeadm 镜像附带为什么不选最新版开发测试环境不需要追新稳定和资料多是第一位的。遇到问题搜索时老版本踩坑记录更多能快速找到解决方案。另一个原因是新版 K8s 对硬件和内核有更高要求内网机器配置参差不齐保守一点更稳妥。2. 离线资源准备把“货”备齐再进场2.1 资源清单梳理动手部署之前我建议你先花半天时间把所有需要用到的软件包列成清单。不要想到一个装一个那样后面会很被动。我的清单大致分四类RPM/DEB 包containerd、kubeadm、kubelet、kubectl 的安装包还有基础依赖比如 conntrack、socat容器镜像K8s 系统镜像 Calico 相关镜像 常用中间件镜像比如 nginx、redis、busybox部署文件Calico 的 yaml 编排文件、私有仓库的 docker-compose 文件、后续可能需要用的 StorageClass 定义运维工具ctr、crictl、nerdctl、kubectl 的二进制以及整理好的部署脚本其中容器镜像是大头。K8s 系统镜像我直接用 kubeadm 提供的kubeadm config images list命令拉取版本列表再配合私有仓库地址重新拉取这样不容易漏。命令类似于kubeadm config images list --kubernetes-version v1.28.2会输出一组镜像名例如 kube-apiserver、kube-controller-manager 等把这些镜像名称里的仓库地址替换成registry.local再逐个拉取。如果资源构建机上已经装好了 containerd也可以用ctrl或nerdctl操作但最省事的方式还是用 Docker 构建机拉好再推送。2.2 镜像保存、校验与推送镜像准备的完整流程我建议这样做在一台能访问软件源的构建机上先用 Docker 把所有镜像拉下来然后重新打上私有仓库的 tag最后导出为 tar 包。具体步骤docker pull registry.local/library/kube-apiserver:v1.28.2 docker save registry.local/library/kube-apiserver:v1.28.2 -o kube-apiserver.tar如果镜像数量多可以用脚本循环处理把镜像清单放在一个文本文件里逐行读取。这里有个容易被忽略的细节每个 tar 包保存后要记录对应的 sha256 值用于进场后校验完整性。命令很简单sha256sum kube-apiserver.tar为什么要记录校验值离线环境最怕的就是拷贝过程中文件损坏一个镜像包损坏可能导致整个集群初始化不了到时候排查起来非常浪费时间。我在实际项目中见过同事因为没有校验把损坏的包带到现场结果卡了整整一下午。花五分钟记录校验值后面省心很多。到了内网环境之后先把 registry 服务跑起来再把这些 tar 包导入并推送到私有仓库。离线环境下 registry 本身可以用 Docker 容器方式运行前提是 Docker 已经提前装好或用离线包安装。也可以在没有 Docker 的情况下直接用 registry 二进制跑但容器方式更简单version: 3 services: registry: image: registry:2.8.3 container_name: registry restart: always ports: - 5000:5000 volumes: - /data/registry:/var/lib/registry environment: REGISTRY_STORAGE_DELETE_ENABLED: true我习惯把 registry 单独放在一台低配机器上或者直接跑在第一个控制面节点上。开发测试环境节点不多共用一台机器问题不大。推送镜像用docker push或者用skopeo copy直接把 tar 包推到 registry也可以。2.3 使用兜底方案节点本地镜像导入如果因为某些原因来不及搭私有仓库还有一套兜底方案把镜像直接导入每个节点的 containerd 中。containerd 不像 Docker 那样开箱即用docker load需要使用ctr工具操作ctr -n k8s.io images import kube-apiserver.tar注意这里的命名空间必须是k8s.io因为 containerd 作为 K8s 的容器运行时默认使用这个命名空间来管理 kubelet 创建的容器。导完之后可以用crictl images检查是否能看到对应镜像。但这个方案只适合临时使用。如果后续要加节点每个新节点都需要手动导入一遍非常麻烦镜像更新时又得全部重新导。所以只要条件允许我强烈建议搭一个私有仓库这是所有离线 K8s 部署里投入产出比最高的一件事。3. kubeadm 初始化核心步骤实操3.1 基础环境配置进入正题。操作系统我以 CentOS 7.9 为例其他发行版类似。每台节点机器需要先做四件事关闭 swap、关闭防火墙、配置主机名、加载内核模块。关闭 swap 是硬性要求。kubelet 在 1.28 版本中默认不支持 swap如果不关闭kubelet 会直接启动失败。执行swapoff -a之后还要把/etc/fstab里的 swap 行注释掉重启后才不会恢复。防火墙方面内网环境可以直接关闭 firewalld也可以只放行必要端口例如 6443、2379、2380、10250、30000-32767。开发测试环境我建议直接关闭减少排查成本但前提是网络团队允许。内核模块方面containerd 需要overlay和br_netfilter模块cat EOF | tee /etc/modules-load.d/containerd.conf overlay br_netfilter EOF modprobe overlay modprobe br_netfilter同时需要调整sysctl参数让 iptables 能正确转发流量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这里的net.ipv4.ip_forward必须开启否则 Pod 之间跨节点通信会出问题Calico 的 NAT 规则也不会生效。3.2 containerd 离线安装与配置containerd 的安装是离线环节最容易出错的地方之一。RPM 包装完后需要修改它的默认配置。先导出默认配置containerd config default /etc/containerd/config.toml然后修改几个关键地方。最重要的就是SystemdCgroup必须是true确保 containerd 使用 systemd 来管理 cgroup和 kubelet 保持一致[plugins.io.containerd.grpc.v1.cri.containerd.runtimes.runc.options] SystemdCgroup true接下来配置镜像仓库 mirror把docker.io的请求转发到本地 registry[plugins.io.containerd.grpc.v1.cri.registry.mirrors] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.docker.io] endpoint [http://registry.local:5000]这里可以多加几个 mirror 段比如registry.local:5000本身就是最终仓库直接用也可以。关键是让 K8s 在拉取docker.io/library/nginx这类镜像时实际请求的是本地仓库避免去外部源拉取。配置完成后重启 containerdsystemctl daemon-reload systemctl restart containerd systemctl enable containerd验证方式很简单使用crictl pull registry.local:5000/library/pause:3.9看能否成功拉取。如果成功说明 containerd 和私有仓库之间的通路是通的。3.3 初始化控制面节点所有节点装好 kubeadm、kubelet、kubectl 之后就可以在控制面节点执行初始化。这里我建议提前写好一个 kubeadm 配置文件避免在命令行里塞一长串参数。apiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.10.11 bindPort: 6443 nodeRegistration: criSocket: /run/containerd/containerd.sock --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 imageRepository: registry.local:5000/library networking: podSubnet: 10.244.0.0/16 serviceSubnet: 10.96.0.0/12 --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd有几个参数需要解释一下criSocket指明 kubelet 使用 containerd 作为运行时如果不写kubeadm 会默认找 docker.sock找不到就会报错imageRepository指向本地私有仓库所有系统镜像都会从这里拼接获取advertiseAddress是控制面节点的内网 IP供其他节点访问 API ServerpodSubnet选用10.244.0.0/16是为了和后面 Calico 的默认网段保持一致避免二次调整配置文件准备完成后执行kubeadm init --config kubeadm-config.yaml --upload-certs如果一切正常终端会输出一段 join 命令类似kubeadm join 192.168.10.11:6443 --token xxxx --discovery-token-ca-cert-hash sha256:xxxx这里要强调一点把这段输出保存好后续加节点时要用。如果当时没保存也没关系可以用下面的命令重新生成kubeadm token create --print-join-command初始化完成后还需要拷贝 kubeconfig 到当前用户目录否则 kubectl 无法访问集群mkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config chown $(id -u):$(id -g) $HOME/.kube/config3.4 安装 Calico 网络插件控制面初始化之后集群处于一个很微妙的状态node 已经注册但状态是 NotReady因为还没有网络插件。此时 APIServer 和 etcd 已经运行可以使用 kubectl 查看节点状态kubectl get nodesCalico 的部署需要用到提前准备好的calico.yaml文件。这个文件里包含很多资源定义其中最重要的是镜像地址需要改成本地仓库。确认文件里所有calico/cni、calico/node、calico/kube-controllers镜像的地址都已经替换成本地地址。例如image: registry.local:5000/library/calico/cni:v3.26.1替换完成之后执行kubectl apply -f calico.yaml然后持续观察 pod 状态kubectl get pods -n kube-system -o wide等待calico-node和calico-kube-controllers进入 Running 状态节点状态会从 NotReady 变为 Ready。这个过程正常情况下需要一到两分钟如果超过五分钟还没 Ready多半是镜像没拉到或者网络模式配置有问题需要按后面排查章节处理。3.5 工作节点加入集群控制面准备就绪后其他节点只需要三步安装 containerd 和 kubeadm、配置 containerd 的 mirror、执行 join 命令。我建议把前三步写成一个初始化脚本在每台工作节点上跑一遍最后手动执行 join 命令。join 命令中有一个容易被忽略的点如果你在控制面初始化时使用了自定义的 imageRepository那么 join 时不需要额外指定镜像仓库kubelet 会自动从控制面获取配置。唯一需要确保的就是 containerd 里的 mirror 配置已经就位。如果 join 时报错提示 token 过期重新在控制面执行kubeadm token create --print-join-command生成新 token 即可。如果提示 CA 证书 hash 不匹配检查 join 命令里的--discovery-token-ca-cert-hash是否和实际一致。更稳妥的方式是使用--discovery-file直接指定 CA 证书文件避免 hash 计算问题。3.6 集群自检清单所有节点加入后我建议按下面的清单过一遍确认基础功能正常kubectl get nodes所有节点状态为 Ready版本一致kubectl get pods -A所有系统组件处于 Running 或 Completed 状态kubectl run test --imagebusybox -- sleep 3600能正常创建测试 Podkubectl exec test -- nslookup kubernetes.defaultDNS 解析正常在两个不同节点上的 Pod 之间 ping 通跨节点网络正常实测中我会新建两个测试 Deployment分别调度到不同节点然后从一个 Pod 去访问另一个 Pod 的 ClusterIP确认 Service 转发也没问题。这些都是开发测试环境后续会大量用到的基础能力尽早验证后面问题少很多。4. 常见问题与排查技巧实录4.1 镜像拉取失败和 ImagePullBackOff这个错误是离线环境出现频率最高的问题。现象是 Pod 一直处于 ImagePullBackOff查看详情时会看到拉取私有仓库镜像失败常见原因有三个镜像地址写错或者不存在需要到 registry 上确认目录和 tagcontainerd 的 mirror 配置没生效重启服务或者重新加载配置访问仓库超时排查节点到 registry 的网络连通性我的排查顺序固定是先crictl images看节点上有没有镜像再curl http://registry.local:5000/v2/_catalog看仓库里有没有该镜像最后crictl pull手动拉一次看具体报错。这样一步步缩小范围基本十分钟内能定位。4.2 kubelet 反复重启初始化完控制面节点后kubelet 如果一直处于 activating先看日志journalctl -u kubelet -f --no-pager常见的报错有两个。一个是 cgroup driver 不匹配报错里会明确提到systemd和cgroupfs检查 containerd 的SystemdCgroup配置和 kubelet 的cgroupDriver是否一致。另一个是节点上没有关闭 swapkubelet 启动时会直接拒绝错误信息里能看到running with swap on is not supported。这两个问题都是配置类的处理起来不难但很考验耐心。我的经验是把配置文件和内核参数做成初始化脚本不要在每台机器上手敲否则很容易漏。4.3 加入节点时 token 报错使用 join 命令时报token is invalid或者token has expired说明证书链或 token 有问题。重新生成 join 命令是最直接的办法kubeadm token create --print-join-command如果提示 CA 证书 hash 问题可以先删除旧的 token 再创建新的。还有一种情况是工作节点之前初始化过残留的配置会导致 join 冲突需要先执行kubeadm reset清理环境然后重新 join。这个操作在开发测试环境很常用节点环境不干净是非常普遍的事。4.4 CoreDNS 一直 PendingCoreDNS 处于 Pending 状态说明集群没有可用的网络插件或者网络插件创建的 Pod 没有正常工作。先确认 Calico 的 Pod 是否 Running再查看有没有 node 处于 NotReady。如果 Calico 已经 Running但 CoreDNS 还是 Pending可能是节点资源不足例如内存太少导致调度失败。开发测试环境里我遇到过因为 master 节点没有打污点容忍导致 Pod 只能调度到 master而 master 内存刚好不够的情况。用kubectl describe pod coredns-xxxx能看到调度失败的详细信息。4.5 节点跨节点通信不正常跨节点 Pod 通信失败是一个隐蔽的问题。现象是同一节点上的 Pod 之间通信正常但跨节点就会超时。一般原因是 Calico 的CALICO_IPV4POOL_CIDR和 Pod 子网不一致或者节点之间的 179 端口未放行。如果使用了 flannel则要检查 VXLAN 所需的 8472 端口是否通。排查时先看calicoctl node status查看 BGP peer 状态。然后再检查节点防火墙和安全组规则。还有一个很常见的坑如果某些节点上有多个网卡Calico 可能选择错误的网卡作为隧道地址需要在 Calico 的环境变量里显式指定IP_AUTODETECTION_METHOD。5. 开发测试环境的高效落地心得一路踩坑走过来最后分享几个对开发测试环境特别有用的落地心得。第一一定要把资源清单和部署脚本做成标准制品放到内网的版本仓库里。换人操作也能按照文档一步步复现不用依赖某一个人的记忆。每次部署完顺手记录当前版本号和已知问题时间久了就是一份很有价值的环境档案。第二镜像仓库的数据定期备份。开发测试环境的镜像仓库看似不重要但一旦哪天磁盘损坏导致仓库数据丢失所有环境都得重新拉取和推送镜像代价非常高昂。简单用 tar 备份整个 registry 数据目录或者定期导出镜像清单都能让恢复流程快很多。第三网络问题占比最高要提前规划网段分配。Pod 子网、Service 子网、节点物理网络这三者在离线部署中如果有任何一环冲突都会产生非常隐蔽的故障。我吃过的亏是节点物理网段正好落在 Pod 子网范围内导致路由错乱排查了两天才发现。建议在部署前就把网段规划画好写入部署文档的第一页。6. 后续扩展与维护建议这套环境跑起来只是开始。开发测试环境在实际使用中会频繁扩容和变化我整理了三个后续一定会遇到的方向。6.1 增加节点与版本升级开发测试环境加节点很简单新节点执行基础初始化脚本配置好 containerd 和私有仓库再执行 join 命令即可。但如果节点需要持久化存储还要提前部署 NFS 或者本地存储插件。升级场景要更谨慎先升级控制面再升级工作节点每一步都要检查所有系统 Pod 的状态。6.2 常用中间件镜像预置开发测试环境必然会用到中间件镜像例如 MySQL、Redis、Kafka、Nginx、MinIO 等。我建议在部署集群的同时把这些镜像全部收集到私有仓库里避免业务同学临时需要时再来找运维要镜像。镜像版本最好和测试业务使用的版本保持一致的节奏更新。6.3 从“能跑”到“好用”基础集群稳定之后下一步就是补充日志、监控和 CI/CD 流水线。比如通过本地镜像仓库配合 Helm 简化应用发布用 Prometheus 监控集群核心指标。这一层看起来是锦上添花但对开发测团队而言体验提升非常明显。我在实际使用中发现离线部署这件事真正困难的地方并不是某一条命令有多难记而是整体流程的串联和细节的积累。只要前期把资源备好、配置统一、文档同步跟上后面的运行维护压力其实小得多。这套流程后来我又完整跑了两遍一次比一次快第一次用了近乎两天最后一次两个小时不到就能拉起一套全新环境这就是标准化和制品化带来的回报。