
Overview
本文章基于 k8s release-1.17 分支代码,代码位于 plugin/pkg/admission/serviceaccount
目录,代码:admission.go[1] 。
api-server 作为常用的服务端应用,包含认证模块 Authentication、授权模块 Authorization 和准入模块 Admission Plugin(可以理解为请求中间件模块 middleware pipeline),以及存储依赖 Etcd。其中,针对准入插件,在 api-server 进程启动时,启动参数 --enable-admission-plugins
需要包含 ServiceAccount
准入控制器来开启该中间件,可以见官方文档:enable-admission-plugins[2] 。ServiceAccount Admission Plugin 主要作用包含:
如果提交的 pod yaml 里没有指定 spec.serviceAccountName 字段值,该插件会添加默认的 default
ServiceAccount;判断 spec.serviceAccountName 指定的 service account 是否存在,不存在就拒绝请求; 为该 pod 创建个 volume,且该 volume source 是 SecretVolumeSource,该 secret 来自于 service account 对象引用的 secret; 如果提交的 pod yaml 里没有指定 spec.ImagePullSecrets 字段值,那就将 service account 对象引用的 ImagePullSecrets 字段值来补位,并且该 volume 会被 mount 到 pod 的 /var/run/secrets/kubernetes.io/serviceaccount
目录中;
比如,往 api-server 进程提交个 pod 对象:
echo > pod.yaml <<EOF
apiVersion: v1
kind: Pod
metadata:
name: serviceaccount-admission-plugin
labels:
app: serviceaccount-admission-plugin
spec:
containers:
- name: serviceaccount-admission-plugin
image: nginx:1.17.8
imagePullPolicy: IfNotPresent
ports:
- containerPort: 80
name: "http-server"
EOF
kubectl apply -f ./pod.yaml
kubectl get pod/serviceaccount-admission-plugin -o yaml
kubectl get sa default -o yaml
就会看到该 pod 对象被 ServiceAccount Admission Plugin 处理后,spec.serviceAccountName 指定了 default
ServiceAccount;增加了个 SecretVolumeSource
的 Volume,volume name 为 ServiceAccount 的 secrets 的 name 值,mount 到 pod 的 /var/run/secrets/kubernetes.io/serviceaccount
目录中;以及因为 pod 和 default service account 都没有指定 ImagePullSecrets 值,pod 的 spec.ImagePullSecrets 没有值:

并且,volume 指定的 secret name 是 default service account 的 secrets 的 name 值:

那么,有个问题,ServiceAccount Admission Controller 或者说 ServiceAccount 中间件,是如何做到的呢?
源码解析
就和我们经常见到的一些服务端框架做的 middleware 中间件模块一样,api-server 框架也是用插件化形式来定义一个个准入控制器 Admission Controller,并且会调用该插件的Admit()
方法,
来判断当前请求是否通过该准入控制器。
AdmissionController 准入控制器实例化
实例化操作很简单,需要注意的是:MountServiceAccountToken
为 true,表示默认去执行 mount volume 操作,且 mount 到 pod 的默认目录;并且资源操作是 Create
操作时才去执行当前准入控制器。代码见 L103-L121[3]:
// 注册到plugin chain中去
func Register(plugins *admission.Plugins) {
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
serviceAccountAdmission := NewServiceAccount()
return serviceAccountAdmission, nil
})
}
// controller初始化
func NewServiceAccount() *Plugin {
return &Plugin{
Handler: admission.NewHandler(admission.Create), // Create操作资源时才执行这个插件
LimitSecretReferences: false,
MountServiceAccountToken: true,
RequireAPIToken: true,
generateName: names.SimpleNameGenerator.GenerateName, // 生成volume mount name时需要
}
}
Admit 操作
Admit 操作是该中间件的核心逻辑,主要工作上文已经详细描述,这里从代码角度学习下,代码见:L160-L248[4] 。
ServiceAccount 检查
首先是检查 pod yaml 中有没有指定 ServiceAccount,没有指定就设置默认的default
ServiceAccount 对象,并且同时检查该 ServiceAccount 在当前 namespace 内是否真的存在:
func (s *Plugin) Admit(/*...*/) (err error) {
// ...
// 如果没有指定就设置默认值
if len(pod.Spec.ServiceAccountName) == 0 {
pod.Spec.ServiceAccountName = DefaultServiceAccountName
}
// 检查该ServiceAccount是否真的存在
serviceAccount, err := s.getServiceAccount(a.GetNamespace(), pod.Spec.ServiceAccountName)
// 判断是否可以mount volume,默认可以
if s.MountServiceAccountToken && shouldAutomount(serviceAccount, pod) {
// 会新建一个secret source类型的volume,并且mount到每一个容器内的"/var/run/secrets/kubernetes.io/serviceaccount"目录下
if err := s.mountServiceAccountToken(serviceAccount, pod); err != nil {
// ...
}
}
// 如果没有指定ImagePullSecrets,就看ServiceAccount内有没有指定,有指定则使用该值否则默认值
if len(pod.Spec.ImagePullSecrets) == 0 {
pod.Spec.ImagePullSecrets = make([]api.LocalObjectReference, len(serviceAccount.ImagePullSecrets))
for i := 0; i < len(serviceAccount.ImagePullSecrets); i++ {
pod.Spec.ImagePullSecrets[i].Name = serviceAccount.ImagePullSecrets[i].Name
}
}
// 还是检查该ServiceAccount是否真的存在
return s.Validate(ctx, a, o)
}
ServiceAccount 检查逻辑很简单,主要目的是为 pod 填补 ServiceAccount 值,因为服务账号就是给 pod 调用 api-server 进程用的,关于服务账号 ServiceAccount 作用可见官网:用户账号与服务账号[5]
Mount Volume
Mount Volume 核心就是会创建个 volume,并 mount 到 pod 每个容器内指定目录,该目录下包含 ca.crt、namespace和token文件
,供 pod 调用 api-server 时使用。从源码角度看看如何创建 volume 以及如何 mount 的 L426-L567[6] :
const (
DefaultAPITokenMountPath = "/var/run/secrets/kubernetes.io/serviceaccount"
)
// 核心逻辑就是创建个secret source volume并mount到pod对象内的指定目录
func (s *Plugin) mountServiceAccountToken(serviceAccount *corev1.ServiceAccount, pod *api.Pod) error {
// 首先找到serviceAccount.secrets下的secret的name值,
// 这里是先list type="kubernetes.io/service-account-token" 的secrets,然后再和serviceAccount.secrets进行匹配,选择第一个匹配成功的。
// 关于type="kubernetes.io/service-account-token" 服务账号类型的secrets,可以见官网:https://kubernetes.io/zh/docs/concepts/configuration/secret/#service-account-token-secrets
serviceAccountToken, err := s.getReferencedServiceAccountToken(serviceAccount)
// 如果pod内的volumes已经引用了该secret作为volume,直接跳过
// ...
// Determine a volume name for the ServiceAccountTokenSecret in case we need it
if len(tokenVolumeName) == 0 {
// 以serviceAccountToken为前缀,加上个随机字符串,生成个volume name
}
// 这里挂载到pod每一个容器内的mount path是"/var/run/secrets/kubernetes.io/serviceaccount"
volumeMount := api.VolumeMount{
Name: tokenVolumeName,
ReadOnly: true,
MountPath: DefaultAPITokenMountPath,
}
// InitContainers和Containers都要mount新建的volume
needsTokenVolume := false
for i, container := range pod.Spec.InitContainers {
// ...
}
for i, container := range pod.Spec.Containers {
// ...
}
// 新创建的volume加到pod volumes中
if !hasTokenVolume && needsTokenVolume {
pod.Spec.Volumes = append(pod.Spec.Volumes, s.createVolume(tokenVolumeName, serviceAccountToken))
}
return nil
}
// 创建volume对象
func (s *Plugin) createVolume(tokenVolumeName, secretName string) api.Volume {
// ...
return api.Volume{
Name: tokenVolumeName,
VolumeSource: api.VolumeSource{
Secret: &api.SecretVolumeSource{
SecretName: secretName,
},
},
}
}
Mount Volume 逻辑也很简单,主要就是为 pod 创建个 volume,并且 mount 到每一个容器的指定路径。该 volume 内包含的数据来自于 ServiceAccount 引用的
secrets 的数据,即 ca.crt、namespace和token
数据文件,这些数据是调用 api-server 时需要的认证数据,且 token 数据已经经过私钥文件签名过了。
那么有个问题,创建 ServiceAccount 时对应的这些 secret 对象是怎么来的呢?secret 里的 token 文件既然已经被私钥签名过,那 api-server 必然需要对应的公钥文件来验证签名才对?至于 secret 对象是怎么来的问题,这是 kube-controller-manager 里的 ServiceAccount 模块的 TokenController 创建的,创建时会用私钥进行签名,所以
kube-controller-manager 启动时必须带上私钥参数 --service-account-private-key-file
,具体可见官网 service-account-private-key-file[7] ;至于 api-server 必须使用对应的公钥来验证签名,同理,kube-apiserver 启动时,也必须带上公钥参数 --service-account-key-file
,具体可见官网 service-account-key-file[8]
总结
本文分析了 ServiceAccount Admission Controller 中间件的主要业务逻辑,如何为 pod 对象补充 serviceAccount、imagePullSecrets 字段数据, 以及创建并挂载 service account volume,供 pod 调用 api-server 使用。总体逻辑比较简单,源码值得学习,供自己二次开发 k8s 时参考学习。
参考文档
serviceaccounts-controller 源码官网解析[9]
为 Pod 配置服务账户[10]
服务账号令牌 Secret[11]
admission.go[12]
Kubernetes Proposal - Admission Control[13]
参考资料
admission.go: https://github.com/kubernetes/kubernetes/blob/release-1.17/plugin/pkg/admission/serviceaccount/admission.go
[2]enable-admission-plugins: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/
[3]L103-L121: https://github.com/kubernetes/kubernetes/blob/release-1.17/plugin/pkg/admission/serviceaccount/admission.go#L103-L121
[4]L160-L248: https://github.com/kubernetes/kubernetes/blob/release-1.17/plugin/pkg/admission/serviceaccount/admission.go#L160-L248
[5]用户账号与服务账号: https://kubernetes.io/zh/docs/reference/access-authn-authz/service-accounts-admin/#user-accounts-versus-service-accounts
[6]L426-L567: https://github.com/kubernetes/kubernetes/blob/release-1.17/plugin/pkg/admission/serviceaccount/admission.go#L426-L567
[7]service-account-private-key-file: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/
[8]service-account-key-file: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/
[9]serviceaccounts-controller 源码官网解析: https://kubernetes.io/zh/docs/reference/access-authn-authz/service-accounts-admin/
[10]为 Pod 配置服务账户: https://kubernetes.io/zh/docs/tasks/configure-pod-container/configure-service-account/
[11]服务账号令牌 Secret: https://kubernetes.io/zh/docs/concepts/configuration/secret/#service-account-token-secrets
[12]admission.go: https://github.com/kubernetes/kubernetes/blob/v1.17.0/plugin/pkg/admission/serviceaccount/admission.go
[13]Kubernetes Proposal - Admission Control: https://github.com/kubernetes/community/blob/master/contributors/design-proposals/api-machinery/admission_control.md





