☰
离线装k8s集群:flannel镜像包准备与导入避坑指南
2026/9/29 19:11:41 网站建设 项目流程

简介:面向Kubernetes集群搭建场景的flannel网络插件离线镜像包,专为需要在内网或离线环境安装k8s的运维与开发人员准备,能够解决部署过程中镜像无法拉取、网络受限等常见问题。包内共3个文件,以2个tar镜像压缩包和1个yaml资源清单文件为主,tar包可直接导入节点使用,yaml文件则用于声明flannel相关网络对象并完成一键部署,整体压缩后约27.34MB,体积小巧,适合作为集群安装的前置配套。镜像版本已锁定为flannel-cni-plugin:v1.1.2与flannel:v0.21.5,搭配对应的kube-flannel.yaml,可有效规避版本不一致带来的配置风险,让用户快速拥有可用的容器网络插件。现有1613人学习下载,适用于k8s集群初始化、CNI网络配置等实操场景,值得正在搭建或维护Kubernetes环境的读者收藏使用。

1. 装 k8s 集群,最容易被 flannel 镜像卡住

在内网或离线环境搭 k8s 集群时,kubeadm init 和 kubeadm join 都能过,唯独 flannel 的 Pod 一直 ContainerCreating,kubectl describe 一看,拉取镜像超时。这就是没提前准备 flannel 必要镜像包的下场。这个镜像包不是单个镜像,而是 flannel 网络插件运行所需的完整镜像集合,包括 flannel 本体和 pause 基础镜像。本篇文章就围绕“安装 k8s 所需 flannel 必要镜像包”这件事,讲清楚到底要准备哪几个镜像、怎么在有网机器上打包、怎么在内网节点导入、以及那些让新手翻车的版本匹配和架构问题。适合正在搭 k8s 集群、卡在 CNI 网络插件这一步的运维和开发同学。

2. flannel 镜像包到底包含什么:拆开 kube-flannel.yml 看依赖

2.1 flannel 不是单镜像,核心是 flanneld 和 pause 两个角色

在准备镜像包之前,先搞清楚 flannel 在 k8s 集群里跑起来需要哪些镜像。很多人以为 flannel 只有一个镜像,实际上从 kube-flannel.yml 这个官方 manifest 里能看到,它创建了一个 DaemonSet,每个节点上跑一个 flannel Pod,这个 Pod 里有两个容器:一个是 flannel 主容器,跑 flanneld 进程,负责为每个节点分配子网、维护 VXLAN 或 host-gateway 隧道;另一个是 initContainer,通常是 pause 镜像,负责先拉起来占住 Pod 的网络命名空间。pause 镜像是 k8s 所有 Pod 都依赖的基础镜像,kubelet 在创建任何 Pod 时都会先拉取 pause。如果节点上只有 flannel 镜像而没有 pause 镜像,flannel Pod 依然起不来。这就是很多人把 flannel 镜像导进去了还是报 ImagePullBackOff 的原因。

2.2 版本匹配:k8s 版本决定 flannel 版本,flannel 版本决定镜像 tag

flannel 的镜像 tag 和 k8s 版本有对应关系。一般建议 flannel 版本不低于 k8s 小版本一个数量级,比如 k8s 1.28 配 flannel v0.24.x 或更新,k8s 1.30 配 flannel v0.25.x 以上。这个不能只看 flannel 仓库的 README,还要看 kube-flannel.yml 里实际写的 image 字段。不同版本的 flannel 镜像 tag 命名不一样,有的是 v0.24.0,有的是 v0.25.4,还有带 -amd64 或 -arm64 后缀的。把镜像包准备错了架构,arm 节点上跑 amd64 镜像,Pod 直接 CrashLoopBackOff。所以第一步先拿kubectl describe pod -n kube-flannel或者直接打开 kube-flannel.yml,看清楚当前要用的镜像完整名字。

2.3 常见镜像清单与拉取来源

以官方 kube-flannel.yml 为例,默认拉取的镜像包括:

ghcr.io/flannel-io/flannel:v0.25.4 registry.k8s.io/pause:3.9

注意是 ghcr.io 不是 docker.io,很多离线环境根本访问不了 ghcr.io,所以要先在有网机器上 pull 下来再 save 成 tar 包。还有一点:kubeadm 初始化的集群,kubelet 默认的 sandbox 镜像(pause)版本可能不是 3.9,而是 kubeadm 配置里指定的 pause 版本。如果 kubeadm init 用的是 1.28,那 pause 可能是 3.9 也可能是 3.10,看 kubeadm 版本。最稳妥的做法是:kubeadm config images list这个命令会列出 kubeadm 自己需要的基础镜像,其中就有 pause 的 tag,以那个为准。把 flannel 仓库里的 pause 版本和 kubeadm 列出来的版本都准备好,避免车辘轳问题。

3. 有网机器上拉镜像并打成 tar 包:最常用的 docker pull + save 流程

3.1 先确定目标 k8s 版本和 flannel 版本

这一步是后续所有操作的基础。我一般会先查一下 kubeadm 版本和 flannel 官方 release 的兼容性。kubeadm 版本用kubeadm version看,flannel 版本直接去 GitHub 的 flannel-io/flannel release 页面看。选定版本后,在有网机器上执行 docker pull。这里有个坑:ghcr.io 拉取慢是常态,建议配置 docker registry mirror,或者用代理拉取。拉取时注意架构,如果目标节点是 arm64,就在有网机器上执行docker pull --platform linux/arm64 ghcr.io/flannel-io/flannel:v0.25.4,并且docker images确认 Architecture 列是 ARM64。

# 在 x86 有网机器上拉取 amd64 和 arm64 两个架构的 flannel 镜像 docker pull --platform linux/amd64 ghcr.io/flannel-io/flannel:v0.25.4 docker pull --platform linux/amd64 registry.k8s.io/pause:3.9 # 如果需要 arm64 架构,加 --platform linux/arm64 重新拉

参数说明:

  • --platform指定拉取镜像的目标平台,不指定的话 docker 会拉当前机器的架构,如果后续要传到 arm 节点上就会报 exec format error。
  • registry.k8s.io/pause:3.9的 tag 必须和 kubeadm 实际使用的 sandbox 镜像一致,分不清的话直接看 kubeadm 节点上的/var/lib/kubelet/kubeadm-flags.env文件里的--pod-infra-container-image参数。

3.2 把镜像 save 成 tar 文件,注意单个文件还是分文件

拉完镜像后,把多个镜像保存到一个 tar 包里是最省事的方式。docker save 支持多个镜像一起导出的语法,但要注意,这个 tar 包不一定能被 containerd 的 ctr 命令优雅地导入。docker save 导出的是 docker 的 image format,在 containerd 环境下,如果用ctr -n k8s.io images import导入,有多数情况是能成功的,但偶尔会出现导入后镜像名和 tag 变成一个 digest 的情况。所以我更推荐用docker save对每个镜像分别导出,再统一传到目标节点。这样即使某个镜像导入失败,也不会影响其他镜像。

# 分别导出 flannel 和 pause 镜像 docker save ghcr.io/flannel-io/flannel:v0.25.4 | gzip > flannel-v0.25.4-amd64.tar.gz docker save registry.k8s.io/pause:3.9 | gzip > pause-3.9-amd64.tar.gz # 或者一个 tar 包含两个镜像(不压缩,体积大但导入兼容性好) docker save ghcr.io/flannel-io/flannel:v0.25.4 registry.k8s.io/pause:3.9 -o flannel-images.tar

逻辑说明:| gzip 是边导出边压缩,能省一半左右空间。不压缩的 tar 导入速度其实更快,因为 containerd 导入时不需要先解压。内网传输如果带宽有限就压缩,如果局域网千兆以上建议直接不压缩,省去导入时的 CPU 开销。

3.3 用 skopeo 替代 docker 拉镜像:兼容 containerd 和镜像仓库

有网机器上没有 docker 但有 podman 或者只想用更标准的方式拉镜像,可以用 skopeo。skopeo 直接操作镜像仓库,不走 docker daemon,导出的是 OCI 格式,和 containerd 的兼容性更好。尤其在目标节点只有 containerd 没有 docker 的情况下,skopeo copy 出来的目录结构可以直接用 ctr 导入,不会出现 docker save 格式导致的意外。

# 用 skopeo 拉取并直接保存为 docker-archive 格式 skopeo copy docker://ghcr.io/flannel-io/flannel:v0.25.4 docker-archive:flannel-v0.25.4.tar:ghcr.io/flannel-io/flannel:v0.25.4

参数说明:docker-archive:后面跟目标文件名,再跟一个冒号指定保存到 tar 里的镜像名和 tag。skopeo 的好处是它能识别 registry 的认证信息,也可以指定--src-creds和--dest-creds,在内网有私有 Harbor 的场景下,直接用 skopeo 把镜像从 ghcr.io 同步到私有仓库,比 docker pull + save + 上传 + load 四步操作少一半。缺点是 skopeo 在部分 Linux 发行版上要单独装 EPEL 源或编译安装,没有 docker 那么普及。整体来看,有 docker 就用 docker,想省事就配置好了 skopeo 再走 skopeo。

4. 把镜像包导入内网节点并完成 flannel 部署:ctr 导入和 kubectl apply 是一套组合拳

4.1 containerd 节点的导入:ctr -n k8s.io 才是正路

目标节点如果是 containerd 运行时(k8s 1.24 以后默认如此),直接docker load是无效的,因为 containerd 没有 docker daemon 那种 load 概念。正确姿势是使用ctr -n k8s.io images import,-n k8s.io指定命名空间是 k8s 自己的。kubelet 只从这个命名空间读镜像,导到 default 命名空间的话 kubelet 一样找不到,这是最常见的导入失败原因之一。

# 在目标节点上执行,先导入 pause 再导入 flannel sudo ctr -n k8s.io images import pause-3.9-amd64.tar.gz sudo ctr -n k8s.io images import flannel-v0.25.4-amd64.tar.gz # 验证镜像是否在 k8s.io 命名空间里 sudo ctr -n k8s.io images list | grep -E "flannel|pause"

逻辑说明:ctr 是 containerd 自带的命令行工具,-n k8s.io表示在 k8s 命名空间内操作。导入后镜像会出现在 containerd 的内容存储里,但 kubelet 真正读取的是经过 CRI 插件暴露的镜像列表,所以导入完成不等于节点就认识了,最好再用crictl images确认一下,crictl走的是 CRI 接口,和 kubelet 看到的一致。

参数说明:如果导入时报unpacking failed: archive/tar: invalid tar header,大概率是 tar 包损坏或者压缩格式不对。docker save 默认不压缩的 tar 是标准格式,gzip 之后的 tar.gz 也没问题,但 skopeo 导出的 docker-archive 格式如果传输出错,解压就会失败。这类问题往下看第 5 章的排查清单。

4.2 从 kube-flannel.yml 部署:先改 image 名字再 apply

镜像导入后,接下来是部署 flannel。使用官方 kube-flannel.yml 时,注意把里面的 image 地址改成和导入镜像一致的完整名称。如果你导入的镜像 tag 是 v0.25.4,那 manifest 里的默认 tag 要是对不上,kubelet 会重新尝试去远端拉取。内网环境下这个拉取会一直卡住。

# 修改 kube-flannel.yml 里的镜像地址为内网实际存在的 tag sed -i 's#ghcr.io/flannel-io/flannel:v0.25.4#ghcr.io/flannel-io/flannel:v0.25.4#' kube-flannel.yml # 然后部署 kubectl apply -f kube-flannel.yml

这段 sed 是幂等操作的占位写法,实际要看你的 kube-flannel.yml 里写的 tag 和你导入的 tag 是否一致,不一致就改成一致。如果之前是 grep 出来镜像名镜像,也可以用grep -n image kube-flannel.yml找到所有 image 行,逐一确认。

部署后检查状态:

kubectl get pods -n kube-flannel -o wide kubectl logs -n kube-flannel -l app=flannel

flannel Pod 日志里看到SubnetSet创建成功和Starting IP allocation这类输出说明 flanneld 启动是正常的。此时再看节点状态,如果之前 node 是 NotReady,现在应该翻成 Ready。如果 node 还是 NotReady,看 kubelet 日志和 flannel 日志配合分析,这属于搭配问题而不是镜像问题。

4.3 如果从私有仓库拉镜像:导入 registries.yaml 或直接改 imagePullPolicy

有些团队会把 flannel 镜像包推到内网 Harbor,然后在节点上配置/etc/containerd/certs.d或者 containerd 的registries.yaml。这种方式的好处是以后有别的镜像也往仓库推,节点不用逐一导入 tar 包。配置私有仓库后,kube-flannel.yml 里的 image 字段要写成harbor.example.com/flannel/flannel:v0.25.4。

# /etc/containerd/certs.d/harbor.example.com/hosts.toml server = "https://harbor.example.com" [host."https://harbor.example.com"] capabilities = ["pull", "resolve"] skip_verify = false

注意修改 containerd 配置后要systemctl restart containerd。这个方式和离线条子导入 tar 包是两条路,B 场景适合集群还在持续扩容,A 场景适合一次性把镜像吐给固定节点。哪条路更省事就看你有多少节点,节点超过 5 个我统一建议推私有仓库,否则后续每个节点导入 tar 包会很疲惫。

5. flannel 镜像包安装避坑:五种翻车现象、原因与处理

5.1 现象:Pod 一直 ImagePullBackOff,describe 里显示拉取失败

原因:节点上没有被 kubelet 识别到的 flannel 镜像。镜像导入到了 containerd 但命名空间不是 k8s.io,或者ctr导入后没跑crictl images确认,kubelet 不认。

解决:先用sudo ctr -n k8s.io images list | grep flannel确认存在,再sudo crictl images | grep flannel确认 CRI 能看到。如果 ctr 能列出但 crictl 看不到,说明镜像 tag 是 docker 格式的旧格式,重新用ctr images import导入时加上--all-platforms参数,或者改一下 tag 再导入。

5.2 现象:flannel Pod 起来后 CrashLoopBackOff,日志报exec format error

原因:镜像架构不对,arm64 节点上导入了 amd64 的 flannel 镜像。

解决:在目标节点上uname -m确认架构,重新拉对应平台的镜像。如果内网传输时把 amd64 和 arm64 混淆了,重拉即可。注意 arm64 的 flannel 镜像 tag 有的版本带-arm64后缀,有的不带,直接看镜像的 Architecture 字段最准。

5.3 现象:flannel Pod 正常,但 node 始终 NotReady,报node controller sync

原因:kube-flannel.yml 里默认的 Pod 网段10.244.0.0/16和 kubeadm init 时指定的--pod-network-cidr不一致。flannel 拿到冲突或错误的子网,kubelet 初始化 pod CIDR 失败。

解决:kubeadm init 时如果用了--pod-network-cidr=10.244.0.0/16,那 kube-flannel.yml 里 net-conf.json 那段的 Network 字段也要是 10.244.0.0/16。这个不是镜像包本身的问题,但通常是在准备镜像包之前就要确认的配置项,建议把 kube-flannel.yml 的 net-conf.json 和 kubeadm-config.yaml 并排检查一遍。改完后先kubectl delete -f kube-flannel.yml再重新 apply,光改 configmap 不会热生效。

5.4 现象:内网节点上ctr images import时卡住不动,或者报dial tcp: lookup xxx失败

原因:镜像 tar 包里包含了多层 manifest 列表(manifest list),ctr 导入时尝试解析所有的 manifest 和索引,如果 tar 包是从一个有认证的私有仓库导出的,导入时可能触发一次校验网络请求。本质上还是内网环境 DNS 或出网策略的问题。

解决:导入前先tar -tf flannel.tar.gz看看里面是否有包含 JSON 索引和 layer 的完整结构,确保 tar 包完整。导入时加--no-unpack参数先只导入镜像元数据,再让运行的时候按需解包。如果导入时确实想去掉 manifest list 的干扰,用docker load在有网机器上加载一次再用docker save重新导出单个镜像的 tar,往往就正常了。

5.5 现象:kube-flannel.yml apply 成功但 flannel DaemonSet 的 Pod 是 0 个

原因:kube-flannel.yml 里的 namespace 是kube-flannel,但kubectl apply时没有先创建这个 namespace,或者 RBAC 的 ServiceAccount 没起来。flannel 需要 ClusterRole 绑定才能读 node 和创建 CRD,APIServer 拒绝后 Pod 调度不出来。

解决:先kubectl create ns kube-flannel,然后kubectl apply -f kube-flannel.yml。同时确认kubectl get sa -n kube-flannel里能看到 flannel 的 ServiceAccount。这个坑不是镜像包的问题,但是离线条环境手动 apply 时极其常见。如果之前 apply 过但没建 namespace,APIServer 会有报错记录,kubectl describe daemonset.apps -n kube-flannel就能看到。

6. 镜像包准备的高级用法:多节点批量分发 + 验证 flannel 网络是否真的通了

镜像包离线导入这件事,到“kubectl get nodes 全 Ready”不算完。flannel 镜像包本身很小,但集群里的场景是多个节点都要有同一份镜像。节点多了以后,挨个scptar 包再ctr images import是不现实的。我一般是把 tar 包放到内网一个 HTTP 服务或直接放到 Harbor,然后写一个简单的分发脚本在各节点上执行。脚本的核心是先把 tar 包拉到本地,再逐节点导入:

#!/bin/bash # 每个节点上执行,从内网 HTTP 服务拉取 flannel 镜像包并导入 containerd cd /tmp wget -q http://192.168.1.10:8080/k8s/flannel-v0.25.4-amd64.tar.gz sudo ctr -n k8s.io images import /tmp/flannel-v0.25.4-amd64.tar.gz sudo crictl images | grep -E "flannel|pause" && echo "导入成功"

参数说明:wget 走内网 HTTP 不占用 NameServer 资源,比 scp 逐节点省事。crictl images没输出时说明导入有问题,脚本里就安排退出码非 0,方便做批量失败检测。这个脚本放到 cron 或 Ansible 里每个节点跑一遍即可。

镜像包就绪后,验证 flannel 网络是否真的通,推荐三步走。先在任意一个节点上创建两个测试 Pod:

kubectl run test-a --image=busybox --command -- sleep 3600 kubectl run test-b --image=busybox --command -- sleep 3600 kubectl exec -it test-a -- ping <test-b的Pod IP>

如果 ping 通,说明 flannel 的 VXLAN 隧道在同一节点或跨节点都正常。然后再验证跨节点的 service 解析:kubectl exec -it test-a -- nslookup kubernetes.default。最后一步是检查 flannel 接口和路由表:ip addr show flannel.1和ip route里有没有 10.244.0.0 网段的路由。这三个检查都是实际生产里的日常巡检项,不要省。

至于 monitring,如果集群里已经装了 Prometheus,可以给 flannel 开 metrics 采集。flanneld 默认在 9091 端口暴露/metrics,在 kube-flannel.yml 的 DaemonSet 里加上annotations: prometheus.io/scrape: "true"和prometheus.io/port: "9091",Prometheus 就能采集到每个节点的 flannel 隧道状态。这一步做不做取决于你的监控体系是否完善,但在接入之后看flannel_network_subnet和interface相关指标,比每次kubectl logs翻历史都快。

我做离线环境从来都是先把镜像包准备成“多点分发”的形式,容器本地导入这套流程只是第一版能跑通的最小闭环。有一条血泪经验是:每个节点导入完都要跑crictl images人工或者说脚本确认一次,不要做完一轮就急着 join 节点,等所有节点 Ready 了再往后走。镜像版本和 kubeadm 的 pause 版本一年能坑我好几次,现在每次搭环境前都会先把kubeadm config images list输出和 flannel 镜像 tag 抄在一张纸上,逐一核对过再动手。希望帮到你,少踩一个算一个。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询