Kubernetes学习笔记之ServiceAccount AdmissionController源码解析

Overview

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

api-server做为经常使用的服务端应用,包含认证模块Authentication、受权模块Authorization和准入模块Admission Plugin(能够理解为请求中间件模块middleware pipeline),以及存储依赖Etcd。 其中,针对准入插件,在api-server进程启动时,启动参数 --enable-admission-plugins 须要包含 ServiceAccount 准入控制器来开启该中间件,能够见官方文档:enable-admission-plugins 。 ServiceAccount Admission Plugin主要做用包含:git

  • 若是提交的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对象:github

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没有值:shell

serviceaccount_admission_plugin

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

serviceaccount_default

那么,有个问题,ServiceAccount Admission Controller或者说ServiceAccount中间件,是如何作到的呢?markdown

源码解析

就和咱们常常见到的一些服务端框架作的middleware中间件模块同样,api-server框架也是用插件化形式来定义一个个准入控制器Admission Controller,而且会调用该插件的Admit()方法, 来判断当前请求是否经过该准入控制器。app

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

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