Kubernetes 的 Service Account (服務帳戶) 是一個由 Kubernetes 管理的帳戶類型,在管理上可說是特別方便,但是在剛接觸到這種帳戶類型時,不是很容易理解應用的情境。這篇文章是我閱讀了好多份文件之後,整理出來的完整脈絡,相信可以對服務帳戶有一定程度的理解。

帳號類型
Kubernetes 的帳號有兩種類型,分別為:
- 
使用者帳戶 (Normal Users)
任何人想要連接並存取 Kubernetes 叢集,都需要先建立一個 "使用者帳戶" 並將憑證資訊提供給用戶端 (如: kubectl),以便通過 Kubernetes 的 API server 的認證 (Authentication)。
我覺得這個名字應該稱為 User Accounts 會比較好理解,但是 Kubernetes 官網稱一般使用者 (Normal Users)。
 
- 
服務帳戶 (Service Accounts)
任何跑在 Pod 裡面的容器想要存取 Kubernetes 的 API 伺服器 (kube-apiserver),就需要先有一個 "服務帳戶" 綁定在 Pod 身上,然後以便通過 Kubernetes 的 API 伺服器的身份認證 (Authentication)。
 
體驗命名空間預設的服務帳戶 (Service Account)
當你建立 namespace 的時候,預設就會幫你建立好一個名為 default 的服務帳戶:
- 
建立 dev 命名空間
kubectl create namespace dev
kubectl label namespace dev name=dev
 
- 
取得 dev 命名空間下的 serviceaccounts
kubectl get serviceaccounts --namespace=dev
NAME      SECRETS   AGE
default   1         9s
 
- 
取得 dev 命名空間下的 default 服務帳戶內容
kubectl get serviceaccounts default -n=dev -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  creationTimestamp: "2022-08-23T14:55:51Z"
  name: default
  namespace: dev
  resourceVersion: "1434536"
  selfLink: /api/v1/namespaces/dev/serviceaccounts/default
  uid: 3b750bc5-fd6c-43b0-9c64-4a4700f522ae
secrets:
- name: default-token-xpqc7
他會自動綁定一個 secrets 保存該服務帳戶的 Token 資訊
 
- 
取得 dev 命名空間下的 default 服務帳戶綁定的 secrets 內容
kubectl get secrets default-token-xpqc7 -n=dev -o yaml
apiVersion: v1
data:
  ca.crt: DATA+OMITTED
  namespace: ZGV2
  token: TOKEN+OMITTED
kind: Secret
metadata:
  annotations:
    kubernetes.io/service-account.name: default
    kubernetes.io/service-account.uid: 3b750bc5-fd6c-43b0-9c64-4a4700f522ae
  creationTimestamp: "2022-08-23T14:55:51Z"
  name: default-token-xpqc7
  namespace: dev
  resourceVersion: "1434535"
  selfLink: /api/v1/namespaces/dev/secrets/default-token-xpqc7
  uid: 868a7d4f-74b8-4be4-8c0e-b9d5a3e678b2
type: kubernetes.io/service-account-token
 
體驗 Pod 如何使用服務帳戶 (Service Account)
- 
我們先在 dev 命名空間中建立一個 Pod,但不特別指定 default 服務帳戶
kubectl run microbot --image=dontrebootme/microbot:v1 -n dev
 
- 
事實上,所有的 Pod 預設會加入 default 服務帳戶
在 namespace 建立 Pod 的時候,如果沒有特別指定 spec.serviceAccountName 的話,Kubernetes 也會預設幫你加上相同命名空間下的 default 服務帳戶。因此,每個 Pod 其實都一定會綁定一個服務帳戶。
kubectl get microbot -n dev -o yaml
此時你會看到如下的 YAML 檔,有個 serviceAccountName: default 已經被自動設定進去了:
apiVersion: v1
kind: Pod
metadata:
  labels:
    run: microbot
  name: microbot
  namespace: dev
spec:
  containers:
  - image: dontrebootme/microbot:v1
    imagePullPolicy: IfNotPresent
    name: microbot
    resources: {}
    terminationMessagePath: /dev/termination-log
    terminationMessagePolicy: File
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-dfs8b
      readOnly: true
  dnsPolicy: ClusterFirst
  enableServiceLinks: true
  nodeName: microk8s-vm
  preemptionPolicy: PreemptLowerPriority
  priority: 0
  restartPolicy: Always
  schedulerName: default-scheduler
  securityContext: {}
  serviceAccount: default
  serviceAccountName: default
  ...
而且,在 Pod 裡面預設會掛載一個 /var/run/secrets/kubernetes.io/serviceaccount 目錄!
 
- 
查看 Pod 裡面的 /var/run/secrets/kubernetes.io/serviceaccount 目錄內容
kubectl exec microbot -it -n dev -- sh
ls -laF /var/run/secrets/kubernetes.io/serviceaccount

看起來他會自動把 serviceaccounts 所包含的 secrets 內容全部掛載到這個目錄下!
 
- 
在 Pod 的容器中對 kube-apiserver 發送 HTTP 要求
apk update
apk add curl
curl --insecure https://kubernetes.default.svc.cluster.local:443/api/
在沒有帶入 TOKEN 的情況下,會是以 system:anonymous 匿名使用者存取:
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {
  },
  "status": "Failure",
  "message": "forbidden: User \"system:anonymous\" cannot get path \"/api/\"",
  "reason": "Forbidden",
  "details": {
  },
  "code": 403
}
 
- 
帶入 default 服務帳戶的 TOKEN 來存取 kube-apiserver
CACERT='/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc.cluster.local:443/api/
此時你會看到 API 要求已經通過驗證:
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "172.25.239.227:16443"
    }
  ]
}
 
以上就是在 Pod 裡面使用「服務帳戶」的標準方法! 👍
替服務帳戶 (Service Account) 加入角色權限
事實上,這個「通過身份驗證」的 TOKEN 雖然可以呼叫 kube-apiserver 的部分 API,但事實上目前這個預設的服務帳戶並無法存取任何 K8s 叢集中的資源,我們還必須透過 RBAC 機制,先建立一個 Role 並指派權限進去,再透過 RoleBinding 將此帳號綁定,才能賦予他足夠的權限存取資源。
- 
先試圖取得 PodList 清單
curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc.cluster.local:443/api/v1/namespaces/dev/pods/
你會得到以下錯誤訊息:
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "pods is forbidden: User \"system:serviceaccount:dev:default\" cannot list resource \"pods\" in API group \"\" in the namespace \"dev\"",
  "reason": "Forbidden",
  "details": {
    "kind": "pods"
  },
  "code": 403
}
此時我們會知道我們的 User 為 system:serviceaccount:dev:default,且 API group 為 "",命名空間(namespace)為 dev,資源類型(kind)為 pods
除此之外,你也可以透過 kubectl auth can-i 命令,快速查出特定使用者是否具有特定資源的權限,相當實用!
kubectl auth can-i get pods -n=dev --as=system:serviceaccount:dev:default
他會很簡單的回你 yes 或 no,以目前的狀態來說,應該會回 no 才對! 👍
 
- 
建立 Role 物件
kubectl create role read-pods -n=dev --verb='get,list' --resource=pods
或透過 --dry-run=client -o yaml 參數產生相對應的 YAML 檔內容:
kubectl create role read-pods -n=dev --verb='get,list' --resource=pods --dry-run=client -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  creationTimestamp: null
  name: read-pods
  namespace: dev
rules:
- apiGroups:
  - ""
  resources:
  - pods
  verbs:
  - get
  - list
 
- 
建立 RoleBinding 物件
kubectl create rolebinding read-pods -n dev --user=system:serviceaccount:dev:default --role=read-pods
或透過 --dry-run=client -o yaml 參數產生相對應的 YAML 檔內容:
kubectl create rolebinding read-pods -n dev --user=system:serviceaccount:dev:default --role=read-pods --dry-run=client -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  creationTimestamp: null
  name: read-pods
  namespace: dev
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: read-pods
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: system:serviceaccount:dev:default
上述語法也可以在 subjects: 欄位寫成這樣,其實都是一樣的,服務帳戶 (kind: ServiceAccount) (name: default) 就是 一般帳戶 (kind: User) (name: system:serviceaccount:dev:default),只是名稱表達的方式不同而已:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods
  namespace: dev
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: read-pods
subjects:
- kind: ServiceAccount
  name: default
先檢查一下 system:serviceaccount:dev:default 服務帳戶是否已經獲得 pods 的 get 權限:
kubectl auth can-i get pods -n=dev --as=system:serviceaccount:dev:default
kubectl auth can-i list pods -n=dev --as=system:serviceaccount:dev:default
 
- 
再取得一次 PodList 清單
curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc.cluster.local:443/api/v1/namespaces/dev/pods/
此時應該就能夠取得完整的 PodList 清單與詳細資訊了:
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "selfLink": "/api/v1/namespaces/dev/pods/",
    "resourceVersion": "1446386"
  },
  "items": [
    {
      "metadata": {
        "name": "microbot",
        "namespace": "dev",
        ...
      }
      ...
    }
    ...
  ]
}
 
體驗建立新的服務帳戶
有了上述對於 default 服務帳戶的理解,相信也就不難理解自訂服務帳戶的用法,以下是體驗的步驟:
- 
在 dev 命名空間建立自訂的服務帳戶 monitor
kubectl create serviceaccount monitor -n dev
 
- 
設定 Role 與 RoleBinding
kubectl create role monitor-pods -n=dev --verb='get,list,watch' --resource='pods,pods/status'
kubectl create rolebinding monitor-pods -n dev --serviceaccount='dev:monitor' --role=monitor-pods
 
- 
使用 YAML 建立 Pod 並指定 serviceAccountName: monitor 服務帳戶
apiVersion: v1
kind: Pod
metadata:
  name: "microbot"
  namespace: dev
  labels:
    app: "microbot"
spec:
  containers:
  - name: microbot
    image: "dontrebootme/microbot:v1"
  serviceAccountName: monitor
用 PowerShell 快速套用的方法:
@'
apiVersion: v1
kind: Pod
metadata:
  name: "microbot"
  namespace: dev
  labels:
    app: "microbot"
spec:
  containers:
  - name: microbot
    image: "dontrebootme/microbot:v1"
  serviceAccountName: monitor
'@ | kubectl apply -f -
 
- 
從 Pods 內的容器呼叫 API 伺服器
kubectl exec microbot -it -n dev -- sh
apk update
apk add curl
CACERT='/var/run/secrets/kubernetes.io/serviceaccount/ca.crt'
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl --cacert $CACERT -H "Authorization: Bearer $TOKEN" https://kubernetes.default.svc.cluster.local:443/api/v1/namespaces/dev/pods/
 
總結
有了這一層理解,你大概就知道如何從你在 Pod 的應用程式中存取 Kubernetes 叢集中的資源。
以前我會覺得要從 Pod 讀取 ConfigMaps 或 Secrets 都要透過 volumeMounts (目錄或檔案) 或 env (環境變數) 的方式掛載進去,但事實上透過 volumeMounts 或 env 的方式並無法套用 RBAC 授權,當你想限制應用程式對叢集資源的存取範圍時,還要透過變更 YAML 並找有力人士套用更新,這也太不可靠了吧。況且,每次變更組態設定都要重新部署 Deployment 或重新啟動 Deployment 才能生效,也沒有很方便。
現在透過服務帳戶的方式,你可以直接從應用程式存取 Kubernetes 資源,還能透過 RBAC 機制限制存取的範圍,可謂是兼具彈性與安全,我覺得相當不錯! 👍
相關連結