真正考验人的从来不是怎么敲命令而是机器断网之后你还有没有后手。最近在折腾一套完全隔离的 CentOS 环境目标机没有任何外网出口yum 源也是内部固化的还不能引入庞大的镜像仓库却要求在裸机上把 Docker 和 Kubernetes 完整跑起来。这时候什么yum install -y docker-ce kubeadm一把梭的思路全部作废唯一靠谱的路线就是离线二进制安装提前在有出网条件的机器上把所有二进制、镜像、依赖全部打包好拷进内网一步步落地。这篇文章会把完整流程拆开讲透包括离线包怎么准备、Docker 怎么解压安装、cri-dockerd 为什么绕不过去、kubeadm 初始化前要做哪些系统预检、镜像怎么导入、工作节点怎么加入。适合运维、交付、实施工程师也适合准备把 K8s 放进隔离环境的同学直接照着抄。1. 为什么离线环境必须用二进制源、依赖与可控性的一场权衡1.1 离线环境真正缺的东西所谓离线环境并不是机器上完全没有网络而是它对外部世界的访问路径被限制死了。常见的表现有三种DNS 出不去ping 通但域名解析不了yum 源被策略固化只允许指向内部镜像站而内部镜像站里根本没有 docker-ce 和 kubernetes 的仓库再有就是安全要求严格的目标机不允许安装编译器、不允许临时拉取外部仓库。这种环境下包管理器能提供的能力基本归零。yum install找的是本地源本地源没有就是没有和有没有外网没关系。而kubeadm init一旦执行kubelet 会按配置主动去拉镜像拉不到就会卡在 waiting 状态后台日志全是 timeout。这也是为什么很多人第一次在断网机器上装 K8s 时明明前面都很顺利最后却栽在镜像这一步。二进制安装的优势在于它把软件本身和系统包管理彻底解耦。tar 包解压完二进制文件就在那里放到/usr/bin就能执行。它不依赖 yum 仓库不依赖源配置只要系统的 glibc、libseccomp 这些动态库满足要求就能跑起来。对于一次性交付、版本锁定、多台机器批量复制的场景这种可搬移性非常关键。1.2 三种离线方案怎么选在决定用二进制之前我把几种常见离线方案都摆出来对比过这里也放出来给大家参考方案优点缺点适合场景yum 本地源依赖自动解决安装命令习惯不变需要提前收集大量 rpmK8s 相关包的依赖链复杂维护成本高长期维护、机器数量大、要持续补丁更新离线二进制包文件少路径可控版本锁定搬移方便依赖库要自己检查systemd unit 要自己写一次性交付、小规模集群、环境被严格隔离内网镜像仓库 脚本镜像管理规范扩容节点快初始搭建复杂要额外维护 registry 服务和存储长期运行的规模化集群节点会持续增加我个人的结论是如果你只是交付三台五台机器或者目标环境根本没条件长期维护源和仓库二进制是性价比最高的选择。等集群真正常态化了再考虑在那台能出网的构建机上搭 registry把镜像推进去后续扩容会轻松很多。1.3 为什么不用纯手工二进制部署提到二进制安装有人会问是不是要手动把 kube-apiserver、etcd、kube-controller-manager、kube-scheduler 一个个配好不是的。纯手工部署意味着要自己签发证书、自己写四五个 systemd unit、自己规划 etcd 集群步骤量多且极易出错完全不推荐。这里说的二进制安装是二进制形态的 kubeadm 引导方式。kubeadm 本身就是个二进制它负责生成证书、生成静态 Pod 清单、初始化 etcd控制面组件全部以静态 Pod 的方式跑起来。这种方式既保留了二进制安装的离线可控性又不需要手工处理 K8s 内部复杂的证书编排是最符合实际工程习惯的做法。2. 动手前先锁版本架构、组件组合和交付目录2.1 版本组合怎么定才不踩雷离线环境最大的问题是没有试错机会装到一半发现版本不兼容重新下载包的成本很高。所以第一步就是把版本组合锁死。下面这套是我实际验证过比较稳的搭配组件推荐版本说明CentOS7.9 或 8.x7.9 要注意 libseccomp 版本8.x 相对省心Docker24.0.7静态二进制包离线解压即用cri-dockerd0.3.14用于把 Docker 桥接到 Kubernetes CRIKubernetesv1.28.2kubeadm、kubelet、kubectl 三者版本一致CNI pluginsv1.3.0通用 CNI 插件集合解压到 /opt/cni/binFlannelv0.24.2网络插件示例镜像少离线友好如果 CentOS 7.9 上 Docker 24 启动遇到 libseccomp 兼容问题可以退到 Docker 20.10.24配合 K8s 1.26 系列使用功能完全够。版本配比的核心原则是Docker 只负责镜像管理和容器运行时cri-dockerd 负责把 Docker 的能力翻译成 Kubernetes 需要的 CRI 接口K8s 侧三个二进制必须同版本否则 kubeadm init 会直接报版本不匹配。2.2 架构一致性不能碰运气下载机和目标机必须是同一 CPU 架构。这是一个看起来基础、实际翻车率极高的点。你在 x86_64 的虚拟机里打包好的 Docker、K8s 二进制拷到 arm64 的物理机上是不可能执行的命令直接报 exec format error。同样镜像也必须跟着架构走。docker pull出来的镜像是分平台清单的在 x86 上下载后 load 到 arm 机器上会直接失败。所以准备离线包之前先确认目标机的架构uname -m看准了再动手。本文默认以 amd64 为例所有 tar 包和二进制下载对应的都是 linux-amd64。2.3 建立一个可复制的交付目录离线安装不是把文件随便丢进一个 U 盘就能开始。我习惯在下载机上维护一个固定的交付目录结构每次交付都往里面填内容delivery/ ├── bin/ # docker、kubeadm、kubelet、kubectl、cri-dockerd 二进制 ├── cni/ # cni-plugins tgz 和解压后的插件文件 ├── images/ # 所有 tar 格式的离线镜像包 ├── systemd/ # docker.service、cri-dockerd.service、kubelet.service ├── conf/ # daemon.json、kubeadm.yaml、sysctl.conf └── install.sh # 批量执行安装过程的脚本这个结构看起来简单但在多台机器批量交付时会省很多事。每一类文件放固定位置到目标机上按目录执行而不是临时翻 U 盘找文件。后面我也会按照这个目录来组织步骤。3. 离线包准备拿什么、去哪儿拿、怎么检查缺依赖3.1 完整文件清单离线包准备的核心是把目标机上要用的和可能要用的全部装进同一个目录。下面是我整理的清单每一样都有明确用途文件用途docker-24.0.7.tgzDocker 引擎静态二进制包cri-dockerd-0.3.14-linux-amd64.tgzDocker 的 CRI 桥接组件kubeadm、kubelet、kubectlKubernetes 控制面引导、节点管理、命令行工具cni-plugins-linux-amd64-v1.3.0.tgz集群网络所需的 CNI 插件集合k8s-images-v1.28.2.tar控制面组件镜像apiserver、controller-manager、scheduler、kube-proxy、etcd、coredns、pausekube-flannel.ymlFlannel 网络插件部署清单离线保存daemon.json、各种 service 文件配置文件样本到目标机后直接覆盖有人会问flannel 的 yml 是不是也要一起准备是的。在离线环境里kubectl apply一个远程 URL 是拿不到的所以 yml 文件必须在有网机器上先保存成文本跟着 tar 包一起拷过去。3.2 下载源与下载姿势Kubernetes 三个二进制文件在 Kubernetes 的 GitHub Releases 页面找对应版本的 assets。cri-dockerd 在 Mirantis 的 Release 页面下搜索匹配你 K8s 版本的 tag。Docker 静态包在 Docker 官方静态仓库的 linux/static/stable 目录下文件名通常形如docker-24.0.7.tgz。CNI 插件在 containernetworking/plugins 的 Release 页面下。下载的时候有几个习惯很重要。第一全部使用固定版本号命名不要用latest否则后续你根本不知道手里是什么版本。第二下载完顺手做一次校验对比发布页给出的 SHA256避免包在传输过程中损坏或被人改过。第三下载机的系统类型和 shell 环境最好保持干净不要拿装有旧版本工具的机器去打包避免环境变量干扰。3.3 依赖库检查ldd 是最好的编译清单二进制文件是可执行的但它仍然依赖系统动态库。最常见的问题是 CentOS 7 自带的 libseccomp 版本太旧导致高版本 Docker 启动时直接报错。怎么提前查用lddldd /usr/bin/dockerd | grep not found ldd /usr/bin/cri-dockerd | grep not found如果输出为空说明动态库依赖基本满足。如果出现libseccomp.so.2 not found说明系统没有这个库或者版本不够。解决方式有两种一是找一台同系统版本的有网机器下载对应版本的 libseccomp rpm 包传到内网用yum localinstall安装二是退而求其次把 Docker 换成 20.10 系列它对这些库的要求更宽松。实际工程里两种方式都行看你对 Docker 版本的执念程度。4. Docker 二进制落地的完整过程解压、配置、systemd 托管4.1 解压并安放二进制Docker 的静态包解压后是一个docker/目录里面包含了 dockerd、docker、containerd、runc、docker-proxy 等所有必要组件。安装步骤非常直接tar -xzf docker-24.0.7.tgz cp docker/* /usr/bin/ chmod x /usr/bin/docker*这一步做完先不要急着配置直接docker version验证一下。如果能看到 Client 和 Server 两段信息说明引擎本身没问题。如果只看到 Client 看不到 Server多半是 dockerd 没起来或者动态库有问题用journalctl -u docker去看日志。4.2 daemon.json 里的三件套Docker 的配置集中在/etc/docker/daemon.json。离线环境里不需要镜像加速但有三个配置项是必须的。先看完整示例{ exec-opts: [native.cgroupdriversystemd], log-driver: json-file, log-opts: { max-size: 100m, max-file: 3 }, storage-driver: overlay2, data-root: /data/docker }第一native.cgroupdriversystemd。这是给 Kubernetes 用的前提条件。kubelet 默认会按 systemd 的方式管理 CGroup如果 Docker 还在用 cgroupfs两者会出现双管理器的冲突kubelet 会直接报错说 cgroup 驱动不一致。这条必须在 Docker 侧先定死。第二storage-driver使用 overlay2这是目前最稳定的存储驱动。第三>[Unit] DescriptionDocker Application Container Engine Afternetwork-online.target firewalld.service Wantsnetwork-online.target [Service] Typenotify ExecStart/usr/bin/dockerd ExecReload/bin/kill -s HUP $MAINPID LimitNOFILEinfinity LimitNPROCinfinity LimitCOREinfinity TimeoutStartSec0 Delegateyes KillModeprocess Restartalways StartLimitBurst3 StartLimitInterval60s [Install] WantedBymulti-user.target把这个文件放到/etc/systemd/system/docker.service然后执行systemctl daemon-reload systemctl enable --now docker systemctl status docker docker infodocker info输出里重点看 Storage Driver 和 Cgroup Driver前者应该是 overlay2后者应该是 systemd。这两项对了Docker 这层就算稳妥了。5. 运行时桥接才是关键把 cri-dockerd 和 kubelet 装到位5.1 kubelet、kubeadm、kubectl 的安装Kubernetes 这边三个二进制安装跟 Docker 类似直接拷贝到/usr/bin并赋予执行权限即可cp kubeadm kubelet kubectl /usr/bin/ chmod x /usr/bin/kubeadm /usr/bin/kubelet /usr/bin/kubectl注意 kubelet 需要加载一个 systemd unit否则systemctl enable kubelet会没有服务。官方版本的kubelet.service内容如下[Unit] Descriptionkubelet: The Kubernetes Node Agent Documentationhttps://kubernetes.io/docs/ Wantsnetwork-online.target Afternetwork-online.target [Service] EnvironmentFile-/etc/sysconfig/kubelet ExecStart/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS Restartalways StartLimitInterval0 RestartSec10 [Install] WantedBymulti-user.target同时创建一个空的/etc/sysconfig/kubelet文件touch /etc/sysconfig/kubelet systemctl daemon-reload systemctl enable kubelet5.2 cri-dockerd为什么必须存在Kubernetes 1.24 版本之后内置的 dockershim 被移除Docker 不能再直接作为 K8s 的容器运行时接入。这不是说 Docker 不能用了而是 Docker 提供的接口和 Kubernetes 需要的 CRI 接口之间缺一层翻译。cri-dockerd 就是做这层翻译的它监听一个 Unix Socket把 Docker 的容器操作转换成 CRI 接口让 kubelet 能继续通过 Docker 创建和管理 Pod。cri-dockerd 的安装也很简单tar -xzf cri-dockerd-0.3.14-linux-amd64.tgz cp cri-dockerd /usr/bin/ chmod x /usr/bin/cri-dockerd然后写它的 systemd unit。注意 socket 路径要用/var/run/cri-dockerd.sock这个会被 kubeadm 和 kubelet 引用[Unit] DescriptionCRI Interface for Docker Application Container Engine Documentationhttps://github.com/Mirantis/cri-dockerd Afterdocker.service Wantsdocker.service [Service] ExecStart/usr/bin/cri-dockerd \ --container-runtime-endpointunix:///var/run/cri-dockerd.sock \ --network-plugincni \ --cni-bin-dir/opt/cni/bin \ --cni-conf-dir/etc/cni/net.d Restartalways StartLimitInterval0 RestartSec10 [Install] WantedBymulti-user.target5.3 验证 socket 与运行状态启动后验证systemctl daemon-reload systemctl enable --now cri-dockerd systemctl status cri-dockerd ls -l /var/run/cri-dockerd.socksocket 文件存在说明 cri-dockerd 已经在监听。如果启动失败多半是 docker.service 还没就绪或者二进制依赖缺失用journalctl -u cri-dockerd查看具体报错。到这里控制面组件和运行时桥接都已经就位下一步就是系统预检和初始化。6. 初始化主节点镜像导入、系统预检与 kubeadm init6.1 先把系统地基打对kubeadm 初始化之前系统层面有几件事必须做否则不是卡在证书就是卡在网络。逐条来。关闭 swap并取消 fstab 里的开机挂载否则 kubelet 会拒启动swapoff -a sed -i /swap/s/^/#/ /etc/fstab加载内核模块并设置网络参数modprobe br_netfilter modprobe overlay cat /etc/sysctl.d/k8s.conf EOF net.bridge.bridge-nf-call-iptables 1 net.bridge.bridge-nf-call-ip6tables 1 net.ipv4.ip_forward 1 EOF sysctl -p /etc/sysctl.d/k8s.confbridge-nf-call-iptables必须为 1否则集群内 Pod 之间的通信流量会绕过 iptables 规则表现为网络策略不生效或者 Service 访问不通。防火墙在隔离环境里直接关闭更省事同时把 SELinux 切到 permissivesystemctl disable --now firewalld setenforce 0 sed -i s/^SELINUXenforcing/SELINUXpermissive/ /etc/selinux/config如果是生产环境不能关防火墙至少要把 6443、2379、2380、10250 这些端口放行并在网络层面也同步开放否则后续 NodePort 访问会有各种奇怪问题。6.2 控制面镜像的导入离线 init 的命门kubeadm init 默认会从镜像仓库拉取控制面组件离线环境必须提前把镜像 load 到本地。流程分两步先在下载机操作images$(kubeadm config images list --kubernetes-version v1.28.2) for image in $images; do docker pull $image done docker save $images -o k8s-images-v1.28.2.tarkubeadm config images list会输出完整的镜像清单包含 apiserver、controller-manager、scheduler、kube-proxy、etcd、coredns、pause 等。把生成的 tar 拷到目标机后docker load -i k8s-images-v1.28.2.tar docker images | grep -E kube-apiserver|etcd|pause看到镜像列表后kubeadm init 时只要本地出现同名镜像就不会再触发网络拉取。这也是离线安装中决定成败的关键一步。6.3 写 kubeadm 配置文件并初始化kubeadm 的配置建议写成文件而不是堆命令行参数。重点是指定criSocket和cgroupDriverapiVersion: kubeadm.k8s.io/v1beta3 kind: InitConfiguration localAPIEndpoint: advertiseAddress: 192.168.10.10 bindPort: 6443 nodeRegistration: criSocket: /var/run/cri-dockerd.sock name: master01 --- apiVersion: kubeadm.k8s.io/v1beta3 kind: ClusterConfiguration kubernetesVersion: v1.28.2 networking: podSubnet: 10.244.0.0/16 --- apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration cgroupDriver: systemd注意networking.podSubnet要和后面 Flannel 的网段保持一致。我这里写的是10.244.0.0/16这是 Flannel 的默认网段。执行kubeadm init --config kubeadm.yaml初始化完成后kubeadm 会打印一段kubeadm join命令务必复制保存。接着配置 kubectlmkdir -p $HOME/.kube cp -i /etc/kubernetes/admin.conf $HOME/.kube/config kubectl get nodes如果看到 master01 处于 NotReady不要慌这是正常的因为网络插件还没装。等下一步装完 CNI 状态就会变成 Ready。7. 工作节点与网络插件把集群从一台扩到多台7.1 工作节点的重复安装工作节点的安装路径和主节点前半段完全一致装 Docker装 cri-dockerd装 kubelet、kubeadm、kubectl。唯一的区别是不需要执行kubeadm init而是加入集群。加入之前工作节点同样要导入镜像。最省事的方式是直接 load 主节点那份k8s-images-v1.28.2.tar虽然工作节点真正用到的只有 pause 和 kube-proxy但全量导入可以避免后续加组件时要单独找镜像。然后执行kubeadm join 192.168.10.10:6443 \ --token token \ --discovery-token-ca-cert-hash sha256:hash \ --cri-socket/var/run/cri-dockerd.sock如果 token 过期在控制面节点重新生成kubeadm token create --print-join-command等几分钟在控制面节点执行kubectl get nodes就能看到两个节点都注册进来了。7.2 Flannel 网络插件的离线部署节点注册只是第一步不装 CNI节点永远是 NotReady因为 kubelet 检查不到已就绪的网络。这里用 Flannel 做示例一是镜像少二是整体依赖简单。先在下载机上准备 Flannel 镜像docker pull docker.io/flannel/flannel:v0.24.2 docker save docker.io/flannel/flannel:v0.24.2 -o flannel-images.tar把 tar 包拷到集群的每个节点逐个 load。同时把cni-plugins-linux-amd64-v1.3.0.tgz解压到每个节点的/opt/cni/binmkdir -p /opt/cni/bin tar -xzf cni-plugins-linux-amd64-v1.3.0.tgz -C /opt/cni/binkube-flannel.yml也要提前保存到控制面节点执行kubectl apply -f kube-flannel.ymlFlannel 启动后会自动创建kube-flannel的 DaemonSet每个节点上的 Pod 会为 Node 配置网络。等待一两分钟再执行kubectl get nodes节点状态应该从 NotReady 变成 Ready。7.3 用一个小应用验证集群验证集群是不是真的能跑业务建议先跑一个简单的 Deploymentdocker pull nginx:1.25 docker save nginx:1.25 -o nginx-images.tar每个节点 load 后在控制面节点执行kubectl create deployment nginx-test --imagenginx:1.25 kubectl scale deployment nginx-test --replicas2 kubectl get pods -o widePod 分布在两个节点并且都处于 Running说明调度、网络、运行时桥接全部正常。如果 Pod 一直 Pending优先检查kubectl describe pod里的提示通常是镜像没导入或 CNI 插件没就绪。8. 复盘离线安装里程里反复踩的六个坑8.1 版本组合错位Docker 24 配 K8s 1.28 是没问题的但前提是 cri-dockerd 版本要跟上。某 Linux 发行版自带的 cri-dockerd 太老时kubelet 连不上 socket各种连接超时。我现在的习惯是下载时就把版本号写进目录名K8s 三个二进制版本严格对齐cri-dockerd 单独锁定。8.2 cgroup 驱动不一致kubelet 报错里最典型的一段是misconfiguration: kubelet cgroup driver: systemd but Docker cgroup driver: cgroupfs。原因就是 daemon.json 里没写native.cgroupdriversystemd或者 kubelet 配置里默认用了 cgroupfs。两边都改成 systemd一次解决。8.3 kubeadm init 一直卡住最常见的原因是缺少 pause 或 etcd 镜像kubelet 在后台反复拉取然后超时。我的排查顺序是先docker images | grep registry.k8s.io看镜像是否齐全再journalctl -u kubelet -f看日志。所有控制面镜像必须全部 load 完再 init不要边 init 边补镜像。8.4 忘记关闭 swap 或注释 fstab很多人swapoff -a执行完就以为结束了重启之后 swap 又起来了kubelet 随即拒绝启动。务必要在 fstab 里把 swap 行注释掉。这个坑如果不在交付文档里写明后续运维人员大概率会重新踩一遍。8.5 CNI 没装导致节点 NotReady节点加入后一直 NotReady很多人的第一反应是去查 kubelet但其实只是没装 CNI。kubectl get pods -n kube-flannel看一下如果 Flannel Pod 没有 Running就去找镜像导入和 CNI 二进制解压的问题。一定要记得/opt/cni/bin在每台节点上都要有不能只在主节点解压。8.6 动态库版本不足CentOS 7 的 libseccomp 太旧Docker 24 起不来是另一个高频坑。我现在的做法是在准备阶段先把所有二进制的ldd检查输出保存下来逐台比对。真遇到依赖缺失就得单独拷 rpm 进内网这个成本比安装本身还高所以务必提前发现。段落尾不设标题最后再分享一个我自己几次踩坑之后沉淀下来的习惯离线交付不要临时找包而是维护一个固定结构的 delivery 目录bin、images、systemd、conf 四类文件严格分离版本号全部写进环境变量写一个 install.sh 一次性串起安装动作。这样每台新机器执行的就是同一套流程出现差异时也能很快锁定是哪台机器的系统环境不同。离线安装这件事真正考验的不是敲命令而是你有没有在出发前把所有依赖和版本都想清楚。
阅读完成 · 觉得有帮助?