AI时代的Kubernetes教程36招-第24招-私有Registry、imagePullSecrets和镜像策略怎么进入生产集群?

让企业镜像进入集群:imagePullSecrets、Credential Provider 与 ImagePullBackOff

拉不下来镜像,多半和网络无关,是凭证断在了节点侧。

[读者] 平台工程师,正把企业私有仓库接进 K8s 集群
[痛点] ImagePullBackOff 反复出现,凭证、CA、Tag 各说各话
[现在读] Pod 级 Secret 已不足以覆盖多仓库与节点侧凭证
[读完] 能画清拉取链路,并用 Digest 把生产发布钉死
[旧方案] 每个 Pod 挂 imagePullSecrets,共用一套 registry 账号
    |
    v
[新需求] 多 Harbor/Nexus/OCI 仓库、私有 CA、只读机器人账号、跨命名空间复用
    |
    v
[冲突] 凭证绑在 Pod/SA 上,节点侧 TLS 与凭证却无人统一管理
    |
    v
[后果] ImagePullBackOff 长期挂起,发布靠人肉逐台节点试错

我是老李,做平台工程十来年,最近半年主要在干一件事:把公司几十个业务集群,从一个自建的、没有 TLS 的老 registry,迁到 Harbor + Nexus + 一个外部 OCI Registry 的组合上。

集群本身不难,难的是「让镜像进来」这一步。以前一个 docker login 就解决的事,现在变成了三套仓库、四类凭证、五处 CA,还夹着一个 kubelet。

我们最初的方案很简单:业务 YAML 里加 imagePullSecrets,指向一个 namespace 内的 docker-registry secret。这套方案撑了三个月。变化来自三件事:一是安全要求 robot 账号按仓库拆分权限;二是私有 CA 强制上 TLS;三是集群数量从 3 个涨到 40 多个,Secret 的复制成了体力活。

问题是——当 Pod 里已经写满了 imagePullSecrets,为什么 ImagePullBackOff 反而更多了?

选择是——继续在 Pod 这一层加 Secret,还是把凭证下沉到节点?

如果选错了,会怎样?Pod 会一直卡在 ImagePullBackOff,而你的排查方向从一开始就是错的。

01、故事

先说现场。

我们是典型的混合部署:3 套自建机房 K8s(v1.24 左右),加上两个云上托管集群。镜像仓库侧有三类:内网 Harbor,走私有 CA 签发的证书;老 Nexus,作为 Docker proxy 缓存外部镜像;一个外部 OCI Registry,给海外节点用。

任务是——让所有节点都能拉到这三个仓库的镜像,且业务方不需要在每个 YAML 里维护凭证。

当时的方案(记为 V1):每个 namespace 建一个 docker-registry 类型的 Secret;每个 Deployment 的 Pod 模板里写 imagePullSecrets;仓库证书由运维手动拷到部分节点的容器运行时目录。

跑起来的第一周还行。第二周开始,告警群里每天都有几条 ImagePullBackOff。

有意思的是,这些报错并不集中:同一个 Deployment,在 A 节点能起来,在 B 节点起不来;重启一次 Pod,可能又好了。

我们花了很久才意识到,问题不在「凭证对不对」,而在「凭证归谁」。Pod 里的 imagePullSecrets 是给 kubelet 用的,它只在 kubelet 决定「要拉这个镜像」的时候才生效;而 kubelet 决定之前,它得先相信那个 registry。

一个是凭证的归属,一个是信任的归属。这两件事,我们一开始全混在一层做了。

凭证有归属
信任看节点
层级错则乱
归位自然安

02、问题

旧方案(V1)失效的地方,具体有三个。

第一,凭证的作用域错位。imagePullSecrets 是 namespace 内的资源,Pod 只能引用同 namespace 的 Secret。40 个集群乘以若干 namespace,Secret 的分发和轮转成了纯体力活;一旦 robot 账号密码轮转,漏掉一个 namespace 就是一个 ImagePullBackOff。

第二,CA 的落点错了。我们把 CA 当成「容器里的东西」,试图用 ConfigMap 挂进 Pod。但拉取镜像的 TLS 握手发生在 kubelet → 容器运行时 → registry 之间,那时候容器还没被创建。挂进 Pod 的 CA,对拉镜像这件事毫无作用。

第三,Tag 的语义被默认了。业务方习惯用 latest 或者复用 v1.2 这种可变 Tag,配合默认的 imagePullPolicy,结果就是「镜像更新了但集群里跑的还是旧的」——这不是拉取失败,而是拉取成功得太自信。

业务影响很直接:发布窗口从 10 分钟拖到 40 分钟,值班同学的第一反应变成了「删 Pod 重试」。

技术表现上,可以归成四类,后面排查也是按这四类走的:

  • ImagePullBackOff / ErrImagePull:凭证不正确、Secret 不存在、或 RBAC 读不到 Secret;
  • x509: certificate signed by unknown authority:节点不信任私有 CA;
  • manifest unknown:Tag 不存在,或 Digest 写错;
  • 镜像拉下来了但不是新的:Tag 被覆盖 + IfNotPresent。

本篇的完成标准,我给自己定了四条,做到才算收工:

  1. 能在不看业务 YAML 的情况下,说清一次拉取要经过哪几个组件;
  2. 能用 Digest 固定住生产镜像,且验证 Pod 真正拉到了那个 Digest;
  3. 能让节点侧统一信任私有 CA,而不是逐个 Pod 修;
  4. 能区分「凭证问题」和「信任问题」,从报错信息直接定位。

需要说明的是,本篇不涉及如何把 SBOM、签名和扫描做出来——那是 CI 供应链层的事,镜像进了集群之后,集群侧只负责按 Digest 拿到那份确定的字节。

先分清凭证
再分清信任
最后钉内容
顺序不可乱

03、原理

先把链路讲清楚,只讲本篇用得上的部分。

一次镜像拉取,涉及四方:API Server(存 Pod spec)、kubelet(执行拉取)、容器运行时(真正的 docker/containerd 客户端)、registry。

关键点在于:Pod spec 里的 imagePullSecrets 只是一个「引用」,它最终是被 kubelet 取出来,作为凭证传给容器运行时的。也就是说,Pod 从来没有自己拉过镜像。

imagePullPolicy 有三个值:Always、IfNotPresent、Never。默认规则是:如果 Tag 是 latest,默认 Always;否则默认 IfNotPresent;如果省略 Tag,等价于 latest。这条默认规则是很多「回滚没生效」事故的源头。

这里给出第一个反直觉判断:

反直觉判断:imagePullPolicy: Always 并不等于「每次启动都从网络重新下载镜像」。容器运行时是有本地内容存储的,Always 更多是让 kubelet 去 registry 做一次 manifest/digest 解析(确认 Tag 现在指向哪个 Digest),命中的层依然走本地缓存。所以 Always 的代价通常不是带宽,而是每一次 Pod 启动都要和 registry 做一次需要认证、需要 TLS 的交互——这恰恰是最容易被凭证和 CA 问题打中的地方。

第二个反直觉判断:

反直觉判断:用 Digest 引用镜像,比用 Always 更接近「总是拿到最新的那一次构建」。Always 是「每次去问一遍 Tag 现在指哪儿」,答案是会变的;image@sha256:... 是「就要这一份字节」,答案是永恒的。生产要的是可复现,不是「最新」。

第三点,关于凭证的两个层级:

  • Pod/ServiceAccount 层:imagePullSecrets,Pod 引用,或挂到 ServiceAccount 上自动生效。它的问题是作用域小、复制成本高。
  • 节点层:kubelet 的 credential provider。kubelet 通过 --image-credential-provider-config 指定配置文件、--image-credential-provider-bin-dir 指定插件二进制目录,插件按 matchImages 匹配镜像地址,返回该 registry 的用户名/密码。这是集群级能力,Pod 里不需要写任何东西。

节点层还有一个「隐式」来源:如果节点上存在容器运行时的 config.json(历史遗留的 docker login 结果),它也可能被用到。但这种隐式凭证不可审计、不可复现,生产上应当明确排除。

理解了这三层,后面的架构和故障就都能串起来了:凭证解决「你是谁」,CA 解决「我信不信你」,Digest 解决「我要的是不是这一份」。三者互不替代。

凭证答身份
信任靠节点
内容看摘要
三问不同层

04、架构

把上面四类角色串起来,一次拉取的链路是这样:

[输入] Pod spec 中的 image 引用 + imagePullSecrets 引用
    |
    v
[模块] kubelet:解析策略、收集凭证、发起 CRI 拉取
    |
    v
[数据/状态] 凭证来源:Pod imagePullSecrets / ServiceAccount / credential provider 插件
    |
    v
[处理] 容器运行时:TLS 校验(私有 CA)→ 认证 → 解析 Tag/Digest → 分层下载
    |
    v
[输出] 本地镜像存储 + 容器启动;失败则回写 ImagePullBackOff 事件

边界说明:这套链路里,Pod 只能影响「凭证来源」这一格,它碰不到 TLS 信任,也碰不到运行时缓存。这是最重要的边界。

各凭证来源的适用条件:

  • imagePullSecrets:适合 namespace 少、仓库单一、凭证轮转不频繁的场景。它是 Pod 级隔离,天然适合多租户里「每个租户一套只读账号」。
  • ServiceAccount 上的 imagePullSecrets:适合「一个 namespace 里所有 Pod 用同一套凭证」,省去每个 Pod 重复声明。
  • credential provider:适合集群多、仓库多、需要按镜像地址自动匹配凭证的场景。它是节点级配置,一次配置全集群生效。

收益与代价:

方案 收益 代价
imagePullSecrets 隔离好、粒度细、无需改节点 复制成本随 namespace 线性增长
SA 级 imagePullSecrets 减少重复声明 整个 namespace 共享一套凭证
credential provider 集群级统一、Pod 无感、可按仓库匹配 需要改 kubelet 配置并重启,插件自身要运维

还有一个容易被忽略的点:CA 不属于这三者中的任何一个。它属于节点上的容器运行时配置。用 Kubernetes 的话说,这是 CRI 的事,不是 Pod 的事。

声明管引用
节点管信任
运行时管缓存
各守一段路

05、实战一次

先跑通最小可用版本(V1),再谈优化。

环境与版本:

Kubernetes: v1.24(自建,containerd 1.6)
Harbor: 2.x,自签 CA
目标:在 demo namespace 拉取 harbor.example.com/library/nginx:v1

依赖:可用的 Harbor 项目、一个只读 robot 账号、CA 证书文件 harbor-ca.crt。

第一步,创建 docker-registry Secret:

kubectl create namespace demo

kubectl -n demo create secret docker-registry harbor-cred \
  --docker-server=harbor.example.com \
  --docker-username='robot$ci-readonly' \
  --docker-password='REDACTED_TOKEN'

注意 robot\(ci-readonly 用单引号包住,\)ci 在双引号里会被 shell 当变量展开,这是后面排查里那条错误链的伏笔。

第二步,给节点装上私有 CA(containerd 侧):

# 在每一台节点上执行
mkdir -p /etc/containerd/certs.d/harbor.example.com
cp harbor-ca.crt /etc/containerd/certs.d/harbor.example.com/ca.crt

cat > /etc/containerd/certs.d/harbor.example.com/hosts.toml <<'EOF'
server = 'https://harbor.example.com'

[host.'https://harbor.example.com']
  ca = '/etc/containerd/certs.d/harbor.example.com/ca.crt'
EOF

systemctl restart containerd

这一步必须在节点上做,而且 kubelet 不需要重启,它不负责 TLS。

第三步,Pod 引用 Secret 和镜像:

apiVersion: v1
kind: Pod
metadata:
  name: pull-demo
  namespace: demo
spec:
  imagePullSecrets:
    - name: harbor-cred
  containers:
    - name: web
      image: harbor.example.com/library/nginx:v1
      imagePullPolicy: IfNotPresent

第四步,启动与验证:

kubectl apply -f pull-demo.yaml
kubectl -n demo get pod pull-demo -w

预期(未在当前环境实测,以下为预期结果):

NAME        READY   STATUS    RESTARTS   AGE
pull-demo   1/1     Running   0          18s

进一步确认「用的是不是我们指定的那一份镜像」:

kubectl -n demo get pod pull-demo \
  -o jsonpath='{.status.containerStatuses[0].imageID}'

示例输出:

harbor.example.com/library/nginx@sha256:9f2b1c0d...

注意输出的形态:运行时回报的是 Digest 形式,哪怕你在 YAML 里写的是 Tag。这正好说明集群内部本来就是按内容寻址的,Digest 是它的母语。

V1 跑通了,但问题也留在这儿:Secret 还是每 namespace 一份,CA 还是每节点手动拷。下一步就是把这些下沉。

先跑最小例
再看凭证源
记录真摘要
才谈可复现

06、排查

下面三条诊断链都是我们在 V1 上真实撞到的形态。

链一:ImagePullBackOff,凭证里的用户名是错的。

现象:kubectl describe pod 事件里写着

Failed to pull image 'harbor.example.com/library/nginx:v1': ...
unexpected status code 401 Unauthorized

怀疑:账号密码不对,或者 Secret 引用的名字不对。

检查:

kubectl -n demo get secret harbor-cred \
  -o jsonpath='{.data..dockerconfigjson}' | base64 -d

证据:解出来的 JSON 里 username 只剩 robot 一段,后面的字符没了。原因是创建 Secret 时用了双引号,$ci-readonly 被 shell 展开成了空串。

根因:凭证不是「配置错误」,是「生成时就被 shell 吃掉了」。

修复:用单引号重新创建 Secret,或直接把 $ 转义。

错误尝试:我们一开始怀疑是 Harbor 的 robot 账号权限没配到项目上,于是去 Harbor 后台反复调权限、重建账号,折腾了两个小时。错误的原因是:401 只说明「认证没通过」,不区分「密码错」和「用户名根本是残缺的」;而 Secret 是 base64 编码不是加密,解一眼就能看到真相,我们却先去了上游。

链二:x509: certificate signed by unknown authority。

现象:事件里直接报 TLS 错误。

怀疑:Harbor 用的私有 CA,节点不认。

检查:

journalctl -u containerd -n 200 | grep -i 'x509\|tls'
crictl pull harbor.example.com/library/nginx:v1

证据:crictl pull 同样失败,且报的是 CA 不认识——这说明问题在节点运行时这一层,和 Kubernetes 对象无关。

根因:CA 只拷到了 3 台节点,新扩容的 7 台没有。

修复:把 CA 和 hosts.toml 变成节点初始化的一部分(镜像里预置或由 DaemonSet 巡检),而不是手工动作。

链三:Tag 被覆盖,但 Pod 没换镜像。

现象:CI 重新推送了 v1 这个 Tag,新 Pod 起来后跑的还是旧代码。

怀疑:镜像没推上去。

检查:

kubectl -n demo get pod pull-demo \
  -o jsonpath='{.status.containerStatuses[0].imageID}'

证据:imageID 里的 Digest 和 CI 刚推的 Digest 不一致。Pod spec 里写的是 :v1,策略是 IfNotPresent,因为本地已经有 :v1 对应的镜像,kubelet 就没有再去 registry。

根因:可变 Tag 加 IfNotPresent,是一个「看起来最省事」的组合。

修复:生产镜像引用改为 Digest。这一步放到下一章。

三条链指向同一个结论:凭证、信任、内容,分别在不同的层,也要在不同的层排查。

先看事件描述
再解出凭证
后验真摘要
别急着猜网

07、优化

基于第 06 章的证据,V2 做三件事。

修改一,把凭证下沉到节点,用 credential provider。

根因:Secret 的复制成本随 namespace 线性增长,且轮转不可控。

配置(写在节点上,kubelet 启动参数指向它):

apiVersion: kubelet.config.k8s.io/v1
kind: CredentialProviderConfig
providers:
  - name: harbor-credential-provider
    matchImages:
      - 'harbor.example.com'
      - '*.harbor.example.com'
    defaultCacheDuration: '12h'
    apiVersion: credentialprovider.kubelet.k8s.io/v1
    args:
      - get-credentials

kubelet 的两个参数:

--image-credential-provider-config=/etc/kubernetes/credential-provider/config.yaml
--image-credential-provider-bin-dir=/etc/kubernetes/credential-provider/bin

原因:credential provider 按 matchImages 匹配镜像地址,匹配到就返回该 registry 的用户名/密码,并可以用 cacheDuration / defaultCacheDuration 控制凭证缓存。kubelet 会周期性调用插件,凭证轮转不再依赖人工改 Secret。

新行为:业务 Pod 的 YAML 里删掉 imagePullSecrets 整段;凭证在节点侧按仓库自动匹配。

验证方式:一,用 crictl pull 在节点上单测,排除 Kubernetes 对象干扰;二,起一个不带 imagePullSecrets 的 Pod,拉私有仓库镜像,观察是否 Running;三,轮转一次 token,观察旧 Pod 在缓存期内仍可用、新 Pod 用新凭证成功。

需要说明的是,credential provider 的插件二进制与配置格式随版本演进,该能力从 alpha 到稳定的具体版本状态,需按当前官方文档核验。升级集群前务必确认当前版本支持的 apiVersion 字段值。

修改二,生产镜像引用改为不可变 Digest。

根因:可变 Tag 加 IfNotPresent 会导致「推了新镜像但集群不更新」,而 Tag 加 Always 又会把每次启动都变成一次带认证的网络交互。

修改:CI 产出镜像后,把不可变 Tag(如 20250115-1a2b3c4)和 Digest 一起写进发布系统;集群侧引用 Digest。

    - name: web
      image: harbor.example.com/library/nginx@sha256:9f2b1c0d...
      imagePullPolicy: IfNotPresent

原因:Digest 的内容是确定的,IfNotPresent 在这种情况下语义完全正确——本地有就是同一份,没变过。既不担心拿旧版本,也不用每次去问 registry。

新行为:回滚等于换 Digest;审计等于看 Digest;「这个 Pod 跑的是哪一次构建」变成可回答的问题。

验证方式:

kubectl -n demo get pod pull-demo \
  -o jsonpath='{.status.containerStatuses[0].imageID}'

输出中的 Digest 必须和发布单上写的 Digest 完全一致。

修改三,CA 的节点化交付。

根因:手工拷 CA 会漏节点。

修改:把 /etc/containerd/certs.d// 的落盘做成节点初始化的固定步骤,并用巡检任务校验每个节点上文件存在且内容一致。

原因:信任是节点的属性,不该按 Pod 修。

新行为:新节点扩容后自动带 CA,不需要人工介入。

三条修改的共同点是:把「每个 Pod 都要做的事」,换成「每个节点只做一次的事」。

凭证往下沉
内容要钉死
信任要前置
才叫生产级

08、演进

同一份需求(拉私有仓库镜像),V1 和 V2 的差异:

[同一输入] 拉取 harbor.example.com/library/nginx 的某个版本
    |
    +--[V1] 每个 Pod 写 imagePullSecrets + 可变 Tag
    |       代价:Secret 随 namespace 复制、轮转不可控、Tag 覆盖不生效
    |
    +--[V2] 节点 credential provider + Digest 引用 + 节点级 CA
    |       代价:需要改 kubelet 配置并重启、插件二进制要单独运维
    |
[Trade-off]
得到:凭证统一、内容可复现、信任集中
失去:节点配置的灵活性;插件本身成为新的故障点
适用边界:集群数量多、仓库多、需要对发布做确定性审计

逐项比较:

正确性。V1 的「正确」依赖运维记得每个 namespace 都改;V2 的正确性由 Digest 保证,是机械性的。

稳定性。V1 的故障面在「Secret 是否齐、CA 是否齐、Tag 是否被覆盖」三处;V2 收敛到「节点配置是否齐」一处,但新增了「插件是否可用」这一个点。故障点数量未必减少,但可观测性变好了——插件出问题,kubelet 日志里是明确的。

复杂度。V1 复杂度在业务 YAML 里(人多手杂);V2 复杂度在节点初始化里(一次配置、长期生效)。复杂度没有消失,只是被搬到了更少人碰的地方。

成本。V1 的主要成本是人力(每次轮转、每次扩容);V2 的主要成本是节点重启窗口和插件维护。

适用范围。单集群、少 namespace、仓库单一,V1 足够了,强上 credential provider 是过度设计。多集群、多仓库、有审计要求,V2 才划算。

遗留问题:Digest 一旦写错,Pod 会永久 ImagePullBackOff,不会自动回退到某个能用的版本——这是特性不是缺陷,但要求发布系统不能写错;credential provider 只解决凭证,不解决 CA,两者要分开治理;镜像来源的可信度(SBOM、签名、扫描)不在这条链路上,集群侧只保证拿到的是这一份字节。

复杂度不消
只是搬了家
搬到少人处
就是收益了

09、洞见

9.1 凭证的归属,决定了故障的排查方向

ImagePullBackOff 是一个笼统的状态,它至少覆盖了四类根因:认证失败、CA 不信任、Tag/Digest 不存在、网络不可达。

反直觉判断:运维最常做的一件事——删掉 Pod 重建——对前三类根因完全无效,对第四类也常常无效。因为拉取这件事由 kubelet 发起,Pod 只是描述;重建一个描述,不会改变执行者的能力。真正有效的动作是先问一句:这条链路里,凭证是从哪一层来的?

9.2 私有 CA 不是配置项,是交付物

很多团队把 CA 当成「环境准备时随手拷一下」的事,于是它有 40 份不同的版本,且没人知道哪台节点缺。

它应该和节点镜像、kubelet 配置并列,属于节点交付物。判断标准很简单:新扩容一台节点,不进行任何人工操作,能不能拉到私有仓库的镜像。

9.3 可变 Tag 的成本,不在拉取时,在回滚时

用可变 Tag 平时省事,出事时最贵。当线上需要回滚,而你只有 :v1 这一个线索时,你要回答的问题是:刚才那一次 v1,到底是哪一个字节?这时你会发现,CI 的构建日志、发布单、镜像扫描报告,全都对不上号。

反直觉判断:不可变 Tag 或 Digest 看起来更麻烦,实际上是把复杂度从事故现场前移到了发布流程。发布流程里多写一个 Digest,事故现场就少烧两个小时。

9.4 供应链安全不该在集群侧补

SBOM、签名、漏洞扫描这些事,如果在集群侧做(比如用准入控制器去查漏洞库),会带来两个问题:一是每加一个准入就多一条发布依赖;二是集群里看到的永远是「镜像已经在节点本地了」。

正确的位置是 CI:镜像构建出来、推送之前,把 SBOM 生成好、签名打上、扫描跑完,把结果和 Digest 一起登记。集群侧只做一件事——按 Digest 把那一份字节拿下来。

层间职责清
排查才有路
复杂度前移
事故自然少

10、系统落地

回到「原来有什么 / 新增什么」。

原来有什么:每个 namespace 一份 docker-registry Secret,每个 Pod 写 imagePullSecrets,CA 手工拷部分节点,业务用可变 Tag。

本篇新增什么:一条五段式的拉取链路模型(Pod → kubelet → 凭证来源 → CRI → 本地存储);节点侧 credential provider 的配置方式与适用边界;用 Digest 固定生产镜像的引用方式;三条可复用的诊断链(401 / x509 / Digest 不一致)。

现在能做什么:新集群接入私有仓库时,不用再逐个 namespace 建 Secret;发布单上写 Digest,回滚和审计有据可查;看到 ImagePullBackOff 时,能按凭证 → 信任 → 内容 → 网络的顺序定位,而不是删 Pod。

还缺什么:节点侧 credential provider 的自动化部署,目前还是节点初始化脚本的一部分;CA 的巡检机制,目前靠人工核对文件一致性;CI 侧的 SBOM、签名、扫描闭环,本篇不覆盖,需要单独一轮。

下一步演进的方向,结合 deploy/registry/ 目录的落地:一,把 /etc/containerd/certs.d// 的落盘写进节点镜像,让 CA 成为不可变交付物;二,在 deploy/registry/ 下增加 credential provider 的配置模板和插件二进制校验,纳入节点初始化;三,在发布流水线里增加一步,把集群侧引用从可变 Tag 改写为 Digest,失败即阻断发布。

从声明起手
往节点落地
把内容钉死
再谈供应链

11、小结

Q1 → 凭证、信任、内容分属三层:Pod 引用凭证、节点负责 CA、Digest 固定内容
Q2 → 选节点 credential provider:凭证按仓库自动匹配,Pod 侧不再维护 Secret
Q3 → 选 Digest 而非 Always:本地有即同一份,回滚审计可对账
状态 → 拉取链路可解释、私有 CA 可交付、生产镜像引用可复现

三句话概括这轮的取舍:imagePullSecrets 没有错,只是不适合「多集群 + 多仓库 + 频繁轮转」;credential provider 也不是银弹,它把故障点从「每个 namespace」搬到了「每个节点」;Digest 是唯一能让「我拉到的就是我要的」变成事实的引用方式。

凭证归节点
信任归节点
内容归摘要
责任分清楚

12、作业

12.1 理解题:为什么把私有 CA 挂进 Pod 的 ConfigMap 里,对拉取镜像没有帮助?

参考答案:拉取镜像的 TLS 握手发生在 kubelet 通过 CRI 调用容器运行时的阶段,此时容器尚未创建,Pod 里的任何挂载都还没生效。CA 必须配置在节点侧的容器运行时,例如 containerd 的 certs.d//ca.crt 与 hosts.toml。Pod 的 ConfigMap 只能影响容器内部进程,不能影响拉取这一步。

12.2 实战题:同一个 Deployment,在 A 节点能起,在 B 节点报 x509: certificate signed by unknown authority,给出排查步骤。

参考答案:一,先确认报错来源是节点而非 Pod,在 B 节点执行 crictl pull ,能复现则问题在节点运行时;二,检查 B 节点是否存在 /etc/containerd/certs.d//ca.crt 和 hosts.toml,内容是否与 A 节点一致;三,查看 journalctl -u containerd 中的 TLS 相关日志,确认是缺 CA 还是 CA 过期、不匹配;四,修复后重启 containerd,再用 crictl pull 验证;五,根因收敛到 CA 交付不齐后,把它加入节点初始化流程,避免下一台新节点重现。

12.3 排障题:Secret 里 base64 解出来的 username 只有 robot,少了后面的部分,可能是怎么造成的?

参考答案:创建 Secret 时用了双引号包裹 --docker-username,其中的 $ 被 shell 当作了变量前缀并被展开为空串。例如 --docker-username='robot$ci' 用双引号写法时,shell 会展开 $ci。修复方式是用单引号或对 $ 转义。由于 Secret 只是 base64 编码而非加密,直接解码 .dockerconfigjson 就能看到真实内容。

12.4 架构判断题:团队要把 40 个集群、3 个私有仓库的镜像拉取统一起来,应该在 Pod 层继续加 imagePullSecrets,还是上节点 credential provider?说明理由与代价。

参考答案:优先选节点 credential provider。理由是凭证属于节点级能力,按 matchImages 匹配镜像地址后可自动返回对应 registry 的凭证,Pod 侧无需维护 Secret,凭证轮转也不必改动业务 YAML。代价是:需要修改 kubelet 参数并重启节点,插件二进制与配置本身需要运维,且它是集群级共享,任何能改节点配置的人都能影响所有镜像拉取,权限边界要收紧。若集群少、仓库单一,继续用 imagePullSecrets 或 ServiceAccount 级引用是更轻的选择。另外,无论选哪一种,私有 CA 都必须在节点侧单独治理,Digest 都应当用于固定生产镜像。

题在链路上
答在分层里
能复现才算
能收敛才稳

13、思考

回到最开始那个问题:为什么 Pod 里已经写满了 imagePullSecrets,ImagePullBackOff 反而更多了?

因为凭证和信任不是同一件事,而我们在同一个地方解决它们。

Pod 能表达的是「用哪个凭证」,表达不了「信不信这个 registry」,也表达不了「要的是哪一份字节」。后两件事,一件归节点上的容器运行时,一件归引用方式本身。把它们塞进 Pod 这一层,表面上统一了,实际上每一次失败都要回到最上层去猜。

这轮迁移留给我最实用的一条判断是:遇到「拉不到镜像」,先问三句话——凭证从哪来、CA 在哪台节点上、引用的是 Tag 还是 Digest。三句话问完,方向基本就定了,剩下的都是执行。

一层管一事
一事问三层
层清则故障明
心定则手不乱

posted @ 2026-10-12 09:56  李福春  阅读(3)  评论(0)    收藏  举报