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

相關文章
相關標籤/搜索