k8s离线部署实战:flannel镜像包制作与导入指南

发布时间:2026/9/29 19:11:51
k8s离线部署实战:flannel镜像包制作与导入指南
简介Kubernetes集群部署中flannel网络插件是保障Pod跨节点通信的关键组件但国内直接拉取镜像经常遇到网络不畅或超时问题。这份镜像包正是为解决这一痛点而整理面向正在搭建k8s环境、需要离线安装或加速部署flannel的运维人员与开发工程师。资源共3个文件压缩包整体27.34MB包含两个tar格式镜像文件和配套的kube-flannel.yaml资源配置清单tar用于离线导入镜像yaml用于声明式创建相关资源结构简洁可直接对照使用。已有1613人学习下载适合在无外网、内网隔离或网络不稳定的服务器环境中使用。解压后通过docker load导入两个镜像再执行kubectl apply应用yaml文件即可完成flannel网络插件安装省去手动寻找镜像、编辑配置的繁琐步骤有效降低Kubernetes集群初始化阶段的出错概率是集群搭建时一份称手的备件。1. k8s 集群搭建里的 flannel 镜像包到底解决的是哪一步k8s 集群搭建做到安装网络插件这一环时很多人会卡在 flannel 镜像包上master 和 node 节点都 init 好了Pod 调度过去却反复 ImagePullBackOffcoredns 起不来整个集群看起来活着但什么也跑不通。问题根源在于 kube-flannel.yml 默认从 quay.io 拉镜像而很多离线内网环境和这台仓库之间的连通性很差时延高、下载慢甚至直接不可达。把 flannel 必要镜像包在能联网的机器上提前拉好、打好 tag、导出再导入集群节点是 k8s 离线部署里最常见也最可靠的方案适合刚开始搭集群、以及需要批量交付内网环境的从业者。2. 先搞清楚 flannel 必要镜像包里有哪几个镜像清单与版本对应2.1 kube-flannel.yml 实际会拉哪几个镜像很多人在做镜像包时只记住了 quay.io/coreos/flannel 这一个名字等 flannel DaemonSet 跑起来才发现还缺东西。实际去翻 kube-flannel.yml里面会出现两个镜像一个是 flannel 主程序负责维护 VXLAN 网络、写入路由规则另一个是 flannel-cni-plugin负责作为 CNI 插件被 kubelet 调用把 Pod 接入 flannel 网桥。两个镜像名分别是quay.io/flannelcni/flannel:v0.21.5某些旧版本是 quay.io/coreos/flanneldocker.io/flannelcni/flannel-cni-plugin:v1.1.0还有个隐藏需求kube-flannel.yml 里有一个 initContainer会在容器启动时去下载 CNI 基础二进制比如 loopback、portmap 这几个插件。离线环境下这个 initContainer 会一直失败表现为 flannel Pod 卡在 Init:0/1。常见做法是把 cni-plugins-linux-amd64-v1.1.1.tgz 手动解压到每个节点的 /opt/cni/bin 目录再创建好 /etc/cni/net.d这样 initContainer 即使拉不到文件也不影响启动。所以制作 flannel 必要镜像包之前先对着 kube-flannel.yml 把镜像名全部列出来不要凭记忆。我一般会执行grep -E image:|name: flannel kube-flannel.yml这会同时显示镜像地址和容器名。如果看到的是 quay.io/coreos/flannel:v0.14.0说明你手里是较老版本的 yaml对应的 cni-plugin 镜像是 quay.io/coreos/flannel-cni-plugin:v0.14.0。新老命名空间不一样导入之后镜像名怎么 tag 就必须跟 yaml 里完全一致这是离线装 flannel 的第一条铁律。2.2 拉不下来时镜像包的源头怎么解决联网机器上执行 docker pull 时如果 quay.io 拉不动常见做法是从 docker.io 上找 flannel 的转存镜像或者直接用 skopeo 从 quay.io 拉。但更省事的路线是去 flannel 官方 release 页面看 releases 标签对应的镜像然后手动把 quay.io 的地址改成 docker.io 下 flannelcni 组织同名的镜像。flannelcni 在 Docker Hub 上有同步推送所以最快的方案是docker pull docker.io/flannelcni/flannel:v0.21.5 docker pull docker.io/flannelcni/flannel-cni-plugin:v1.1.0这个命令在大部分网络环境下比拉 quay.io 快得多因为 Docker Hub 的分发链路通常更顺畅。拉下来后打上 quay.io 的原路径 tag是为了让节点 load 之后kubelet 按照 yaml 里的镜像名能找到本地缓存——只要你后续把 imagePullPolicy 改成 IfNotPresentkubelet 就不会再去远程拉。这里有个细节镜像包不等于只有一个文件。如果你在节点上还得跑 kubeadm init 之后的检查kubeadm 会额外要求 kube-proxy 和 pause 两个镜像那是另一套镜像包的事。这个标题里说的 flannel 必要镜像包范围就锁死在 flannel 主镜像和 cni-plugin 镜像上加上一个可选的 CNI 基础二进制 tgz。2.3 flannel 与 k8s 版本对照tag 选错会直接影响 Pod 启动k8s 的版本策略是每个小版本对应的 CNI 协议版本和节点配置路径会有微调flannel 版本太老在 k8s 1.23 及以后会遇到 cni 配置文件路径失败的问题。照着 kube-flannel.yml 里镜像的 tag 来准备镜像包是最稳的但如果你是自己用 kubeadm init 出来的新集群我建议把 flannel 和 cni-plugin 都提到 v0.21 及以上。原因很简单新版本 flannel 不再依赖 etcd直接通过 kube-apiserver 读取集群信息少一个 etcd 端点配置的坑。tag 选错最典型的现象是flannel Pod 能起来但在日志里反复出现 failed to ensure that the daemon has the right cniVersion后面跟着 pod CIDR 分配失败。这时候不用怀疑镜像没导好先查 tag。版本对照表大致按这个惯性取值k8s 版本flannel 建议 tagcni-plugin 建议 tag备注1.21 - 1.23v0.17.x - v0.19.xv1.0.x老版本走 etcd 模式也行1.24 - 1.26v0.21.xv1.1.x - v1.2.xCNI spec 要求更高1.27v0.24.xv1.2.x建议直接 latest 前先 pin tagdocker pull 的时候我会先 docker manifest inspect 一下镜像架构这一步能同时验证 tag 存在和检查多架构支持省得导入后才发现平台不对。命令很简单docker manifest inspect docker.io/flannelcni/flannel:v0.21.5输出里能看到 amd64、arm64 等架构列表。如果只需要 amd64后续就不必考虑多架构问题如果集群里混了 ARM 节点就必须用 skopeo 或者 docker manifest 来做多架构复制这个放到第 5 章展开。3. 在联网机器上制作 flannel 镜像包拉取、打 tag、导出与导入3.1 定好镜像清单再动手变量化脚本先跑通不要一个镜像一个镜像手动敲命令。先写好镜像清单变量后面所有操作都基于同一个变量减少手滑打错标签的概率。我习惯把整个流程拆成两段联网机器上做镜像包目标节点上做导入部署。#!/usr/bin/env bash set -euo pipefail FLANNEL_VERSIONv0.21.5 CNI_PLUGIN_VERSIONv1.1.0 IMAGES( quay.io/flannelcni/flannel:${FLANNEL_VERSION} quay.io/flannelcni/flannel-cni-plugin:${CNI_PLUGIN_VERSION} ) for img in ${IMAGES[]}; do mirrordocker.io/flannelcni/${img#quay.io/flannelcni/} docker pull ${mirror} docker tag ${mirror} ${img} done这个脚本里 IMAGES 数组写的是最终导入到节点后 kubelet 要用的名字也就是 yaml 里的原镜像地址。docker pull 拉的是 Docker Hub 镜像docker tag 改成 quay.io 的路径目的就是让镜像在本地出现两个名字原始镜像名和最终消费名。docker save 时只用消费名打包导入节点后 containerd 本地缓存里存的 key 就是 quay.io 开头的那串和 kube-flannel.yml 完全对得上。镜像清单里漏了 flannel-cni-plugin 是新手最常见的问题因为只看到了 DaemonSet 里那个 flannel 主容器没注意 kube-flannel.yml 后面还有一个 cni 插件的 initContainer。写成数组的好处是后续加镜像比如以后要装 multus 或者 metric-server 相关镜像往数组里加一行就能复用整个流程。3.2 docker save 打包与 scp 传输注意压缩和断点镜像拉好后下一步是打包。docker save 出的 tar 通常有几百兆直接 scp 很浪费带宽先压缩再传是常规做法。docker save \ quay.io/flannelcni/flannel:v0.21.5 \ quay.io/flannelcni/flannel-cni-plugin:v1.1.0 \ -o flannel-images.tar tar -czf flannel-images.tar.gz flannel-images.tar ls -lh flannel-images.tar*tar 和 gzip 分开做比 docker save 完直接 tar czf 更可控——docker save 输出已经是 tar 格式再用 gzip 压缩一次就是为了传输省流量。节点上解压后再 docker load相当于走两遍本地磁盘但换来的是传输量可能从 300MB 降到 80MB 左右。传输时我一般用 scp但如果网络条件差宁可分节点传也不要一次传所有节点。中断之后重传前先在目标机器上校验文件大小或 md5防止传了一半的 tar 包在 load 时报 tar: Unexpected EOF。这一步不写进代码也没关系但一定要做md5sum flannel-images.tar.gz # 在联网机器上执行 md5sum flannel-images.tar.gz # 在目标节点上执行结果必须一致3.3 在目标节点导入docker load 与 containerd 的 ctr import 两条路镜像包到了目标节点接下来的导入分两种情况。如果你的节点还是 docker 作为容器运行时直接 loadtar -xzf flannel-images.tar.gz docker load -i flannel-images.tar docker images | grep flannel用 containerd 的节点尤其是 kubeadm 1.24 之后默认的 containerd socket 运行时docker load 是无效的。必须走 ctr 导入而且带不带 namespace 参数是关键k8s 的 containerd 镜像都存在 k8s.io 这个 namespace 里tar -xzf flannel-images.tar.gz ctr -n k8s.io images import flannel-images.tar crictl images | grep flannelctr 导入时如果镜像名里没有带 tag 的 reference导入后可能变成一个 digest 形式的镜像名crictl images 里显示为空 tagkubelet 按名字找不到。解决办法是导入前用 --base-name 指定一个参考名或者导入后手动 ctr images tag 一下。这条很容易踩我一般会直接这样写ctr -n k8s.io images import --base-name quay.io/flannelcni/flannel:v0.21.5 flannel-images.tar--base-name 的作用是给镜像指定默认的引用名这样导入后 containerd 里同时存在 digest 和带 tag 的引用kubelet 查镜像时能匹配到。判断节点到底用的哪种运行时不要靠猜去问 kubelet 的配置crictl info | grep runtimeName输出如果是 containerd就走 ctr是 docker就走 docker load。这一步判断错了后面所有操作全是无用功。3.4 修改 kube-flannel.yml 的 imagePullPolicy 后执行部署镜像已经导入节点但 kube-flannel.yml 默认的 imagePullPolicy 是 Alwayskubelet 每次创建 Pod 都会尝试去远程仓库确认镜像是否存在一次尝试超时可能就 30 秒甚至更久反复重试导致 Pod 一直 ContainerCreating。所以离线部署时一定要改这个策略wget -O kube-flannel.yml https://github.com/flannel-io/flannel/releases/latest/download/kube-flannel.yml sed -i s/imagePullPolicy: Always/imagePullPolicy: IfNotPresent/g kube-flannel.yml grep -n imagePullPolicy kube-flannel.ymlsed 把 yaml 里所有的 Always 改成 IfNotPresent这样 kubelet 只在本地缓存里找不到镜像时才去远程拉取。对于本来就没有远程仓库的离线集群更狠一点可以改成 Never这样 kubelet 完全不会发起网络请求直接使用本地镜像。改完就可以部署了kubectl apply -f kube-flannel.yml部署后观测 DaemonSetkubectl -n kube-flannel get pods -o wide注意新版本 yaml 里 flannel 的 namespace 是 kube-flannel不是老的 kube-system。很多老教程写的是 kube-system如果照着去查 Pod 发现一个都没有那不是你没装成功是查错了 namespace。4. flannel 镜像包导入后的 5 个高频坑现象、原因与解决4.1 Pod 一直 ImagePullBackOff本地明明有镜像却仍去拉仓库现象crictl images 里能看到 flannel 镜像状态也显示为 ready但 kube-flannel Pod 反复 ImagePullBackOffevents 里显示 Back-off pulling image。原因三个可能。imagePullPolicy 是 Alwayskubelet 忽略本地缓存或者 ctr import 时没有正确的 reference镜像虽然存在但名字是 digest又或者本机导入的镜像 tag 和 yaml 里的 tag 不一致比如 yaml 写 v0.21.5你本地打的是 v0.21.5多一个号都匹配不上。解决先看 yaml 到底引用哪个镜像名和 tag再在节点上执行 crictl images 核对。如果 tag 不一致重新 docker tag 后再次 save/load如果只是 imagePullPolicy 的问题按 3.4 节的 sed 语句改掉。遇到过多次明明镜像就在那里因为这种小不一致卡了很久所以现在我会先把这两个地方调到一模一样再部署。4.2 docker load 成功了但容器运行时是 containerd加载错地方现象节点上用 docker images 能看到 flannel 镜像docker load 也提示 Loaded image但 kubelet 调度 Pod 时依然报 image not foundPod 状态不启动。原因这个节点的 kubelet 用的是 containerdcontainerd 其实没有读取 docker 的镜像存储。docker load 之后镜像只存在于 docker daemon 自己的目录containerd 完全不知道。解决回到 3.3 节用 ctr -n k8s.io images import 导入一份。导入之后建议两侧都保留因为后续如果你想在节点上用 docker exec 进容器排障镜像还得在 docker 里再有一份。这是踩过几次才养成的习惯先 crictl info 看运行时再决定导入命令不凭印象操作。4.3 只导入 amd64 镜像ARM 节点上的 flannel Pod CrashLoopBackOff现象集群里混有 arm64 节点导入 amd64 镜像后arm 节点上的 flannel Pod 反复 CrashLoopBackOff日志里直接出现 exec format error或者没有明显错误但容器一直重启。原因镜像本身是 amd64 架构的arm64 节点执行 x86 二进制时直接无法运行应用层看不出什么原因。解决检查每个节点架构后分类导入或者干脆用 skopeo 复制整个 manifest list。skopeo 的好处是能一次性把多架构镜像存成 OCI 格式然后在目标节点上 ctr import 时自动按当前平台选择对应架构。做法放到第 5 章展开这里先记住一点混架构集群里不能只准备一个架构的镜像包。4.4 漏掉 flannel-cni-pluginPod 起来了但 pod network 配置失败现象flannel 主 Pod 处于 Running但业务 Pod 一直 ContainerCreating事件里报 failed to set up sandbox container network节点上 /run/flannel/subnet.env 也不存在。原因kube-flannel.yml 里有个 initContainer 会去下载 cni 插件二进制如果离线环境下它拉不到flannel-cni-plugin 镜像没导入这个 initContainer 就会一直失败导致 flannel Pod 永远卡在 Init:0/1。解决回到 2.1 节把 flannel-cni-plugin 也加入镜像包。初始化容器需要的不仅是镜像还有一套 CNI 二进制文件把这些文件手动放到 /opt/cni/bin 下然后创建 /etc/cni/net.d/10-flannel.conflist。不然 flannel DaemonSet 起来但实际 CNI 配置无法落盘业务 Pod 依然无法联网。4.5 多网卡节点给 flannel 选错网卡NodePort 和 Pod 间通信不通现象镜像导入成功、flannel Pod 也 Running但两个节点上的 Pod 互 ping 不通节点上的 NodePort 访问也时通时不通。原因flannel 在节点上自动选择了错误网卡比如机器上有 docker0、ens160、eth1 多张网卡flannel 把 VXLAN 隧道建在了 docker0 后面导致跨节点报文走不通。解决flannel 的 ConfigMap 或者启动参数里指定网卡。老一点的做法是在 kube-flannel-cfg 的 config 里加一条 flannel_iface新版本用容器 args 传 --ifaceens160。改完后把 flannel Pod 删掉让 DaemonSet 重建一次等 Pod IP 变化后再测 pods 互通。args: - --ip-masq - --kube-subnet-mgr - --ifaceens160改完重启后还有一个验证点去节点上看 flannel 日志里的 Interface 字段必须和你指定的网卡一致否则重启多少次都没用。5. 从单机到批量把 flannel 镜像包分发到整个集群5.1 for 循环加 scp 批处理所有 node最容易复制的做法单节点导入打通之后最固定的做法是写一个简单的分发脚本。集群节点少的时候我懒得用 ansible一个 shell 循环 scp 加 ssh 执行就搞定了。NODES192.168.1.11 192.168.1.12 192.168.1.13 USERroot MIRROR_FILEflannel-images.tar.gz for node in ${NODES}; do scp ${MIRROR_FILE} ${USER}${node}:/opt/flannel-images.tar.gz ssh ${USER}${node} tar -xzf /opt/flannel-images.tar.gz -C /opt \ ctr -n k8s.io images import --base-name quay.io/flannelcni/flannel:v0.21.5 /opt/flannel-images.tar done这里的 NODES 变量用空格分隔for 循环默认按空格拆分直接复用。ssh 里把解压和导入放在一条命令里是为了避免中途忘了解压导致第二次跑到同一台机器时文件已经被覆盖也就是说这个脚本是有幂等性的——重复执行不会出问题。这个方案的边界是节点多了以后 scp 逐个传会变慢而且每个节点都要装好 ctr 命令openssh 也不能用非 root 账号免密有问题。对 10 台以内节点够用再往上就应该换配置管理工具。5.2 用私有 registry 中转tag 前缀统一后免 load节点多的时候一台一台 load 镜像效率太低更常见的生产做法是自建私有 registry把 flannel 镜像 push 进去节点上只需要配置 containerd 的 registry 镜像地址kubelet 直接像拉公网镜像一样从私有仓库拉。自建 registry 只需要一台机器跑起来docker run -d --name registry --restartalways -p 5000:5000 registry:2然后给本地镜像重新打 tag推送到 registrydocker tag quay.io/flannelcni/flannel:v0.21.5 registry.local:5000/flannel:v0.21.5 docker tag docker.io/flannelcni/flannel-cni-plugin:v1.1.0 registry.local:5000/flannel-cni-plugin:v1.1.0 docker push registry.local:5000/flannel:v0.21.5 docker push registry.local:5000/flannel-cni-plugin:v1.1.0这之后所有节点的 kube-flannel.yml 里镜像地址统一改成 registry.local:5000/flannel:v0.21.5 这种格式就行。镜像名里域名部分会被 containerd 识别为私有仓库地址节点上需要配置 /etc/containerd/certs.d/registry.local:5000/hosts.toml或者直接把 registry 地址加入 /etc/hosts 并配好证书。私有 registry 的优势是 flannel 和其他组件镜像都走同一套标准分发路径后续升级只需要重新推送新 tag节点上不需要再手动导包。缺点是搭建和证书配置需要额外工作量如果你只是搭个测试集群这套方案显得过重。5.3 用 skopeo 离线复制镜像绕过 docker daemon 与多架构问题skopeo 是运维人员做镜像迁移的一把好手。它不像 docker 那样需要本机有 daemon 在跑直接对镜像仓库做操作用来做离线同步很合适。多架构镜像直接复制整个 manifest listskopeo copy --all \ docker://docker.io/flannelcni/flannel:v0.21.5 \ dir:/opt/flannel-images-multiarch上面的命令会把所有架构的镜像层和 manifest 原样复制到本地目录。之后把这整个目录打包传到目标机器再用 skopeo 导入到本地 containerdskopeo copy --all \ dir:/opt/flannel-images-multiarch \ containers-storage:quay.io/flannelcni/flannel:v0.21.5containers-storage: 这个前缀会把镜像导入到本机的 containers/storage 存储也就是 containerd 使用的存储后端。这样每个节点不需要提前跑 docker load直接把整个目录拷过去就能导入。skopeo copy 还有一个好处是支持不同仓库间直接搬比如从 quay.io 搬到私有 registry中间完全不落盘skopeo copy --all \ docker://quay.io/flannelcni/flannel:v0.21.5 \ docker://registry.local:5000/flannel:v0.21.5这个命令在 quay.io 可访问的机器上执行完成后私有 registry 里就有了多架构镜像。节点端依然只需要改 imagePullPolicy 为 IfNotPresentkubelet 按需从私有 registry 拉取。这里要注意 skopeo 的 --all 参数不加它遇到 manifest list 时只会复制当前平台对应的那个 manifest加了才会保留所有架构。混架构集群里丢了 --all基本就是复现第 4 章的 ARM 节点崩。6. 装完别急着收工用三个命令确认 flannel 网络真正可用镜像包导入、yaml 部署完Pod 显示 Running 不代表网络通了。我每次做完都会按顺序跑三个验证命令缺一个都不放心。kubectl -n kube-flannel get pods -o wide这个命令看 flannel Pod 是否所有节点都 Running注意看每个 Pod 所在节点是否齐全。然后看具体的日志kubectl -n kube-flannel logs -l appflannel --tail100日志里重点找一句话发送 VXLAN 相关的 UDP 包时有没有报错以及每次启动是否找到了正确的网卡 IP。如果有 conntrack 或者 bridge 相关错误说明 CNI 配置还有问题不是镜像包的锅。最后跑一个真实的跨节点 Pod 连通性测试在集群里部署一个带网络的 busybox Pod让它去 ping 另一个节点上 Pod 的 IP。两个 Pod 能互通才算 flannel 真正生效。这个流程走完后我还有一个固定习惯把这次使用的 flannel 版本、cni-plugin 版本、k8s 版本记到集群部署文档里。下次升级 k8s 小版本或 containerd 时先回头核对一遍版本对应关系确认当前镜像 tag 是否还能适配再决定要不要重新准备镜像包。很多生产集群的 flannel 问题其实都来自版本漂移后镜像 tag 和集群实际配置不匹配提前固定版本能省掉一大半排查时间。希望这份流程能帮你少走几步弯路。本文还有配套的精品资源点击获取