乐趣区

关于kubernetes:Kubernetes学习笔记之ServiceAccount-AdmissionController源码解析

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 <<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

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

退出移动版