
简介面向Kubernetes管理员与运维工程师这份calico-image-v3.25.0离线包用于无外网或网络受限环境下快速部署Calico网络插件。Calico基于BGP路由为容器集群提供高性能网络与安全策略控制离线包可解决在线拉取镜像超时或失败的问题适配内网生产、离线机房等场景。压缩包采用zip格式共3个文件tar镜像聚合了Felix、BIRD、Typha等核心组件镜像yaml文件包含Calico部署所需资源定义可按实际环境调整txt文档提供镜像导入、插件启用到排错的完整说明。包体总大小约187.24MB结构紧凑便于传输与存档。目前已有453人学习下载。使用后可直接导入镜像至本地仓库按需修改yaml配置再依据txt文档完成Calico v3.25.0的部署验证大幅降低离线网络插件的实施成本并确保集群网络策略稳定可用适合在受限网络中搭建或扩容K8s集群的团队。1. calico-image-v3.25.0离线包内网集群不用再等外网拉镜像给一台完全没有外网的Kubernetes集群装Calico是所有离线部署运维躲不过去的一道坎。你按教程写好kubectl apply -f calico.yaml等半天却发现calico-node的Pod全部ImagePullBackOffkubelet的events里只有一条反复出现的“Failed to pull image”。原因很简单集群拉不到外网镜像而Calico的安装清单里写的是上游仓库的完整镜像地址。calico-image-v3.25.0离线包要解决的就是这件事在有网的机器上把Calico运行需要的所有容器镜像拉下来、打标签、归档压缩再用移动介质或内网文件服务器送进离线集群导入到容器运行时里让Calico在完全不碰外网的情况下正常跑起来。这篇文章写给两类人一是刚接手离线K8s集群、正在跟ImagePullBackOff死磕的新手照着步骤能把包做出来、导进去二是已经在做镜像同步的运维熟手我重点讲版本、架构、命名空间这类细节还有我踩过的几个真实坑。整个流程不复杂但翻车点全藏在镜像名、tag、容器运行时差异这些不起眼的地方。2. 离线包里到底该放哪些镜像组件清单与架构选型先定死2.1 Calico v3.25.0在K8s里跑通需要哪几个镜像先别急着找工具打包得先搞明白Calico v3.25.0在Kubernetes节点上运行到底依赖哪几个容器镜像。安装用的那份manifest里image:字段后面出现的内容就是全部答案。我一般是直接打开将要用于离线安装的YAML文件用一条grep把镜像列表全部抠出来而不是凭记忆去猜。grep -nE ^\simage:\s calico.yaml | sort -u这条命令会打印出calico.yaml里所有镜像定义的行号和内容。注意用sort -u去重不同控制器可能引用同一个镜像。执行完之后你会看到一批带完整仓库地址的镜像名核心组件通常是这几个calico/cni负责把Pod网卡创建出来kubelet通过CNI插件调用它calico/node是核心代理里面包含Felix、BIRD这些进程负责下发路由和网络策略calico/kube-controllers负责回收IP、同步策略等控制面逻辑calico/typha在高并发场景下为node组件做API Server的访问代理小集群可以不部署。每个镜像的职责不同但离线包里一个都不能少否则Calico组件会在启动时报“找不到插件”或“控制器连接失败”之类的错。打包前先对着这份清单确认你的安装YAML用到了几个镜像版本tag是不是都是v3.25.0。我见过有人只打包了node和cni结果kube-controllers拉不到镜像整个集群的策略功能直接瘫痪。2.2 版本匹配v3.25.0与你的K8s版本怎么对上Calico的每个大版本都声明了自己兼容的Kubernetes版本范围v3.25.0属于3.25系列这套离线包理论上只保证和特定范围内的K8s版本配合良好。你集群的K8s版本如果落在这个范围之外安装时可能遇到CRD不识别、API版本冲突、node组件反复重启等问题。核对方法很笨但有效先在目标集群上查版本再对照Calico发布说明里的兼容矩阵。查K8s版本用下面这条kubectl version --short重点看Server Version那一行。得到K8s版本后去Calico对应版本的Release Notes里找“Compatibility”一节确认v3.25.0是否覆盖你的版本。注意这是一次离线安装集群里的K8s版本已经是既成事实你不能为了凑Calico版本去升级集群所以在选离线包版本之前先确认兼容性别装到一半发现CRD格式对不上。另外一个容易忽视的点Calico的kubectl apply会用到的CRD也只在特定版本存在比如某些字段从v3.25开始废弃。所以离线包里的manifest文件必须和镜像tag是同一个版本不要拿v3.24的YAML配v3.25.0的镜像这种混搭导致的报错往往很隐蔽日志里看不出明确的版本提示。2.3 架构选型amd64与arm64的离线包为什么不能偷懒合并Kubernetes集群如今很少是清一色的x86了控制节点用amd64、部分计算节点是arm64或者整集群就是arm架构这种场景越来越常见。无外网环境下最容易犯的错误是在一台amd64机器上docker pull完镜像再save然后拿去给arm64节点用启动时直接报exec format error完全跑不起来。这里要明白docker save的一个行为它导出的是本地实际拉取到的镜像不是“这个镜像名对应的所有平台版本”。你在x86机器上pull一个多架构镜像Docker默认拉取的是amd64那份镜像层save出来的tar包里也只有amd64的内容。arm64节点加载这个tar等于让arm64的CPU去执行x86的二进制结果就是exec format error。所以离线包的目录结构我会按架构分开命名上直接写清楚calico-image-v3.25.0-amd64.tar.gz calico-image-v3.25.0-arm64.tar.gz制作之前先确认集群的架构构成一条命令就能看全kubectl get nodes -o wide看ARCH列如果集群里两种架构都有那就分别打两个离线包。别想着合并成一个docker save不支持把一个本地镜像导出成多架构tar想合并只能用支持manifest列表的工具从registry层直接拉取但那样对网络和工具要求都更高离线场景不值当。按架构分开放导入时各自对号入座最省心。3. 在有网机器上打离线包从拉镜像到归档压缩的一条龙脚本3.1 先从部署清单里把镜像名全部抠出来制作离线包的第一步是拿到准确的镜像清单这一步不做好后面全是白干。我前面说过用grep去yaml里抠镜像名但更推荐把镜像名批量提取出来重定向到一个文件里后面拉取脚本直接读取这个文件避免手写漏项。如果你拿到的Calico部署文件是一个单独的calico.yaml用下面这条命令可以提取出干净的唯一镜像列表grep -E ^\simage:\s calico.yaml \ | sed -E s/.*image:\s// \ | sed -E s///g \ | sort -u images-full.txt这条命令做了几件事第一段grep找出所有包含image:的行第二段sed把行首的空格和image:前缀去掉第三段把引号剥掉最后排序去重写入文件。images-full.txt里就是完整的镜像地址列表含仓库地址和tag。拿到这个文件后打开看一眼确认里面的版本号是不是v3.25.0有没有意外的镜像源。注意images-full.txt里的镜像名是你安装YAML里声明的那份Calico的组件镜像名是固定规范的但仓库地址可能因发行版或镜像源不同而有差异。打包机上执行docker pull时就用这些完整地址不要自己改写否则可能出现解析不到的问题。3.2 用脚本批量拉取并统一镜像tag镜像清单确认后接下来就是批量拉取、重打tag、save归档。我在这一节给出的脚本兼顾了三种需求一是从前面生成的清单读取镜像二是把镜像重打成不带仓库前缀的短名三是按架构区分输出目录。为什么要重打短名因为导入到离线节点后本地镜像名越短越不容易写错后续改yaml时也更好匹配。#!/usr/bin/env bash set -euo pipefail VERSIONv3.25.0 ARCH${1:-amd64} WORKcalico-image-${VERSION}-${ARCH} LISTimages-full.txt mkdir -p ${WORK}/images # 逐行处理镜像清单每行格式为“完整镜像地址” while IFS read -r full_image; do echo pull ${full_image} docker pull ${full_image} # 把仓库地址剥掉只保留 组件名:tag short_name$(basename ${full_image}) echo tag as ${short_name} docker tag ${full_image} ${short_name} # 按短名保存导入后直接可用 tar_name${short_name%%:*} docker save -o ${WORK}/images/${tar_name}.tar ${short_name} done ${LIST} echo archive tar -czf ${WORK}.tar.gz -C ${WORK} images/ echo checksum sha256sum ${WORK}.tar.gz脚本逻辑不复杂逐行读镜像地址pull完用basename取出最后一个斜杠后面的部分比如your-registry.local/calico/node:v3.25.0会变成node:v3.25.0随后用这个短名打tag并save。每个组件单独生成一个tar包最后统一压缩成一个tar.gz。强烈建议加参数ARCH${1:-amd64}打arm64包时执行bash build-offline.sh arm64输出文件名和目录都带上架构后缀避免后续拿错包。这个脚本有三个参数相关的细节值得说明。tar_name${short_name%%:*}的意思是取冒号前的部分作为文件名例如node:v3.25.0会生成node.tar这么做的目的是多个镜像虽然版本相同但组件名必须区分。目录命名里的${ARCH}变量决定了产物归档在哪个目录amd64机器打出来的包不要拿去给arm64节点用。整个脚本用set -euo pipefail开头其中任何一条命令失败都会立即中止防止拉取一半继续打包出残缺的离线包。执行完脚本后在当前目录会生成一个名为calico-image-v3.25.0-amd64.tar.gz的压缩包里面是四个tar包和一个压缩时的完整归档。下一步是生成校验值脚本里已经通过sha256sum打印出来了把这一串值连同压缩包一起记录下来传到内网后再算一次对比能有效发现传输过程中文件损坏的问题。3.3 归档、压缩与校验tar.gz还是裸tar怎么选离线包用裸tar还是tar.gz取决于你的传输通道和节点数量。裸tar的好处是导入时可以少一步解压docker load或者ctr import都支持直接读tar文件tar.gz的好处是体积通常小50%左右适合U盘拷贝或者跨网络传输。我一般做两层考虑节点少、直接U盘拷贝用裸tar加目录结构节点多、走内网文件服务器分发用tar.gz。压缩这一步真没什么玄学tar.gz足够可靠。要注意的是磁盘空间预估save出来的四个tar包加在一起通常有2GB到3GB压缩后大概1GB多。打包机上要预留至少两倍的空间因为在归档之前docker的镜像存储目录里已经占了一份save的tar文件又占一份最后压缩时又需要临时空间。空间不足时脚本会卡在tar命令但不会给出明确提示你只能看到磁盘写满。校验值一定要生成并单独保存。前面脚本输出的sha256sum结果加上一个-c参数还能在校验时自动比对sha256sum -c checksum.txtchecksum.txt内容就是一行哈希值 calico-image-v3.25.0-amd64.tar.gz。在目标环境里执行这条命令输出OK说明文件完整输出FAIL说明传输过程中已经被损坏这时候不要导入重新拷一遍。4. 把离线包送进节点crictl、ctr与docker导入的差异4.1 先判断节点用的是 containerd 还是 dockerd离线包制作完成只是第一步真正让Calico跑起来的是目标节点上的容器运行时。先搞清楚节点用的是containerd还是dockerd导入命令完全不同混用是最常见的翻车原因。判断方法很简单直接问节点上的crictlcrictl info | grep runtimeName如果输出是containerd说明节点用containerd作为容器运行时如果输出是docker说明走dockerd。注意crictl本身不负责导入镜像它只是CRI的客户端真正导入要走容器运行时自己提供的命令。还有一种情况节点同时装了dockerd和containerd但kubelet配置的是containerd这时候你即使docker load成功了kubelet还是看不到镜像因为kubelet只会去CRI对应的容器运行时里找。判断完运行时还要记住另一个关键点每个节点都要导入离线包。Calico的镜像不是集群共享的而是每个节点本地有一份kubelet启动Pod时从节点本地拉取。集群有十台节点就得在十台节点上各导入一遍。所以离线包分发的路径要提前规划好别等到了节点上才发现包里只有镜像没有分发脚本。4.2 三种导入方式的命令与易错点导入命令本身不复杂但每一条都有对应的坑。先看containerd环境的正确姿势核心是ctr命令加-n k8s.io参数# 先解压 tar.gz tar -xzf calico-image-v3.25.0-amd64.tar.gz # 导入每个组件的 tar 包注意命名空间不能省 ctr -n k8s.io images import images/node.tar ctr -n k8s.io images import images/cni.tar ctr -n k8s.io images import images/kube-controllers.tar ctr -n k8s.io images import images/typha.tar这里-n k8s.io是导入操作的核心。kubelet通过CRI访问的镜像是存在k8s.io这个命名空间下的ctr默认操作的命名空间是default。如果你不带-n k8s.io镜像也确实导入成功了查看也是正常的但kubelet就是看不到Pod该ImagePullBackOff还是ImagePullBackOff。这个坑我踩过一次当时反复导入了好几遍最后才发现是命名空间问题。如果节点容器运行时是dockerd导入命令换成docker load -i images/node.tar docker load -i images/cni.tar docker load -i images/kube-controllers.tar docker load -i images/typha.tardocker load不需要指定命名空间因为它管理的镜像是全局的。导入完成后用docker images | grep calico验证即可看到短名镜像node:v3.25.0、cni:v3.25.0这些。不论哪种导入方式导入完都建议用crictl确认一遍kubelet视角下的镜像列表crictl images如果这一步能看到node、cni这些条目说明kubelet已经能看到镜像后面安装Calico就有基础了。4.3 让Calico清单引用本地镜像改yaml还是开私有仓库镜像导入节点之后还有最后一道工序calico.yaml里声明的镜像名是带仓库地址的完整路径而你现在节点上的镜像是短名。两者对不上kubelet还是会按yaml里的完整地址去找镜像导致导入白做。解决方式有两条路按集群规模选。方案一是直接把yaml里的镜像地址改成短名适合小集群或一次性安装。我用一个循环把它处理干净VERSIONv3.25.0 for name in cni node kube-controllers typha; do sed -i s#your-registry.local/calico/${name}:${VERSION}#${name}:${VERSION}#g calico.yaml done这段sed的作用是把yaml里所有含your-registry.local/calico/组件名:v3.25.0的字符串替换成组件名:v3.25.0。your-registry.local是你在制作离线包时改为实际的仓库地址。执行完以后用grep -E image: calico.yaml | sort -u确认yaml里应该只剩cni:v3.25.0、node:v3.25.0这些短名没有仓库前缀。方案二是把镜像推到内网私有仓库适合节点多、后续还要扩容的环境。导入短名后打上内网仓库的完整标签再pushfor name in cni node kube-controllers typha; do docker tag ${name}:v3.25.0 registry.local:5000/calico/${name}:v3.25.0 docker push registry.local:5000/calico/${name}:v3.25.0 done然后把yaml里的镜像地址全部替换成内网仓库地址方式同上。这两种方案没有绝对优劣小集群图省事用方案一生产环境要长期演进就上方案二。不管是哪种方案替换完yaml之后都要顺手检查一下yaml里每个容器的imagePullPolicy如果是Alwayskubelet会尝试从镜像仓库重新拉取离线环境下又会卡住稳妥的做法是把它改成IfNotPresent。5. calico离线包落地避坑从导入失败到节点NotReady的5个真问题5.1 Pod一直ImagePullBackOff镜像名和yaml里的仓库地址对不上现象kubectl get pods -n kube-system看到calico-node全部ImagePullBackOffkubectl describe pod的Events里显示Failed to pull image并且失败信息里的镜像名带着仓库域名恰好是你没有访问权限的那个外网地址。原因节点上确实导入了镜像但Calico的DaemonSet清单里image:字段写的是带仓库地址的完整名字kubelet不会自动把完整地址的镜像“降级”成短名而是原样去拉取。短名镜像和完整地址镜像在容器运行时里被当作两个不同的镜像。解决先看节点上实际导入进去的镜像叫什么用crictl images或者ctrl -n k8s.io images list查一下然后二选一要么把yaml里的image字段替换成节点上的短名要么用ctr -n k8s.io images tag给镜像补一个完整地址的tag。我习惯用前一种因为yaml是一次性改镜像tag补多了之后升级时容易混乱。5.2 ctr导入的镜像kubelet看不见忘了指定k8s.io命名空间现象用ctr images import导入离线包命令成功且用ctr images list能看到镜像但crictl images列表里空空如也Pod照样拉不到镜像。原因ctr默认操作的是default命名空间而kubelet走CRI读取的是k8s.io命名空间。两个命名空间互相看不到对方持有的镜像等于导了个寂寞。这个坑在containerd环境里极常见因为很多人用ctr时没看帮助文档不知道有个命名空间参数。解决导入命令加上-n k8s.io重新导入一遍ctr -n k8s.io images import images/node.tar导入完成用crictl images验证。注意不同发行版可能把kubelet的命名空间改成别的值以节点上crictl info输出的实际值为准不要看到一条教程就无脑套用。5.3 tar跨架构导入后报exec format error现象节点上镜像导入成功calico-node Pod启动后CrashLoopBackOff日志里反复出现exec format error或者cannot execute binary file。原因离线包是在amd64机器上打的拿去给了arm64节点。前面说过docker save只保存本机拉取的单架构镜像所以tar包里的二进制和节点CPU架构不匹配。解决先确认节点系统架构uname -m看输出是x86_64还是aarch64。如果集群是arm64回到有网机器上用脚本带架构参数重新打一个arm64包bash build-offline.sh arm64重打之前还要确认打包机本身是arm64机器或者在arm64环境里重新执行docker pull。否则你只是把目录名改成了arm64里面的镜像层仍然是amd64的。这个点不搞清楚打包打三遍都是废包。5.4 calico-node反复CrashLoopBackOff组件镜像版本与实际校验不一致现象所有镜像都是v3.25.0导入也成功了yaml里的image字段也对上了但calico-node就是起不来事件里要么是version check failed要么是failed to find plugin。原因manifest里的cni插件版本和node组件事先没有对齐比如calico-node的镜像tag是v3.25.0但cni镜像的tag是v3.24.2或者你用了一份与镜像版本不匹配的配置清单。Calico的node进程启动时会检查publish版本和二进制版本不一致就会直接退出。解决把yaml里所有image:字段的tag统一成完全相同的版本我用一条命令检查所有镜像版本是否整齐grep -E image:.*calico calico.yaml | sort -u如果发现混着不同版本重新替换所有tag后再apply。这里有个细节很多发行版会把cni的镜像tag写成带-k8s后缀或者别的版本号做法是按实际运行的清单重新生成离线包的镜像清单而不是手工指定。5.5 后续升级yaml导致节点重新拉外网imagePullPolicy的坑现象离线导入后Calico运行正常某天你修改了Calico配置重新kubectl apply了新的yaml随后发现部分节点的calico-node开始拉不到镜像Pod被驱逐。原因新的yaml里镜像tag变了比如从v3.25.0改成v3.25.1节点本地没有这个版本kubelet只能去远端仓库拉。也可能yaml里容器的imagePullPolicy被渲染成Always即使tag没变每次apply都会触发远端检查。解决升级时离线包要跟着yaml一起换版本单独换yaml不带镜像包必然导致拉取失败。如果只是apply了同名版本但策略是Always把策略改回IfNotPresent再重启sed -i s/imagePullPolicy: Always/imagePullPolicy: IfNotPresent/g calico.yaml改完后重新apply等DaemonSet滚动完成再观察Pod状态。注意这个sed只对calico.yaml生效不要拿去全局替换其他应用的yaml否则会把别的服务的拉取策略也改掉。6. 装完先别收工三步验证离线Calico网络已通再加一个更省事的后续维护验证离线包装成的Calico是否真正可用不能只看Pod Running网络要通、策略要生效才算彻底落地。我固定做三步验证缺一步都不踏实。第一步看组件Pod状态。所有节点的calico-node都处于Runningcalico-kube-controllers也Ready这是基础。kubectl get pods -n kube-system -l k8s-appcalico-node -o wide确认每台节点都有对应的Pod。第二步查BGP或路由状态。如果集群用了BGP模式用calicoctl查看节点间的peer状态是否Established。没有现成calicoctl二进制时可以在节点上用kubectl exec进calico-node容器执行kubectl exec -n kube-system deploy/calico-kube-controllers -- calicoctl node status更好用的方式是在线获取节点列表确认Node的BGP会话不是Down状态。第三步真正的网络联通测试。跨节点跑两个测试Pod指定nodeSelector把它们调度到不同节点然后从一个Pod ping另一个Pod的ClusterIP和Pod IPkubectl run test-a --imagebusybox --restartNever -- sleep 3600 kubectl run test-b --imagebusybox --restartNever -- sleep 3600 kubectl exec test-a -- ping -c 3 test-b-pod-ip能通说明CNI网络面正常。之后再验证策略创建一条只允许特定namespace互通的NetworkPolicy看不符合规则的访问是否被拒绝。通过后删掉策略恢复原状。两步网络都通Calico才算真正装好离线包的使命也到此完成。这里再补一个后续维护的加分技巧当集群以后要加新节点不需要再拿tar包手动导入一遍把离线包里的镜像推到内网registry新节点的容器运行时直接从这个内网仓库拉取。这样你只需要在初始化节点时配置好registry地址Calico组件就能在新节点上被自动调度拉取。具体命令就是前面4.3节里的docker tag和docker push提前做一次省掉后续每台节点重复的劳动。我现在的习惯是每次制作离线包都会把最终实际使用的images.txt存一份快照连同yaml文件放在同一个目录下次升级版本时直接对比新旧清单能一眼看出哪些镜像tag变了避免升级时漏带镜像。这个习惯救过我一次因为EKS升级时漏掉了一个typha镜像导致新节点反复起不来。做离线部署没什么玄学能提前想到的坑都列出来踩过一遍后面自然顺。希望这篇笔记帮到你少走一段这种为了镜像折腾到凌晨的路。本文还有配套的精品资源点击获取