Kubernetes 學習筆記之 ServiceAccount TokensController 源碼解析

2021-02-16 代碼與遠方
Overview

本文章基於 k8s release-1.17 分支代碼,代碼位於pkg/controller/serviceaccount目錄,代碼:tokens_controller.go

Kubernetes 學習筆記之 ServiceAccount AdmissionController 源碼解析 文章中,知道一個 ServiceAccount 對象都會引用一個type="kubernetes.io/service-account-token" 的 secret 對象,這個 secret 對象內的 ca.crt 、 namespace 和 token 數據會被掛載到 pod 內的每一個容器,供調用 api-server 時認證授權使用。

當創建一個 ServiceAccount 對象時,引用的 type="kubernetes.io/service-account-token" 的 secret 對象會自動創建。比如:

kubectl create sa test-sa1 -o yaml
kubectl get sa test-sa1 -o yaml
kubectl get secret test-sa1-token-jg6lm -o yaml

serviceaccount_token

問題是,這是怎麼做到的呢?

源碼解析TokensController 實例化

實際上這是由 kube-controller-manager 的 TokenController 實現的,kube-controller-manager 進程的啟動參數有 --root-ca-file 和 --service-account-private-key-file ,其中, --root-ca-file 就是上圖中的 ca.crt 數據, --service-account-private-key-file 是用來籤名上圖中的 jwt token 數據,即 token 欄位值。

當 kube-controller-manager 進程在啟動時,會首先實例化 TokensController,並傳遞實例化所需相關參數。其中,從啟動參數中讀取 ca 根證書和私鑰文件內容,並且使用 serviceaccount.JWTTokenGenerator() 函數生成 jwt token,代碼在 L546-L592

func (c serviceAccountTokenControllerStarter) startServiceAccountTokenController(ctx ControllerContext) (http.Handler, bool, error) {
 // ...
 // 讀取--service-account-private-key-file私鑰文件
 privateKey, err := keyutil.PrivateKeyFromFile(ctx.ComponentConfig.SAController.ServiceAccountKeyFile)
 if err != nil {
  return nil, true, fmt.Errorf("error reading key for service account token controller: %v", err)
 }

 // 讀取--root-ca-file的值作為ca,沒有傳則使用kubeconfig文件內的ca值
 var rootCA []byte
 if ctx.ComponentConfig.SAController.RootCAFile != "" {
  if rootCA, err = readCA(ctx.ComponentConfig.SAController.RootCAFile); err != nil {
   return nil, true, fmt.Errorf("error parsing root-ca-file at %s: %v", ctx.ComponentConfig.SAController.RootCAFile, err)
  }
 } else {
  rootCA = c.rootClientBuilder.ConfigOrDie("tokens-controller").CAData
 }

 // 使用tokenGenerator來生成jwt token,並且使用--service-account-private-key-file私鑰來籤名jwt token
 tokenGenerator, err := serviceaccount.JWTTokenGenerator(serviceaccount.LegacyIssuer, privateKey)
 //...

 // 實例化TokensController
 controller, err := serviceaccountcontroller.NewTokensController(
  ctx.InformerFactory.Core().V1().ServiceAccounts(), // ServiceAccount informer
  ctx.InformerFactory.Core().V1().Secrets(), // Secret informer
  c.rootClientBuilder.ClientOrDie("tokens-controller"),
  serviceaccountcontroller.TokensControllerOptions{
   TokenGenerator: tokenGenerator,
   RootCA:         rootCA,
  },
 )
 // ...
 // 消費隊列數據
 go controller.Run(int(ctx.ComponentConfig.SAController.ConcurrentSATokenSyncs), ctx.Stop)

 // 啟動ServiceAccount informer和Secret informer
 ctx.InformerFactory.Start(ctx.Stop)

 return nil, true, nil
}

TokensController 實例化時,會去監聽 ServiceAccount 和 kubernetes.io/service-account-token 類型的 Secret 對象,並設置監聽器:

func NewTokensController(serviceAccounts informers.ServiceAccountInformer, secrets informers.SecretInformer, cl clientset.Interface, options TokensControllerOptions) (*TokensController, error) {
    e := &TokensController{
        // ...
     // 分別為service和secret創建對應的限速隊列queue,用來存儲事件數據
        syncServiceAccountQueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "serviceaccount_tokens_service"),
        syncSecretQueue:         workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "serviceaccount_tokens_secret"),
    }
 // ...
 e.serviceAccounts = serviceAccounts.Lister()
 e.serviceAccountSynced = serviceAccounts.Informer().HasSynced
 // 註冊service account資源對象的事件監聽,把事件放入syncServiceAccountQueue限速隊列中
 serviceAccounts.Informer().AddEventHandlerWithResyncPeriod(
  cache.ResourceEventHandlerFuncs{
   AddFunc:    e.queueServiceAccountSync,
   UpdateFunc: e.queueServiceAccountUpdateSync,
   DeleteFunc: e.queueServiceAccountSync,
  },
  options.ServiceAccountResync,
 )

 // ...
 secrets.Informer().AddEventHandlerWithResyncPeriod(
  cache.FilteringResourceEventHandler{
   FilterFunc: func(obj interface{}) bool {
    switch t := obj.(type) {
    case *v1.Secret:
     return t.Type == v1.SecretTypeServiceAccountToken // 這裡過濾出"kubernetes.io/service-account-token"類型的secret
    default:
     utilruntime.HandleError(fmt.Errorf("object passed to %T that is not expected: %T", e, obj))
     return false
    }
   },
   // 同理,註冊secret資源對象的事件監聽,把事件放入syncSecretQueue限速隊列中
   Handler: cache.ResourceEventHandlerFuncs{
    AddFunc:    e.queueSecretSync,
    UpdateFunc: e.queueSecretUpdateSync,
    DeleteFunc: e.queueSecretSync,
   },
  },
  options.SecretResync,
 )

 return e, nil
}
// 把service對象存進syncServiceAccountQueue
func (e *TokensController) queueServiceAccountSync(obj interface{}) {
    if serviceAccount, ok := obj.(*v1.ServiceAccount); ok {
        e.syncServiceAccountQueue.Add(makeServiceAccountKey(serviceAccount))
    }
}
// 把secret對象存進syncSecretQueue
func (e *TokensController) queueSecretSync(obj interface{}) {
    if secret, ok := obj.(*v1.Secret); ok {
        e.syncSecretQueue.Add(makeSecretQueueKey(secret))
    }
}

把數據存入隊列後,goroutine 調用 controller.Run()來消費隊列數據,執行具體業務邏輯:

func (e *TokensController) Run(workers int, stopCh <-chan struct{}) {
 // ...
 for i := 0; i < workers; i++ {
  go wait.Until(e.syncServiceAccount, 0, stopCh)
  go wait.Until(e.syncSecret, 0, stopCh)
 }
 <-stopCh
 // ...
}

Controller 業務邏輯ServiceAccount 的增刪改查

當用戶增刪改查 ServiceAccount 時,需要判斷兩個業務邏輯:當刪除 ServiceAccount 時,需要刪除其引用的 Secret 對象;當添加/更新 ServiceAccount 時,需要確保引用的 Secret 對象存在,如果不存在,則創建個新 Secret 對象。可見代碼:

func (e *TokensController) syncServiceAccount() {
 // ...
 // 從本地緩存中查詢service account對象
 sa, err := e.getServiceAccount(saInfo.namespace, saInfo.name, saInfo.uid, false)
 switch {
 case err != nil:
  klog.Error(err)
  retry = true
 case sa == nil:
  // 該service account已經被刪除,需要刪除其引用的secret對象
  sa = &v1.ServiceAccount{ObjectMeta: metav1.ObjectMeta{Namespace: saInfo.namespace, Name: saInfo.name, UID: saInfo.uid}}
  retry, err = e.deleteTokens(sa)
 default:
  // 創建/更新service account時,需要確保其引用的secret對象存在,不存在則新建一個secret對象
  retry, err = e.ensureReferencedToken(sa)
  // ...
 }
}

先看如何刪除其引用的 secret 對象的業務邏輯,刪除邏輯也很簡單:

// 刪除service account引用的secret對象
func (e *TokensController) deleteTokens(serviceAccount *v1.ServiceAccount) ( /*retry*/ bool, error) {
 // list出該service account所引用的所有secret
 tokens, err := e.listTokenSecrets(serviceAccount)
 // ...
 for _, token := range tokens {
  // 再一個個刪除secret對象
  r, err := e.deleteToken(token.Namespace, token.Name, token.UID)
  // ...
 }
 // ...
}
func (e *TokensController) deleteToken(ns, name string, uid types.UID) ( /*retry*/ bool, error) {
    // ...
 // 對api-server發起刪除secret對象資源的請求
    err := e.client.CoreV1().Secrets(ns).Delete(name, opts)
    // ...
}

這裡關鍵是如何找到 serviceAccount 所引用的所有 secret 對象,不能通過 serviceAccount.secrets 欄位來查找,因為這個欄位值只是所有 secrets 的部分值。實際上,從緩存中,首先 list 出該 serviceAccount 對象所在的 namespace 下所有 secrets,然後過濾出 type=kubernetes.io/service-account-token 類型的secret,然後查找 secret annotation 中的 kubernetes.io/service-account.name 應該是 serviceAccount.Name 值,和 kubernetes.io/service-account.uid應該是 serviceAccount.UID 值。只有滿足以上條件,才是該 serviceAccount 所引用的 secrets。首先從緩存中找出該 namespace 下所有 secrets,這裡需要注意的是緩存對象 updatedSecrets 使用的是 LRU(Least Recently Used) Cache 最少使用緩存,減少內存使用:

func (e *TokensController) listTokenSecrets(serviceAccount *v1.ServiceAccount) ([]*v1.Secret, error) {
 // 從LRU cache中查找出該namespace下所有secrets
 namespaceSecrets, err := e.updatedSecrets.ByIndex("namespace", serviceAccount.Namespace)
 // ...
 items := []*v1.Secret{}
 for _, obj := range namespaceSecrets {
  secret := obj.(*v1.Secret)
  // 判斷只有符合相應條件才是該serviceAccount所引用的secret
  if serviceaccount.IsServiceAccountToken(secret, serviceAccount) {
   items = append(items, secret)
  }
 }
 return items, nil
}
// 判斷條件
func IsServiceAccountToken(secret *v1.Secret, sa *v1.ServiceAccount) bool {
    if secret.Type != v1.SecretTypeServiceAccountToken {
        return false
    }
    name := secret.Annotations[v1.ServiceAccountNameKey]
    uid := secret.Annotations[v1.ServiceAccountUIDKey]
    if name != sa.Name {
        return false
    }
    if len(uid) > 0 && uid != string(sa.UID) {
        return false
    }
    return true
}

所以,當 ServiceAccount 對象刪除時,需要刪除其所引用的所有 Secrets 對象。

再看如何新建 secret 對象的業務邏輯。當新建或更新 ServiceAccount 對象時,需要確保其引用的 Secrets 對象存在,不存在就需要新建個 secret 對象:

// 檢查該ServiceAccount對象引用的secrets對象存在,不存在則新建
func (e *TokensController) ensureReferencedToken(serviceAccount *v1.ServiceAccount) (bool, error) {
 // 首先確保serviceAccount.secrets欄位值中的secret都存在
 if hasToken, err := e.hasReferencedToken(serviceAccount); err != nil {
  return false, err
 } else if hasToken {
  return false, nil
 }

 // 對api-server發起請求查找該serviceAccount對象
 serviceAccounts := e.client.CoreV1().ServiceAccounts(serviceAccount.Namespace)
 liveServiceAccount, err := serviceAccounts.Get(serviceAccount.Name, metav1.GetOptions{})
 // ...
 if liveServiceAccount.ResourceVersion != serviceAccount.ResourceVersion {
  return true, nil
 }

 // 如果是新建的ServiceAccount,則給ServiceAccount.secrets欄位值添加個默認生成的secret對象
 secret := &v1.Secret{
  ObjectMeta: metav1.ObjectMeta{
   Name:      secret.Strategy.GenerateName(fmt.Sprintf("%s-token-", serviceAccount.Name)),
   Namespace: serviceAccount.Namespace,
   Annotations: map[string]string{
    v1.ServiceAccountNameKey: serviceAccount.Name, // 這裡使用serviceAccount.Name來作為annotation
    v1.ServiceAccountUIDKey:  string(serviceAccount.UID), // 這裡使用serviceAccount.UID來作為annotation
   },
  },
  Type: v1.SecretTypeServiceAccountToken,
  Data: map[string][]byte{},
 }

 // 生成jwt token,該token是用私鑰籤名的
 token, err := e.token.GenerateToken(serviceaccount.LegacyClaims(*serviceAccount, *secret))
 // ...
 secret.Data[v1.ServiceAccountTokenKey] = []byte(token)
 secret.Data[v1.ServiceAccountNamespaceKey] = []byte(serviceAccount.Namespace)
 if e.rootCA != nil && len(e.rootCA) > 0 {
  secret.Data[v1.ServiceAccountRootCAKey] = e.rootCA
 }

 // 向api-server中創建該secret對象
 createdToken, err := e.client.CoreV1().Secrets(serviceAccount.Namespace).Create(secret)
 // ...
 // 寫入LRU cache中
 e.updatedSecrets.Mutation(createdToken)

 err = clientretry.RetryOnConflict(clientretry.DefaultRetry, func() error {
  // ...
  // 把新建的secrets對象放入ServiceAccount.Secrets欄位中,然後更新ServiceAccount對象
  liveServiceAccount.Secrets = append(liveServiceAccount.Secrets, v1.ObjectReference{Name: secret.Name})
  if _, err := serviceAccounts.Update(liveServiceAccount); err != nil {
   return err
  }
  // ...
 })

 // ...
}

所以,當 ServiceAccount 對象新建時,需要新建個新的 Secret 對象作為 ServiceAccount 對象的引用。業務代碼還是比較簡單的。

Secret 的增刪改查

當增刪改查 secret 時,刪除 secret 時同時需要刪除 serviceAccount 對象下的 secrets 欄位引用;

func (e *TokensController) syncSecret() {
 // ...
 // 從LRU Cache中查找該secret
 secret, err := e.getSecret(secretInfo.namespace, secretInfo.name, secretInfo.uid, false)
 switch {
 case err != nil:
  klog.Error(err)
  retry = true
 case secret == nil:
  // 刪除secret時:
  // 查找serviceAccount對象是否存在
  if sa, saErr := e.getServiceAccount(secretInfo.namespace, secretInfo.saName, secretInfo.saUID, false); saErr == nil && sa != nil {
   // 從service中刪除其secret引用
   if err := clientretry.RetryOnConflict(RemoveTokenBackoff, func() error {
    return e.removeSecretReference(secretInfo.namespace, secretInfo.saName, secretInfo.saUID, secretInfo.name)
   }); err != nil {
    klog.Error(err)
   }
  }
 default:
  // 新建或更新secret時:
  // 查找serviceAccount對象是否存在
  sa, saErr := e.getServiceAccount(secretInfo.namespace, secretInfo.saName, secretInfo.saUID, true)
  switch {
  case saErr != nil:
   klog.Error(saErr)
   retry = true
  case sa == nil:
   // 如果serviceAccount都已經不存在,刪除secret
   if retriable, err := e.deleteToken(secretInfo.namespace, secretInfo.name, secretInfo.uid); err != nil {
                // ...
   }
  default:
   // 新建或更新secret時,且serviceAccount存在時,查看是否需要更新secret中的ca/namespace/token欄位值
   // 當然,新建secret時,肯定需要更新
   if retriable, err := e.generateTokenIfNeeded(sa, secret); err != nil {
    // ...
   }
  }
 }
}

所以,對 kubernetes.io/service-account-token 類型的 secret 增刪改查的業務邏輯,也比較簡單。重點是學習下官方 golang 代碼編寫和一些有關 k8s api的使用,對自己二次開發 k8s 大有裨益。

總結

本文主要學習 TokensController 是如何監聽 ServiceAccount 對象和 kubernetes.io/service-account-token 類型 Secret 對象的增刪改查,並做了相應的業務邏輯處理,比如新建 ServiceAccount 時需要新建對應的 Secret 對象,刪除 ServiceAccount 需要刪除對應的 Secret 對象,以及新建 Secret 對象時,還需要給該 Secret 對象補上 ca.crt/namespace/token欄位值,以及一些邊界條件的處理邏輯等等。

同時,官方的 TokensController 代碼編寫規範,以及對 k8s api 的應用,邊界條件的處理,以及使用了 LRU Cache 緩存等等,都值得在自己的項目裡參考。

學習要點

tokens_controller.go L106 使用了LRU cache。

參考文獻

為 Pod 配置服務帳戶

服務帳號令牌 Secret

serviceaccounts-controller 源碼官網解析



相關焦點

  • 《蹲坑學kubernetes》之17-14:ServerAccount
    《蹲坑學kubernetes》之17-14:ServerAccountAPI Server作為Kubernetes網關,是訪問和管理資源對象的唯一入口,其各種集群組件訪問資源都需要經過網關才能進行正常訪問和管理。
  • 深入解析Kubernetes service 概念
    深入解析Kubernetes service 概念Kubernetes在Kubernetes平臺上,Pod是有生命周期,為了可以給客戶端一個固定的訪問端點,因此需要在客戶端和Pod之間添加一個中間層,這個中間層稱之為ServiceService是什麼?
  • Kubernetes 1.14 二進位集群安裝
    /kube-controller-manager \\--profiling \\--cluster-name=kubernetes \\--controllers=*,bootstrapsigner,tokencleaner \\--kube-api-qps=1000 \\--kube-api-burst=2000 \\--leader-elect \\--use-service-account-credentials
  • client-go 源碼學習總結
    前言目前在雲原生社區的 Kubernetes 源碼研習社中和廣大學友們共同學習鄭東旭大佬的 Kubernetes 源碼剖析[1]這本書。當前正在開展第一期學習活動,第五章節 client-go 的學習。之所以從這一章節開始學習,主要是考慮到 client-go 在源碼中相對比較獨立,可以單獨閱讀。
  • Kubernetes學習筆記之LRU算法源碼解析
    Kubernetes學習筆記之LRU算法源碼解析Overview本文章基於k8s release-1.17分支代碼。之前一篇文章學習 Kubernetes學習筆記之ServiceAccount TokensController源碼解析 ,主要學習ServiceAccount有關知識,發現其中使用了LRU Cache,代碼在 L106 。
  • KubeEdge源碼分析之(三)cloudcore
    作者 | 之江實驗室端邊雲作業系統團隊原文連結:http://rrd.me/fd6wW前言本系列的源碼分析是在 commit da92692baa660359bb314d89dfa3a80bffb1d26c 之上進行的。
  • Kubernetes API 安全機制詳解
    有兩個可選參數:--service-account-key-file 給Token籤名的私鑰。如果未指定,將使用apiserver配置參數--tls-private-key-file指定的私鑰。--service-account-lookup 如果啟用,被Kubernetes API刪除的Token將被廢除。服務帳戶通常是Kubernetes API服務自動創建,並關聯到pod。
  • Kubernetes scheduler學習筆記
    為了能更好的使用它,所以從源碼的角度,對它進行一個全方位的分析與學習。scheduler的功能不多,但邏輯比較複雜,裡面有很多考慮的因素,總結下來大致有如下幾點:Leader選主,確保集群中只有一個scheduler在工作,其它只是高可用備份實例。通過endpoint:kube-scheduler作為仲裁資源。
  • 源碼視角,全方位學習Kubernetes scheduler
    12、VolumeNodePredicate> 無13、VolumeZonePredicate> 檢查存儲區域劃分:檢查Node中是否有label:failure-domain.beta.kubernetes.io/zone或者failure-domain.beta.kubernetes.io
  • Kubernetes RBAC角色權限控制
    Namespace是kubernetes項目中的一個邏輯管理單位。rolebinding.rbac.authorization.k8s.io/example-rolebinding createdrole.rbac.authorization.k8s.io/example-role createdserviceaccount/example-sa created
  • 初識K8S之理論和搭建
    controller-manager:執行集群級別的功能,如複製組件、追蹤工作結點狀態、處理結點失敗等。是整個集群的控制管理中心,kube-controler-manager中的nodecontroller模塊 通過apiservice提供的監聽接口,實時監控node機的狀態信息。
  • vSphere with Kubernetes實戰之:用戶訪問控制 - 文章精選 - CTI...
    1         5d6h  $ kubectl get secret  NAME                                 TYPE                                  DATA   AGE  default-token-lqfwr                  kubernetes.io/service-account-token
  • Kubernetes 1.8.0 版本發布
    SIG NodeSIG Node 負責 Pod 和 Host 主機之間資源交互的組件,以及管理節點上 Pod 的生命周期對於 1.8 版本,SIG Node 繼續專注於支持更廣泛的工作負載類型,包括支持硬體和對性能敏感的工作負載,如數據分析和深度學習,同時不斷增強 Node 的可靠性。
  • Kubernetes ELK 日誌收集
    /es/es.yamlkubectl apply -f es.yaml創建es svc#這裡的service使用無頭服務wget down.i4t.com/kubernetes/es/es-svc.yamlkubect apply
  • Kubernetes-應用部署問題定位和處理
    /kubernetes/issues/6842pods/mypod接下來,要檢查的是apiserver上的Pod是否與要創建的Pod相匹配。3.2 能否通過DNS解析正常解析代理服務對於處於同一個命名空間的容器化應用,可以直接通過代理服務的名稱(mysql-0-svc)訪問MySQL master。
  • Kubernetes的Local Persistent Volumes使用小記
    歡迎訪問我的GitHubhttps://github.com/zq2599/blog_demos內容:所有原創文章分類匯總及配套源碼,涉及Java、Docker、Kubernetes、DevOPS等;和常見的分布式文件系統相比,本地磁碟故障會導致數據丟失,保存重要數據請勿使用HostPath Volume和Local PV;基本概念說完了,接下來實戰體驗;實戰環境信息作業系統:CentOS Linux release 7.8.2003 (Core)kubernetes
  • 使用 Kubernetes 最易犯的 10 個錯誤
    或者,更好的方法是,部署一個像 nginx-ingress-controller(或者 traefik)之類的東西作為暴露給外部負載均衡器的單個 NodePort endpoint,並基於 K8s ingress resource 配置在集群中分配並路由流量。
  • Kubernetes 入門命令整理及解析
    方便記憶的規律kubernetes命令有一些相通的規律,可以幫助我們快速掌握。|service -o wide #獲取特定的資源類型kubectl get nodes --show-labels # 查看node標籤kubectl api-versions # 查看k8s當前支持的api版本kubectl logs pod_name # 查看pod內進程輸出