本文章基於k8s release-1.17分支代碼,代碼位於 plugin/pkg/admission/serviceaccount
目錄,代碼:admission.go 。nginx
api-server做爲經常使用的服務端應用,包含認證模塊Authentication、受權模塊Authorization和准入模塊Admission Plugin(能夠理解爲請求中間件模塊middleware pipeline),以及存儲依賴Etcd。 其中,針對准入插件,在api-server進程啓動時,啓動參數 --enable-admission-plugins
須要包含 ServiceAccount
准入控制器來開啓該中間件,能夠見官方文檔:enable-admission-plugins 。 ServiceAccount Admission Plugin主要做用包含:git
default
ServiceAccount;/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
而且,volume指定的secret name是default service account的secrets的name值:api
那麼,有個問題,ServiceAccount Admission Controller或者說ServiceAccount中間件,是如何作到的呢?markdown
就和咱們常常見到的一些服務端框架作的middleware中間件模塊同樣,api-server框架也是用插件化形式來定義一個個准入控制器Admission Controller,而且會調用該插件的Admit()
方法, 來判斷當前請求是否經過該准入控制器。app
實例化操做很簡單,須要注意的是: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操做是該中間件的核心邏輯,主要工做上文已經詳細描述,這裏從代碼角度學習下,代碼見:L160-L248 。oop
首先是檢查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核心就是會建立個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時參考學習。