容器镜像可以被视为是一个封装了应用程序和其所需软件依赖的可执行软件包。它是一个独立运行的软件包,可以放置在任何支持容器技术的环境中。容器镜像对于其运行时环境有明确的要求和假设。
通常情况下会创建应用程序的容器镜像,并将其存储在一个仓库中(称为Registry)。然后可以在Kubernetes中的Pod中引用这个容器镜像。如果需要获取特定版本的Kubernetes容器镜像,比如最新的次要版本v1.28,可以访问Kubernetes官方网站下载相应的镜像。容器镜像就是一个打包了应用程序和它所需的所有软件依赖的可执行软件包。它能够独立运行,并且具备在特定环境中运行的预设条件。可以在Kubernetes中使用容器镜像来部署应用程序。
一、镜像名称
容器镜像通常会被赋予 pause、example/mycontainer 或者 kube-apiserver 这类的名称, 镜像名称也可以包含所在仓库的主机名, 还可以包含仓库的端口号。如果不指定仓库的主机名,Kubernetes 认为使用的Docker 公共仓库。
在镜像名称之后,可以添加一个标签(Tag)(与使用 docker 或 podman 等命令时的方式相同)。 使用标签能辨识同一镜像序列中的不同版本。镜像标签可以包含小写字母、大写字母、数字、下划线(_)、句点(.)和连字符(-)。 关于在镜像标签中何处可以使用分隔字符(_、- 和 .)还有一些额外的规则。 如果不指定标签,Kubernetes 会认为想使用标签 latest。
二、更新镜像
当最初创建一个 Deployment、 StatefulSet、Pod 或者其他包含 Pod 模板的对象时,如果没有显式设定的话, Pod 中所有容器的默认镜像拉取策略是 IfNotPresent,会使kubelet 在镜像已经存在的情况下直接略过拉取镜像的操作。
1、镜像拉取策略
容器的 imagePullPolicy 和镜像的标签会影响 kubelet 尝试拉取(下载)指定的镜像。以下列表包含了 imagePullPolicy 可以设置的值,以及这些值的效果:
- IfNotPresent:只有当镜像在本地不存在时才会拉取;
- Always:每当 kubelet 启动一个容器时,kubelet 会查询容器的镜像仓库, 将名称解析为一个镜像摘要。 如果 kubelet 有一个容器镜像,并且对应的摘要已在本地缓存,kubelet 就会使用其缓存的镜像; 否则,kubelet 就会使用解析后的摘要拉取镜像,并使用该镜像来启动容器;
- Never:Kubelet 不会尝试获取镜像。如果镜像已经以某种方式存在本地, kubelet 会尝试启动容器;否则,会启动失败。 更多细节见提前拉取镜像。
只要能够可靠地访问镜像仓库,底层镜像提供者的缓存语义甚至可以使 imagePullPolicy: Always 高效。 容器运行时可以注意到节点上已经存在的镜像层,这样就不需要再次下载。
注意:在生产环境中部署容器时,应该避免使用 :latest 标签,因为这使得正在运行的镜像的版本难以追踪,并且难以正确地回滚。相反,应指定一个有意义的标签,如 v1.42.0,和/或者一个摘要。
为了确保 Pod 总是使用相同版本的容器镜像,可以指定镜像的摘要; 将 <image-name>:<tag> 替换为 <image-name>@<digest>,例如 image@sha256:45b23dee08af5e43a7fea6c4cf9c25ccf269ee113168c19722f87876677c5cb2。当使用镜像标签时,如果镜像仓库修改了代码所对应的镜像标签,可能会出现新旧代码混杂在 Pod 中运行的情况。 镜像摘要唯一标识了镜像的特定版本,因此 Kubernetes 每次启动具有指定镜像名称和摘要的容器时,都会运行相同的代码。 通过摘要指定镜像可固定运行的代码,这样镜像仓库的变化就不会导致版本的混杂。有一些第三方的准入控制器 在创建 Pod(和 Pod 模板)时产生变更,这样运行的工作负载就是根据镜像摘要,而不是标签来定义的。 总之,无论镜像仓库上的标签发生什么变化,都想确保所有的工作负载都运行相同的代码,那么指定镜像摘要会很有用。
2、默认镜像拉取策略
当(或控制器)向 API 服务器提交一个新的 Pod 时,集群会在满足特定条件时设置 imagePullPolicy 字段:
- 如果省略了 imagePullPolicy 字段,并且为容器镜像指定了摘要, 那么 imagePullPolicy 会自动设置为 IfNotPresent;
- 如果省略了 imagePullPolicy 字段,并且容器镜像的标签是 :latest, imagePullPolicy 会自动设置为 Always;
- 如果省略了 imagePullPolicy 字段,并且没有指定容器镜像的标签, imagePullPolicy 会自动设置为 Always;
- 如果省略了 imagePullPolicy 字段,并且为容器镜像指定了非 :latest 的标签, imagePullPolicy 就会自动设置为 IfNotPresent。
注意:容器的 imagePullPolicy 的值总是在对象初次创建时设置的, 如果后来镜像的标签或摘要发生变化,则不会更新。例如,如果用一个 非 :latest 的镜像标签创建一个 Deployment, 并在随后更新该 Deployment 的镜像标签为 :latest,则 imagePullPolicy 字段 不会 变成 Always,因此必须手动更改已经创建的资源的拉取策略。
3、必要镜像拉取
如果想总是强制执行拉取,可以使用下述的一中方式:
- 设置容器的 imagePullPolicy 为 Always;
- 省略 imagePullPolicy,并使用 :latest 作为镜像标签; 当提交 Pod 时,Kubernetes 会将策略设置为 Always;
- 省略 imagePullPolicy 和镜像的标签; 当提交 Pod 时,Kubernetes 会将策略设置为 Always;
- 启用准入控制器 AlwaysPullImages。
4、ImagePullBackOff
在使用容器运行时创建Pod时,有时候容器的状态可能会显示为”Waiting”且出现”ImagePullBackOff”错误。当容器出现”ImagePullBackOff”状态时,意味着Kubernetes无法成功获取容器镜像,导致无法启动容器。这可能是由于镜像名称无效或由于没有正确配置私有仓库的imagePullSecret。
在出现”ImagePullBackOff”状态后,Kubernetes会尝试继续拉取镜像,并在每次尝试之间增加一定的延迟时间(即回退延迟)。Kubernetes会逐渐增加延迟时间,直到达到最大限制,也就是300秒(即5分钟)的编译限制。
简而言之,当kubelet在创建Pod时发现无法拉取容器镜像时,容器的状态会显示为”Waiting”和”ImagePullBackOff”。Kubernetes会持续尝试拉取镜像,并根据设定的回退延迟进行重试,直到达到最大限制。这通常是由于镜像名称无效或缺少正确的私有仓库认证信息所引起的问题。
三、串行和并行镜像拉取
默认情况下,kubelet会以串行方式拉取镜像,也就是说,在处理一个镜像拉取请求时,kubelet不会同时发送其他镜像拉取请求,而是等待当前请求完成后再发送下一个请求。每个节点独立地决定镜像的拉取操作。即使使用串行的镜像拉取方式,两个不同的节点仍然可以并行地拉取相同的镜像。
如果想启用并行镜像拉取,可以在kubelet的配置中将serializeImagePulls字段设置为false。当serializeImagePulls被设置为false时,kubelet会立即向镜像服务发送镜像拉取请求,从而可以同时拉取多个镜像。但在启用并行镜像拉取时,请确保容器运行时的镜像服务能够处理并行的镜像拉取请求。
kubelet不会并行地拉取同一个Pod中的多个容器的镜像。例如,如果一个Pod有一个初始容器和一个应用容器,那么这两个容器的镜像拉取将不会并行进行。然而,如果有两个使用不同镜像的Pod,并且启用了并行镜像拉取,kubelet将代表这两个不同的Pod并行地拉取镜像。
最大并行镜像拉取数量:
特性状态: Kubernetes v1.27 [alpha]
当serializeImagePulls被设置为false时,kubelet默认没有限制同时拉取的最大镜像数量。如果想要限制并行镜像拉取的数量,可以在kubelet的配置中设置maxParallelImagePulls字段。当maxParallelImagePulls被设置为n时,只能同时拉取n个镜像,超过n的任何镜像都必须等到至少一个正在进行拉取的镜像拉取完成后,才能拉取。通过限制并行镜像拉取的数量,可以防止镜像拉取消耗过多的网络带宽或磁盘I/O。
注意:maxParallelImagePulls必须设置为大于或等于1的正整数。如果将maxParallelImagePulls设置为大于等于2,则必须将serializeImagePulls设置为false。在无效的maxParallelImagePulls设置下,kubelet将会启动失败。
四、索引多架构镜像
除了提供二进制的镜像之外, 容器仓库也可以提供容器镜像索引。 镜像索引可以指向镜像的多个镜像清单, 提供特定于体系结构版本的容器。 这背后的理念是让可以为镜像命名(例如:pause、example/mycontainer、kube-apiserver) 的同时,允许不同的系统基于它们所使用的机器体系结构取回正确的二进制镜像。
Kubernetes 自身通常在命名容器镜像时添加后缀 -$(ARCH)。 为了向前兼容,请在生成较老的镜像时也提供后缀。 这里的理念是为某镜像(如 pause)生成针对所有平台都适用的清单时, 生成 pause-amd64 这类镜像,以便较老的配置文件或者将镜像后缀硬编码到其中的 YAML 文件也能兼容。
五、使用私有仓库
从私有仓库读取镜像时可能需要密钥。 凭据可以用以下方式提供:
- 配置节点向私有仓库进行身份验证:所有 Pod 均可读取任何已配置的私有仓库;需要集群管理员配置节点;
- kubelet 凭据提供程序,动态获取私有仓库的凭据:kubelet 可以被配置为使用凭据提供程序 exec 插件来访问对应的私有镜像库;
- 预拉镜像:所有 Pod 都可以使用节点上缓存的所有镜像;需要所有节点的 root 访问权限才能进行设置;
- 在 Pod 中设置 ImagePullSecrets:只有提供自己密钥的 Pod 才能访问私有仓库;
- 特定于厂商的扩展或者本地扩展:如果在使用定制的节点配置,(或者云平台提供商)可以实现让节点向容器仓库认证的机制。
下面将详细描述每一项。
1、config.json说明
对于 config.json 的解释在原始 Docker 实现和 Kubernetes 的解释之间有所不同。 在 Docker 中,auths 键只能指定根 URL,而 Kubernetes 允许 glob URLs 以及前缀匹配的路径。 这意味着,像这样的 config.json 是有效的:
{ "auths": { "*my-registry.io/images": { "auth": "…" } } }
使用以下语法匹配根 URL (*my-registry.io):
pattern: { term } term: '*' 匹配任何无分隔符字符序列 '?' 匹配任意单个非分隔符 '[' [ '^' ] 字符范围 字符集(必须非空) c 匹配字符 c (c 不为 '*', '?', '\\', '[') '\\' c 匹配字符 c 字符范围: c 匹配字符 c (c 不为 '\\', '?', '-', ']') '\\' c 匹配字符 c lo '-' hi 匹配字符范围在 lo 到 hi 之间字符
现在镜像拉取操作会将每种有效模式的凭据都传递给 CRI 容器运行时。例如下面的容器镜像名称会匹配成功:
my-registry.io/images my-registry.io/images/my-image my-registry.io/images/another-image sub.my-registry.io/images/my-image a.sub.my-registry.io/images/my-image
kubelet 为每个找到的凭据的镜像按顺序拉取。这意味着在 config.json 中可能有多项:
{ "auths": { "my-registry.io/images": { "auth": "…" }, "my-registry.io/images/subpath": { "auth": "…" } } }
如果一个容器指定了要拉取的镜像 my-registry.io/images/subpath/my-image, 并且其中一个失败,kubelet 将尝试从另一个身份验证源下载镜像。
2、提前拉取镜像
注意:该方法适用于能够控制节点配置的场合。 如果云供应商负责管理节点并自动置换节点,这一方案无法可靠地工作。
默认情况下,kubelet 会尝试从指定的仓库拉取每个镜像。 但是,如果容器属性 imagePullPolicy 设置为 IfNotPresent 或者 Never, 则会优先使用(对应 IfNotPresent)或者一定使用(对应 Never)本地镜像。如果希望使用提前拉取镜像的方法代替仓库认证,就必须保证集群中所有节点提前拉取的镜像是相同的。这一方案可以用来提前载入指定的镜像以提高速度,或者作为向私有仓库执行身份认证的一种替代方案。所有的 Pod 都可以使用节点上提前拉取的镜像。
3、指定ImagePullSecrets
注意:建议运行使用私有仓库中镜像的容器时使用。
Kubernetes 支持在 Pod 中设置容器镜像仓库的密钥。 imagePullSecrets 必须全部与 Pod 位于同一个名字空间中。 引用的 Secret 必须是 kubernetes.io/dockercfg 或 kubernetes.io/dockerconfigjson 类型。
4、创建Secret
首先需要知道用于向仓库进行身份验证的用户名、密码和客户端电子邮件地址,以及它的主机名。 运行以下命令,注意替换适当的大写值:
kubectl create secret docker-registry <name> \ --docker-server=DOCKER_REGISTRY_SERVER \ --docker-username=DOCKER_USER \ --docker-password=DOCKER_PASSWORD \ --docker-email=DOCKER_EMAIL
如果已经有 Docker 凭据文件,则可以将凭据文件导入为 Kubernetes Secret, 而不是执行上面的命令。 基于已有的 Docker 凭据创建 Secret 解释了如何完成这一操作;如果在使用多个私有容器仓库,这种技术将特别有用。 原因是 kubectl create secret docker-registry 创建的是仅适用于某个私有仓库的 Secret。
注意:Pod 只能引用位于自身所在名字空间中的 Secret,因此需要针对每个名字空间重复执行上述过程。
5、Pod引用ImagePullSecrets
在创建 Pod 时,可以在 Pod 定义中增加 imagePullSecrets 部分来引用该 Secret。 imagePullSecrets 数组中的每一项只能引用同一名字空间中的 Secret。例如:
cat <<EOF > pod.yaml apiVersion: v1 kind: Pod metadata: name: foo namespace: awesomeapps spec: containers: - name: foo image: janedoe/awesomeapp:v1 imagePullSecrets: - name: myregistrykey EOF cat <<EOF >> ./kustomization.yaml resources: - pod.yaml EOF
对使用私有仓库的每个 Pod 执行以上操作, 设置该字段的过程也可以通过为服务账号资源设置 imagePullSecrets 来自动完成。 有关详细指令, 可参见将 ImagePullSecrets 添加到服务账号。也可以将此方法与节点级别的 .docker/config.json 配置结合使用。 来自不同来源的凭据会被合并。
四、使用案例
配置私有仓库有多种方案,以下是一些常用场景和建议的解决方案。
1、集群运行非专有镜像(例如,开源镜像)。镜像不需要隐藏。
- 使用来自公共仓库的公共镜像:无需配置;某些云厂商会自动为公开镜像提供高速缓存,以便提升可用性并缩短拉取镜像所需时间。
2、集群运行一些专有镜像,这些镜像需要对公司外部隐藏,对所有集群用户可见。
- 使用托管的私有仓库,在需要访问私有仓库的节点上可能需要手动配置。
- 或者,在防火墙内运行一个组织内部的私有仓库,并开放读取权限,不需要配置 Kubernetes。
- 使用控制镜像访问的托管容器镜像仓库服务,与手动配置节点相比,这种方案能更好地处理集群自动扩缩容。
- 或者,在不方便更改节点配置的集群中,使用 imagePullSecrets
3、集群使用专有镜像,且有些镜像需要更严格的访问控制
- 确保 AlwaysPullImages 准入控制器被启用。否则,所有 Pod 都可以使用所有镜像。
- 确保将敏感数据存储在 Secret 资源中,而不是将其打包在镜像里
4、集群是多租户的并且每个租户需要自己的私有仓库
- 确保 AlwaysPullImages 准入控制器。否则,所有租户的所有的 Pod 都可以使用所有镜像。
- 为私有仓库启用鉴权。
- 为每个租户生成访问仓库的凭据,放置在 Secret 中,并将 Secret 发布到各租户的名字空间下。
- 租户将 Secret 添加到每个名字空间中的 imagePullSecrets。
如果需要访问多个仓库,可以为每个仓库创建一个 Secret。
五、旧版凭据提供程序
在旧版本的 Kubernetes 中,kubelet 与云提供商凭据直接集成。 这使它能够动态获取镜像仓库的凭据。kubelet 凭据提供程序集成存在三个内置实现: ACR(Azure 容器仓库)、ECR(Elastic 容器仓库)和 GCR(Google 容器仓库)。 从 Kubernetes v1.26 到 v1.28 不再包含该旧版机制,因此需要:
- 在每个节点上配置一个 kubelet 镜像凭据提供程序。
- 使用 imagePullSecrets 和至少一个 Secret 指定镜像拉取凭据。