概述
- 相比 K8s 集群的其他功能,私有镜像的自动拉取,看起来可能是比较简单的
- 而镜像拉取失败,大多数情况下都和权限有关
- 所以,在处理相关问题的时候,我们往往会轻松的说:这问题很简单,肯定是权限问题
- 但实际的情况是,我们经常为一个问题,花了多个人的时间却找不到原因
- 这主要还是我们对镜像拉取,特别是私有镜像自动拉取的原理理解不深
- 顺序上来说,私有镜像自动拉取会首先通过阿里云 Acr credential helper 组件
- 再经过 K8s 集群的 API Server 和 kubelet 组件,最后到 docker 容器运行时
- 但是我们从后往前,从最基本的 docker 镜像拉取说起
镜像拉取这件小事
-
为了讨论方便,我们来设想一个场景
-
很多人会使用网盘来存放一些文件,像照片,文档之类
-
当我们存取文件的时候,我们需要给网盘提供账户密码,这样网盘服务就能验证我们的身份
-
这时,我们是文件资源的所有者,而网盘则扮演着资源服务器的角色
-
账户密码作为认证方式,保证只有我们自己可以存取自己的文件
-
这个场景足够简单,但很快我们就遇到新需求:我们需要使用一个在线制作相册的应用
-
按正常的使用流程,我们需要把网盘的照片下载到本地,然后再把照片上传到电子相册,这个过程是比较很繁琐的
-
我们能想到的优化方法是,让相册应用,直接访问网盘来获取我们的照片,而这需要我们把用户名和密码授权给相册应用使用
-
这样的授权方式,优点显而易见,但缺点也是很明显的:我们把网盘的用户名密码给了相册服务,相册服务就拥有了读写网盘的能力,从数据安全角度,这个是很可怕的
-
其实这是很多应用都会遇到的一个一般性场景,私有镜像拉取其实也是这个场景
-
这里的镜像仓库,就跟网盘一样,是资源服务器,而容器集群则是三方服务,它需要访问镜像仓库获取镜像
理解 OAuth 2.0 协议
-
OAuth 协议是为了解决上述问题而设计的一种标准方案,我们的讨论针对 2.0版本
-
相比把账户密码直接给三方应用,此协议采用了一种间接的方式来达到同样的目的
-
如下图,这个协议包括六个步骤,分别是三方应用获取用户授权,三方应用获取临时 Token 以及三方应用存取资源
-
这六步理解起来不容易,主要是因为安全协议的设计,需要考虑协议的易证明性,所以我们换一种方式来解释这个协议。简单来说,这个协议其实就做了两件事情:
- 在用户授权的情况下,三方应用获取 token 所表示的临时访问权限
- 然后三方应用使用这个 token 去获取资源
-
如果用网盘的例子来说明的话,那就是用户授权网盘服务给相册应用创建临时 token,然后相册应用使用这个 token 去网盘服务获取用户的照片
-
实际上 OAuth 2.0 各个变种的核心差别,在于第一件事情,就是用户授权资源服务器的方式
-
最简单的一种,适用于三方应用本身就拥有被访问资源控制权限的情况。这种情况下,三方应用只需要用自己的账户密码登录资源服务器并申请临时token 即可;
-
当用户对三方应用足够信任的情况下,用户直接把账户密码给三方应用,三方应用使用账户密码向资源服务器申请临时 token;
-
用户通过资源服务器提供的接口,登录资源服务器并授权资源服务器给三方应用发放 token;
-
完整实现 OAuth 2.0 协议,也是最安全的。三方应用首先获取以验证码表示的用户授权,然后用此验证码从资源服务器换取临时 token,最后使用 token 存取资源
从上面的描述我们可以看到,资源服务器实际上扮演了鉴权和资源管理两种角色,这两者分开实现的话,协议流程会变成下图这样

Docker 扮演的角色
1 )大图
-
镜像仓库 Registry 的实现,目前使用“把账户密码给三方应用”的方式
-
即假设用户对 Docker 足够信任,用户直接将账户密码交给 Docker,然后 Docker 使用账户密码跟鉴权服务器申请临时 token
2 ) 理解 docker login
- 首先,我们在拉取私有镜像之前,要使用 docker login 命令来登录镜像仓库
- 这里的登录其实并没有和镜像仓库建立什么会话之类的关系。登录主要就做了三件事情:
- 第一件事情是跟用户要账户密码。
- 如下图,当执行登录命令,这个命会提示输入账户密码
- 这件事情对应的是大图的第一步
- 第二件事情,docker 访问镜像仓库的 https 地址,并通过挑战 v2 接口来确认
- 接口是否会返回 Docker-Distribution-Api-Version 头字段
- 这件事情在协议图中没有对应的步骤。它的作用跟 ping 差不多
- 只是确认下 v2镜像仓库是否在线,以及版本是否匹配
- 第三件事情,docker 使用用户提供的账户密码
- 访问 Www-Authenticate 头字段返回的鉴权服务器的地址 Bearer realm
- 如果这个访问成功,则鉴权服务器会返回 jwt 格式的 token 给 docker
- 然后 docker 会把账户密码编码并保存在用户目录的 .docker/docker.json 文件里
- 下图是我登录仓库之后的 docker.json 文件
- 这个文件作为 docker 登录仓库的唯一证据,在后续镜像仓库操作中,会被不断的读取并使用
- 其中关键信息 auth 就是账户密码的 base64 编码
- 第一件事情是跟用户要账户密码。
拉取镜像是怎么回事
- 镜像一般会包括两部分内容,一个是 manifests 文件,这个文件定义了镜像的元数据
- 另一个是镜像层,是实际的镜像分层文件,镜像拉取基本上是围绕这两部分内容展开
- 因为我们这篇文章的重点是权限问题,所以我们这里只以 manifests 文件拉取为例
拉取 manifests 文件,基本上也会做三件事情:
-
首先,docker 直接访问镜像 manifests 的地址,以便获取 Www-Authenticate 头字段
- 这个字段包括鉴权服务器的地址 Bearer realm
- 镜像服务地址 service,以及定义了镜像和操作的 scope
-
接着,docker 访问上边拿到的 Bearer realm 地址来鉴权,以及在鉴权之后获取一个临时的 token
- 这对应协议大图使用账户密码获取临时 token 这一步,使用的账户密码直接读取自 docker.json 文件
- 这对应协议大图使用账户密码获取临时 token 这一步,使用的账户密码直接读取自 docker.json 文件
-
最 后, 使 用 上 边 的 token, 以 Authorization 头 字 段 的 方 式, 来 下 载 manifests 文件
- 这对应的是协议大图下载镜像这一步。当然因为镜像还有分层文件
- 所以实际 docker 还会用这个临时 token 多次下载文件才能完整镜像下载
K8s 实现的私有镜像自动拉取
1 ) 基本功能
-
K8s 集群一般会管理多个节点,每个节点都有自己的 docker 环境,如果让用户分别到集群节点上登录镜像仓库,这显然是很不方便的
-
为了解决这个问题,K8s 实现了自动拉取镜像的功能,这个功能的核心,是把 docker.json 内容编码,并以
Secret 的方式作为 Pod 定义的一部分传给 Kubelet -
具体来说,步骤如下:
- 1 )创 建 secret。 这 个 secret 的 .dockerconfigjson 数 据 项 包 括 了 一 份 base64 编码的 docker.json 文件
- 2 )创建 pod,且 pod 编排中 imagePullSecrets 指向第一步创建的 secret
- 3 )Kubelet 作为集群控制器,监控着集群的变化。当它发现新的 pod 被创建,就会通过 API Server 获取 pod 的定义,这包括 imagePullSecrets 引用的 secret
- 4 )Kubelet 调用 docker 创建容器且把 .dockerconfigjson 传给 docker
- 5 )最后 docker 使用解码出来的账户密码拉取镜像,这和上一节的方法一致
进阶方式
-
上边的功能,一定程度上解决了集群节点登录镜像仓库不方便的问题
-
但是我们,在创建 Pod 的时候,仍然需要给 Pod 指定 imagePullSecrets
-
K8s 通过变更准入控制(Mutating Admission Control)进一步优化了上边的基本功能
-
进一步优化的内容如下:
- 1 )在 第 一 步 创 建 secret 之 后, 添 加 default service account 对 imagePullSecrets 的引用;
- 2 )Pod 默认使用 default service account,而 service account 变更准入控制器会在 default service account 引用 imagePullSecrets 的情况下,添加 imagePullSecrets 配置到 pod 的编排里
阿里云实现的 Acr credential helper
-
阿里云容器服务团队,在 K8s 的基础上实现了控制器 Acr credential helper。
-
这个控制器可以让同时使用阿里云 K8s 集群和容器镜像服务产品的用户,在不用配置自己账户密码的情况下,自动使用私有仓库中的容器镜像
-
具体来说,控制器会监听 acr-configuration 这个 configmap 的变化,其主要关心 acr-registry 和 watch-namespace 这两个配置。前一个配置指定为临时账户授权的镜像仓库地址,后一个配置管理可以自动拉取镜像的命名空间
-
当控制器发现有命名空间需要被配置却没有被配置的时候,它会通过阿里云容器镜像服务的 API,
来获取临时账户和密码 -
有了临时账户密码,Acr credential helper 为命名空间创建对应的 Secret 以及更改 default SA 来引用这个 Secret。这样,控制器和 K8s 集群本身的功能,一起自动化了阿里云 K8s 集群拉取阿里云容器镜像服务上的镜像的全部流程
总结
- 理解私有镜像自动拉取的实现,有一个难点和一个重点
- 难点是 OAuth 2.0 安全协议的原理,上文主要分析了为什么 OAuth 会这么设计
- 重点是集群控制器原理,因为整个自动化的过程,实际上是包括 Admission control 和 Acr credential helper 在内的多个控制器协作的结果