前言
前面写 Deployment 和 Pod 时,我一直在 YAML 里写类似这样的字段:
1 | containers: |
一开始我只把它理解成“指定一个镜像”。但继续往下看,会冒出几个问题:
- 这个镜像是谁拉取的?
- Pod 调度到节点后,容器是谁启动的?
latest标签到底会不会每次都重新拉?imagePullPolicy应该怎么理解?- 为什么生产环境不推荐随便使用
latest?
这篇就把这条链路补上:
1 | Pod 声明 image |
我目前的理解是:Kubernetes 决定“应该运行什么”,容器运行时负责“把容器真正跑起来”。
image 字段到底表达什么
一个容器镜像通常可以写成:
1 | registry.example.com/demo/demo-api:1.0.0 |
拆开看:
1 | registry.example.com 镜像仓库地址 |
如果不写仓库地址,通常会使用默认镜像仓库。比如:
1 | image: nginx:1.27 |
这对学习示例很方便,但在公司内部环境里,往往会使用私有仓库:
1 | image: registry.example.com/platform/demo-api:1.0.0 |
这个字段表达的是:
1 | 请在容器里运行这个镜像对应的文件系统和启动配置 |
它不是把代码直接写进 YAML,而是通过镜像引用一份已经构建好的应用包。
从 Pod 到容器启动的链路
把第二篇的集群组件拿回来:
1 | Deployment 创建 Pod |
kubelet 不直接实现容器。它通过 CRI,也就是 Container Runtime Interface,和容器运行时交互。
1 | kubelet |
现在很多 Kubernetes 集群使用 containerd。以前大家经常把 Docker 和 Kubernetes 绑在一起理解,但更准确地说:
1 | Kubernetes |
这两个层次分开后,很多概念就不会混在一起了。
imagePullPolicy 是什么
imagePullPolicy 控制 kubelet 在启动容器前如何处理镜像拉取。
常见取值有三个:
1 | Always |
示例:
1 | apiVersion: apps/v1 |
可以这样理解:
1 | Pod 要启动 |
默认策略和 latest 的关系
如果不显式写 imagePullPolicy,Kubernetes 会根据镜像标签推断默认值。
常见规则可以先这样记:
- 镜像标签是
latest,默认更倾向Always; - 镜像标签不是
latest,默认通常是IfNotPresent; - 如果没有写 tag,也会被当成
latest。
例如:
1 | image: nginx |
它等价于:
1 | image: nginx:latest |
这会带来一个问题:latest 这个名字看起来像“最新版本”,但它不是稳定版本号。它只是一个会移动的标签。
1 | 今天 latest -> 镜像 A |
如果两个节点在不同时间拉取 latest,就可能出现同一个 Deployment 下的 Pod 实际运行不同镜像内容。
1 | Pod A on Node 1 -> latest 对应镜像 A |
所以生产环境里,我更倾向使用明确版本标签,甚至配合镜像 digest。
tag 和 digest 的区别
镜像 tag 像一个名字:
1 | example/demo-api:1.0.0 |
但 tag 理论上可以被重新指向另一份镜像内容。digest 则指向具体镜像内容:
1 | example/demo-api@sha256:xxxxxxxx |
可以这样类比:
1 | tag |
在很多团队里,常见做法是:
1 | 开发或测试: |
我现在至少会避免在生产 YAML 里写:
1 | image: example/demo-api:latest |
因为它让“现在到底跑的是什么内容”变得不够清楚。
IfNotPresent 不是一定不更新
IfNotPresent 的意思是:节点本地没有这个镜像时才拉取。
如果镜像 tag 没变,但仓库里这个 tag 被重新推送了,节点本地又已经有旧镜像,就可能继续使用旧内容。
1 | Node 本地已有 example/demo-api:1.0.0 |
这也是为什么“覆盖同一个 tag”会让排查变麻烦。
更稳的方式是每次构建产生新 tag:
1 | example/demo-api:1.0.0 |
这样 Deployment 修改 .spec.template 中的 image 字段后,会触发滚动发布。
1 | image: 1.0.0 |
镜像拉取失败会看到什么
如果镜像地址写错、仓库不可达、没有权限,Pod 可能会卡在镜像拉取相关状态。
常见状态包括:
1 | ImagePullBackOff |
排查时可以看 Pod 事件:
1 | kubectl describe pod <pod-name> |
我会关注几个问题:
- 镜像名字和 tag 是否写对;
- 节点能否访问镜像仓库;
- 私有仓库是否配置了 imagePullSecrets;
- 镜像是否真的存在;
- 架构是否匹配,例如 amd64 / arm64。
链路可以这样拆:
1 | Pod Pending |
很多时候,看到 Pod 起不来,不要马上怀疑应用代码,先看容器有没有真正启动成功。
私有镜像仓库和 imagePullSecrets
如果镜像在私有仓库里,节点拉取镜像时需要认证信息。Kubernetes 可以通过 Secret 提供镜像仓库凭据。
命令示例:
1 | kubectl create secret docker-registry regcred \ |
在 Pod 中引用:
1 | apiVersion: apps/v1 |
关系是:
1 | imagePullSecrets |
这个 Secret 和上一篇应用读取的 Secret 不完全一样。它主要给节点拉镜像使用,不是应用进程自己读取数据库密码。
容器启动命令和镜像默认命令
镜像里通常有默认启动命令,比如 Dockerfile 中的 ENTRYPOINT 和 CMD。
Kubernetes 也可以在容器定义里覆盖:
1 | containers: |
我现在把它理解成:
1 | image |
学习示例里经常用 busybox:
1 | command: |
但真实业务镜像最好有清晰默认启动方式。否则每个 Deployment 都要写一长串启动命令,后面维护会很累。
镜像、配置和发布的关系
到这里可以把前几篇串起来:
1 | 镜像 |
一个更完整的 Deployment:
1 | apiVersion: apps/v1 |
这份 YAML 里:
1 | image |
我会遵守的几个镜像习惯
第一,生产环境尽量不用 latest。
1 | # 不推荐 |
第二,不覆盖同一个 tag。每次构建尽量生成新 tag。
1 | git commit -> image tag |
第三,镜像只放程序,不把环境配置和密码打进去。
1 | 程序 -> 镜像 |
第四,镜像拉取失败时先看 Pod 事件。
1 | kubectl describe pod <pod-name> |
第五,私有仓库凭据用 imagePullSecrets,不要把账号密码写进镜像名或普通配置里。
小结
这一篇整理下来,我对镜像和容器运行时的理解可以概括成几句话:
- Pod 的
image字段引用一份容器镜像; - Scheduler 只负责选节点,真正启动容器的是节点上的 kubelet 和容器运行时;
- kubelet 通过 CRI 调用 containerd、CRI-O 等运行时;
imagePullPolicy控制镜像拉取策略;latest是会移动的标签,不适合生产环境依赖;IfNotPresent会优先使用节点本地已有镜像;- 私有镜像仓库通常需要
imagePullSecrets; - 修改 Deployment 中的镜像字段会触发滚动发布;
- 镜像、配置、Secret 应该分工清楚。
下一篇准备进入资源请求与限制。镜像解决的是“运行哪份程序”,资源请求与限制要解决的是“这个程序能用多少 CPU 和内存,调度器又怎么根据资源做决定”。
参考资料
- Images:https://kubernetes.io/docs/concepts/containers/images/
- Container Runtime Interface:https://kubernetes.io/docs/concepts/architecture/cri/
- Pull an Image from a Private Registry:https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/
- Deployments:https://kubernetes.io/docs/concepts/workloads/controllers/deployment/