解决containerd拉取HTTP私服镜像失败问题以Harbor为例的避坑实践最近在帮几个团队迁移到Kubernetes 1.24版本时遇到了一个看似简单却让人头疼的问题Pod创建失败报错信息总是提示“ImagePullBackOff”或“ErrImagePull”。仔细一看日志发现containerd死活拉取不到我们内部Harbor仓库的镜像。这场景太熟悉了——从Docker切换到containerd后那些原本在Docker下运行良好的HTTP私服镜像地址突然就变得“不可信”了。如果你也正在Kubernetes新版本环境中挣扎于containerd的镜像拉取配置特别是面对那些没有配置TLS证书的HTTP私有仓库这篇文章就是为你准备的。我会带你深入问题根源不只是给出配置片段而是让你理解containerd与Docker在处理镜像仓库时的本质差异并通过Harbor这个典型例子手把手演示如何系统性地排查和修复问题避免在容器化部署的关键环节踩坑。1. 问题根源为什么containerd对HTTP如此“挑剔”要解决问题首先得明白问题从何而来。很多从Docker迁移过来的开发者会感到困惑同样的Harbor地址用docker pull就能成功换成crictl pull或者Kubernetes Pod就失败。这背后的核心差异在于安全模型的演进和运行时架构的分离。Docker作为一个集成的容器引擎其设计包含了从客户端到守护进程的完整逻辑它对镜像仓库的协议处理相对“宽松”。特别是对于本地网络或明确指定的HTTP仓库Docker可以通过--insecure-registry参数或在daemon.json中配置来绕过TLS验证。这种便利性在开发和测试环境中很常见但也带来了一定的安全风险。而containerd的定位是一个专注、模块化的容器运行时。它被设计为更底层、更标准的组件通常通过CRI容器运行时接口被Kubernetes调用。在安全方面containerd遵循了更严格的原则默认情况下所有非localhost的镜像仓库通信都必须使用HTTPS。这是为了符合现代云原生基础设施的安全基准防止中间人攻击和镜像篡改。这种设计哲学上的差异直接导致了配置方式的不同。Docker的配置是“中心化”的修改daemon.json并重启Docker服务即可全局生效。而containerd的配置则更加细粒度并且针对不同的客户端工具配置的加载路径和生效范围可能不同。这是第二个关键点crictlKubernetes CRI客户端和ctrcontainerd原生客户端读取的配置可能是不同的。很多人在配置了/etc/containerd/config.toml后发现crictl pull可以了但ctr image pull依然失败就是这个原因。具体到拉取HTTP私服镜像失败错误信息通常是这样的Failed to pull image http://harbor.example.com/library/nginx:latest: failed to resolve reference http://harbor.example.com/library/nginx:latest: failed to do request: Head https://harbor.example.com/v2/library/nginx/manifests/latest: x509: certificate signed by unknown authority或者更直接的http://server:port/v2/: dial tcp: lookup server on 127.0.0.53:53: no such host注意看错误信息里它自动把http尝试升级成了https然后因为证书问题或连接失败而报错。这就是containerd在“挑剔”的体现。那么为什么我们还需要使用HTTP私服呢在现实的企业环境中尤其是在开发测试、离线环境或内部安全网络中为每个私有仓库部署和维护TLS证书特别是由可信CA签发的证书可能成本过高或过于复杂。Harbor等仓库管理软件支持纯HTTP模式便于快速搭建和集成。因此学会正确配置containerd以支持HTTP仓库是一项必备的运维技能。2. 核心配置深入解读containerd的config.toml解决问题的钥匙就在/etc/containerd/config.toml这个配置文件里。这个文件的语法是TOML格式结构清晰但嵌套较深。我们需要重点关注的是[plugins.io.containerd.grpc.v1.cri.registry]这个配置块它专门用于定义CRI插件也就是Kubernetes用来跟containerd打交道的部分如何与镜像仓库交互。这个配置块下主要有三个重要的子表tablemirrors: 定义仓库的镜像端点Endpoint可以用于配置镜像加速或指定特定仓库的访问地址。configs: 定义针对特定仓库的配置如认证信息auth和TLS设置。这里就是配置跳过TLS验证的关键所在。auths: 存储仓库的认证凭证用户名/密码或token通常更推荐用configs来集成配置。下面我们用一个具体的Harbor实例来拆解配置。假设我们的内部Harbor仓库地址是http://harbor.internal.com:8080并且没有配置HTTPS。2.1 配置镜像端点Mirrorsmirrors的配置告诉containerd“当你要访问某个仓库时请去我指定的这个地址拉取镜像。”对于私有仓库我们通常不是做镜像加速而是明确指定使用HTTP协议访问该仓库的地址。[plugins.io.containerd.grpc.v1.cri.registry.mirrors] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.harbor.internal.com:8080] endpoint [http://harbor.internal.com:8080]关键点解析harbor.internal.com:8080这是镜像仓库的主机名或主机名加端口。当Kubernetes Pod中镜像名为harbor.internal.com:8080/project/app:v1时containerd会匹配这个键。endpoint [http://...]这里显式指定了使用http://协议。这个配置至关重要它直接阻止了containerd默认尝试使用https://去连接。注意mirrors的键即仓库地址必须与Pod中镜像名称的仓库部分完全匹配。如果你的Harbor使用了非标准端口或者IP地址这里需要相应地配置。2.2 配置TLS跳过验证Configs仅有mirrors指定HTTP端点还不够因为containerd在连接到这个端点时依然会尝试进行TLS握手尽管我们用的是HTTP。对于自签证书或纯HTTP仓库我们需要明确告诉containerd“连接这个仓库时跳过TLS证书验证。”这需要在configs子表中进行配置[plugins.io.containerd.grpc.v1.cri.registry.configs] [plugins.io.containerd.grpc.v1.cri.registry.configs.harbor.internal.com:8080.tls] insecure_skip_verify true关键点解析harbor.internal.com:8080这个键必须与上面mirrors中配置的、以及Pod中使用的仓库地址保持一致。[plugins...tls]这个子表专门用于配置TLS相关参数。insecure_skip_verify true这就是“魔法开关”。设置为true后containerd将不会验证该仓库服务器的证书是否由可信CA签发、是否过期或与主机名是否匹配。安全提醒insecure_skip_verify true会显著降低连接的安全性仅在完全信任的网络环境如公司内网中对非生产环境或确实无需TLS的仓库使用。对于生产环境强烈建议为私有仓库配置有效的TLS证书。2.3 完整配置示例与解释将上述两部分组合起来并加入常用的Docker Hub镜像加速配置一个完整的配置片段如下[plugins.io.containerd.grpc.v1.cri.registry] # 全局镜像仓库配置入口 [plugins.io.containerd.grpc.v1.cri.registry.mirrors] # 配置Docker Hub镜像加速例如使用阿里云镜像 [plugins.io.containerd.grpc.v1.cri.registry.mirrors.docker.io] endpoint [https://registry.cn-hangzhou.aliyuncs.com] # 配置HTTP协议的Harbor私有仓库 [plugins.io.containerd.grpc.v1.cri.registry.mirrors.harbor.internal.com:8080] endpoint [http://harbor.internal.com:8080] [plugins.io.containerd.grpc.v1.cri.registry.configs] # 针对特定仓库的配置 [plugins.io.containerd.grpc.v1.cri.registry.configs.harbor.internal.com:8080.tls] # 跳过对该仓库的TLS证书验证 insecure_skip_verify true配置完成后必须重启containerd服务以使配置生效sudo systemctl daemon-reload sudo systemctl restart containerd重启后可以使用sudo systemctl status containerd检查服务状态是否正常。3. 验证与排查你的配置真的生效了吗修改配置并重启服务后不要急于在Kubernetes中创建Pod。先通过命令行工具进行验证可以更快地定位问题。3.1 使用crictl进行验证crictl是兼容CRI的命令行工具Kubernetes通过kubelet调用containerd的方式与它类似。因此用crictl能拉取成功通常意味着Kubernetes Pod也能成功。首先确保crictl的运行时端点配置正确。编辑/etc/crictl.yaml如果不存在则创建runtime-endpoint: unix:///run/containerd/containerd.sock image-endpoint: unix:///run/containerd/containerd.sock timeout: 10 pull-image-on-create: true然后尝试拉取一个具体的镜像sudo crictl pull harbor.internal.com:8080/my-project/my-app:latest如果配置正确你会看到类似Image is up to date for sha256:...的成功信息。如果失败仔细查看错误输出。常见的错误和排查方向错误信息特征可能原因排查步骤failed to resolve reference结尾是no such host域名解析失败检查/etc/hosts或DNS服务器确保harbor.internal.com可解析。dial tcp [IP]:8080: connect: connection refused网络连接失败确认Harbor服务正在运行curl -v http://harbor.internal.com:8080检查防火墙规则。x509: certificate signed by unknown authorityTLS验证失败确认config.toml中对应仓库的insecure_skip_verify true已配置且格式正确。http://server:port/v2/: unexpected status code 401 Unauthorized认证失败镜像仓库需要认证。需要在config.toml的auths或configs中配置用户名密码或通过Kubernetes Secret配置imagePullSecrets。3.2 理解ctr与crictl的差异这是最容易让人困惑的一点。你可能会发现用crictl pull成功了但用ctr image pull却依然报错。这不是你的配置错了而是因为**ctr是containerd的原生客户端它默认不读取用于CRI的配置**。ctr的设计更底层主要用于调试和直接操作containerd的命名空间和内容存储。它不通过CRI插件因此也就不会应用config.toml中[plugins.io.containerd.grpc.v1.cri]下面的配置。要让ctr拉取HTTP镜像需要在命令中显式使用--plain-http参数sudo ctr -n k8s.io image pull --plain-http harbor.internal.com:8080/my-project/my-app:latest注意-n k8s.io参数指定了命名空间因为Kubernetes使用的镜像都存放在k8s.io命名空间下。所以请记住Kubernetes拉镜像是通过CRI接口因此只要crictl能成功你的Kubernetes Pod拉取就没有问题。ctr的行为不同是正常的不必为此焦虑。3.3 在Kubernetes中最终验证完成命令行验证后就可以在Kubernetes中进行最终测试了。创建一个简单的Pod清单文件test-pod.yamlapiVersion: v1 kind: Pod metadata: name: test-http-registry-pod spec: containers: - name: test-app image: harbor.internal.com:8080/my-project/my-app:latest command: [echo, Hello from HTTP Harbor] restartPolicy: Never应用这个Podkubectl apply -f test-pod.yaml然后观察Pod状态kubectl get pod test-http-registry-pod kubectl describe pod test-http-registry-pod # 查看详细事件 kubectl logs test-http-registry-pod # 如果Pod运行起来查看输出如果看到Pod状态从ContainerCreating变为Running并很快完成或者成功输出了“Hello from HTTP Harbor”那么恭喜你配置完全成功了。4. 高级场景与故障排除指南掌握了基础配置后我们来看看一些更复杂的场景和可能遇到的“坑”。4.1 使用IP地址而非域名在某些网络环境中特别是离线部署或简化DNS配置时我们可能更倾向于直接使用IP地址。配置方式与域名类似但需要注意细节。假设Harbor地址是http://192.168.1.100:8080Pod中镜像名为192.168.1.100:8080/project/app:v1那么config.toml中应该这样配置[plugins.io.containerd.grpc.v1.cri.registry.mirrors] [plugins.io.containerd.grpc.v1.cri.registry.mirrors.192.168.1.100:8080] endpoint [http://192.168.1.100:8080] [plugins.io.containerd.grpc.v1.cri.registry.configs] [plugins.io.containerd.grpc.v1.cri.registry.configs.192.168.1.100:8080.tls] insecure_skip_verify true特别注意mirrors和configs中的键必须包含端口号:8080且与镜像地址中的完全一致。containerd在进行匹配时是非常严格的字符串匹配。4.2 配置私有仓库认证如果Harbor设置了项目为私有拉取镜像时需要提供凭证。有两种主要方式方式一在containerd config.toml中配置不推荐用于生产可以在configs中直接添加auth配置但这样密码会以明文形式存储在节点上。[plugins.io.containerd.grpc.v1.cri.registry.configs.harbor.internal.com:8080.auth] username admin password Harbor12345方式二使用Kubernetes Secret推荐这是更安全、更符合Kubernetes实践的方式。创建docker-registry类型的Secretkubectl create secret docker-registry harbor-regcred \ --docker-serverharbor.internal.com:8080 \ --docker-usernameadmin \ --docker-passwordHarbor12345 \ --namespacedefault在Pod或Deployment的spec中引用这个Secretspec: containers: - name: myapp image: harbor.internal.com:8080/my-project/my-app:latest imagePullSecrets: - name: harbor-regcred这种方式将认证信息与运行时配置解耦更便于管理和轮换密钥。4.3 多节点集群的配置管理在Kubernetes集群中每个工作节点worker node都需要单独配置containerd。手动登录每个节点修改config.toml效率低下且容易出错。推荐以下方法使用配置管理工具如Ansible、SaltStack、Puppet等将config.toml作为模板文件推送到所有节点然后重启containerd服务。使用自定义系统镜像在构建节点系统镜像如AMI、GCE Image时就将正确的config.toml打包进去。注意修改配置后每个节点都必须重启containerd服务。单纯的systemctl reload containerd可能不足以加载所有配置变更最稳妥的方式是执行systemctl restart containerd。4.4 常见故障排查清单当问题出现时可以按照以下清单逐步排查检查配置文件语法使用containerd config dump命令可以查看当前加载的完整配置确认你的修改是否已被正确加载。也可以使用tomlv或在线TOML校验工具检查config.toml语法。确认服务已重启修改config.toml后是否执行了sudo systemctl restart containerd用sudo systemctl status containerd查看服务状态和有无错误日志。核对仓库地址匹配Pod中镜像名的仓库部分如harbor.internal.com:8080是否与config.toml中mirrors和configs下的键完全一致包括大小写、端口号。网络连通性从节点上能否ping通或curl到Harbor仓库地址curl -v http://harbor.internal.com:8080能否返回响应使用crictl直接测试如前所述用sudo crictl pull image直接测试其成功与否是Kubernetes能否拉取镜像的最直接指示。查看containerd日志如果问题依旧查看containerd的详细日志可能会提供线索。日志位置通常在/var/log/containerd/containerd.log或者通过journalctl -u containerd查看。检查Kubernetes事件使用kubectl describe pod pod-name查看Events部分错误信息通常会在这里更清晰地显示。5. 从HTTP迁移到HTTPS的最佳实践虽然本文重点解决HTTP配置问题但从长远和安全角度出发将私有仓库迁移到HTTPS是必然选择。这里给出一个平滑迁移的路线图建议。第一阶段内部测试环境保持HTTPSkip Verify对于开发、测试集群可以继续使用HTTP并跳过验证以简化部署。但应确保集群网络处于防火墙保护的内网中。第二阶段为私有仓库配置自签名证书在生产环境或准生产环境应该启用HTTPS。如果暂时无法获取公共可信CA证书可以使用自签名证书。为Harbor生成自签名证书。将自签名证书的CA公钥或仓库的.crt文件分发到每个Kubernetes节点。在containerd配置中不再设置insecure_skip_verify true而是通过ca_file参数指定CA证书路径。[plugins.io.containerd.grpc.v1.cri.registry.configs.harbor.internal.com.tls] ca_file /etc/containerd/certs.d/harbor.internal.com/ca.crt将mirrors中的endpoint改为https://地址。第三阶段部署可信CA签发的证书最终目标是使用由企业内PKI或Let‘s Encrypt等公共CA签发的证书。这样无需在节点上分发CA文件安全性最高也最便于管理。配置上只需将endpoint改为https://并移除所有tls特殊配置即可。迁移过程中可以并行运行HTTP和HTTPS端点一段时间逐步更新Pod的镜像地址和节点配置实现无缝切换。整个配置过程最需要耐心的地方在于细节的匹配和验证。每次修改配置后养成用crictl pull做快速验证的习惯能节省大量在Kubernetes层调试的时间。最后记得将稳定有效的config.toml片段纳入你的基础设施代码库中作为集群节点标准化配置的一部分。