Overview

本文章基于k8s release-1.17分支代码,代码位于 plugin/pkg/admission/serviceaccount 目录,代码:admission.go

api-server作为罕用的服务端利用,蕴含认证模块Authentication、受权模块Authorization和准入模块Admission Plugin(能够了解为申请中间件模块middleware pipeline),以及存储依赖Etcd。
其中,针对准入插件,在api-server过程启动时,启动参数 --enable-admission-plugins 须要蕴含 ServiceAccount 准入控制器来开启该中间件,能够见官网文档:enable-admission-plugins
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 <<EOFapiVersion: v1kind: Podmetadata:  name: serviceaccount-admission-plugin  labels:    app: serviceaccount-admission-pluginspec:  containers:    - name: serviceaccount-admission-plugin      image: nginx:1.17.8      imagePullPolicy: IfNotPresent      ports:        - containerPort: 80          name: "http-server"EOFkubectl apply -f ./pod.yamlkubectl get pod/serviceaccount-admission-plugin -o yamlkubectl 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

// 注册到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

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作用可见官网:
用户账号与服务账号

Mount Volume

Mount Volume外围就是会创立个volume,并mount到pod每个容器内指定目录,该目录下蕴含 ca.crt、namespace和token文件 ,供pod调用api-server时应用。
从源码角度看看如何创立volume以及如何mount的 L426-L567

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
至于api-server必须应用对应的公钥来验证签名,同理,kube-apiserver启动时,也必须带上公钥参数 --service-account-key-file ,具体可见官网 service-account-key-file

总结

本文剖析了ServiceAccount Admission Controller中间件的次要业务逻辑,如何为pod对象补充serviceAccount、imagePullSecrets字段数据,
以及创立并挂载service account volume,供pod调用api-server应用。总体逻辑比较简单,源码值得学习,供本人二次开发k8s时参考学习。

参考文档

serviceaccounts-controller源码官网解析

为 Pod 配置服务账户

服务账号令牌 Secret

admission.go

Kubernetes Proposal - Admission Control