kubeadm join はノードをクラスターにどのように追加しているのか

kubeadm では、kubeadm join コマンドの実行で、新しいノードの追加がかんたんにできます。 このとき何が行われているのかを調べてみました。

ノード追加のワークフロー

The join workflow

kubeadm join では次のようなステップが行われる。

  1. API Server から必要となるクラスターの情報をダウンロードする
    • Bootstrap token と CA key hash を使ってデータの信頼性を検証する
  2. クラスターの情報が取得できたら kubelet は TLS bootstrapping プロセスを開始できる
    • TLS Bootstrap では shared token を使用して Kubernetes API Server で一時的に認証をし、証明書署名要求 (CSR) を送信する
    • コントロール プレーンは既定でこの CSR リクエストに自動的に署名する
  3. ノードに割り当てられた identity を使って kubelet から API Server に接続するよう構成する

コントロール プレーンのノードを追加する場合では、上記に加え、コントロール プレーンのコンポーネントの構成や etcd メンバーの追加が行われる。

kubeadm join のログを追ってみる

kubeadm join phases internal design

実際に kubeadm join コマンドを実行し、ログ出力からどのような処理が行われているかを観察します。

root@node02:~# kubeadm join 192.168.56.10:6443 \
  --token w7b0wx.ekyppyi0gpy54eb7 \
  --discovery-token-ca-cert-hash sha256:1c4dd8edc15c4e3fa422c373407ba40e71f4fe140865ebb3c7878d70502fa235 \
  -v 10

以下では各フェーズのログを抜粋/整形して紹介します。

Preflight checks フェーズでは、クラスターの動作要件を満たしているかどうかのチェックが行われます。

[preflight] Running pre-flight checks
I0226 05:39:19.365646    6279 preflight.go:92] [preflight] Running general checks

続いて Discovery cluster-info フェーズが実行されます。 このフェーズは Shared token discovery と File/https discovery の2種類がありますが、今回は --token オプションを渡しているため Shared token discovery となります。

--token string Use this token for both discovery-token and tls-bootstrap-token when those values are not provided.

Shared token discovery では、CA 証明書を kube-public ネームスペースの cluster-info ConfigMap から取得します。

I0226 05:39:19.467065    6279 join.go:530] [preflight] Discovering cluster-info
I0226 05:39:19.467085    6279 token.go:80] [discovery] Created cluster-info discovery client, requesting info from "192.168.56.10:6443"
I0226 05:39:19.467432    6279 round_trippers.go:466] curl -v -XGET  -H "Accept: application/json, */*" 
  'https://192.168.56.10:6443/api/v1/namespaces/kube-public/configmaps/cluster-info?timeout=10s'
...
I0226 05:39:19.482307    6279 request.go:1181] Response Body:
{
  "kind": "ConfigMap",
  "apiVersion": "v1",
  "metadata": {
    "name": "cluster-info",
    "namespace": "kube-public",
    ...
  }
  "data": {
    "kubeconfig": "apiVersion: v1
      clusters:
      - cluster:
          certificate-authority-data: <CA cert data>
          server: https://192.168.56.10:6443
    ...
  }
}

取得した CA 証明書は --discovery-token-ca-cert-hash フラグのハッシュ値を利用して公開鍵が検証されます。 その後追加の検証として、セキュアな接続を介して再度 cluster-info の取得を行い、最初に取得した CA との比較が行われます。

I0226 05:39:19.483177    6279 token.go:118] [discovery] Requesting info from "192.168.56.10:6443" again to validate TLS against the pinned public key
I0226 05:39:19.483702    6279 round_trippers.go:466] curl -v -XGET  -H "Accept: application/json, */*"
  'https://192.168.56.10:6443/api/v1/namespaces/kube-public/configmaps/cluster-info?timeout=10s'

kube-system ネームスペースの ConfigMap から、クラスターの構成情報を含む kubeadm の設定と、kube-proxy / kubelet の kubeconfig が取得されます。

I0226 05:39:19.492823    6279 token.go:135] [discovery] Cluster info signature and contents are valid and TLS certificate validates against pinned roots, will use API Server "192.168.56.10:6443"
I0226 05:39:19.493000    6279 discovery.go:52] [discovery] Using provided TLSBootstrapToken as authentication credentials for the join process
I0226 05:39:19.493169    6279 join.go:544] [preflight] Fetching init configuration
I0226 05:39:19.493347    6279 join.go:590] [preflight] Retrieving KubeConfig objects
[preflight] Reading configuration from the cluster...
[preflight] FYI: You can look at this config file with 'kubectl -n kube-system get cm kubeadm-config -o yaml'
I0226 05:39:19.495291    6279 round_trippers.go:466] curl -v -XGET  -H "Accept: application/json, */*" -H "Authorization: Bearer <masked>"
  'https://192.168.56.10:6443/api/v1/namespaces/kube-system/configmaps/kubeadm-config?timeout=10s'
I0226 05:39:19.499289    6279 round_trippers.go:466] curl -v -XGET  -H "Authorization: Bearer <masked>" -H "Accept: application/json, */*"
  'https://192.168.56.10:6443/api/v1/namespaces/kube-system/configmaps/kube-proxy?timeout=10s'
I0226 05:39:19.869234    6279 round_trippers.go:466] curl -v -XGET  -H "Accept: application/json, */*" -H "Authorization: Bearer <masked>"
  'https://192.168.56.10:6443/api/v1/namespaces/kube-system/configmaps/kubelet-config-1.23?timeout=10s'

クラスターの情報が揃うと bootstrap-kubelet.conf ファイルが書き込まれます。

I0226 05:39:19.881471    6279 kubelet.go:119] [kubelet-start] writing bootstrap kubelet config file at /etc/kubernetes/bootstrap-kubelet.conf
I0226 05:39:19.882004    6279 kubelet.go:134] [kubelet-start] writing CA certificate at /etc/kubernetes/pki/ca.crt
I0226 05:39:19.882379    6279 loader.go:372] Config loaded from file:  /etc/kubernetes/bootstrap-kubelet.conf

その後 kubelet が開始されます。 kubelet は、起動後に bootstrap-kubelet.conf が存在する場合に、TLS Bootstrap のプロセスを実行します。

TLS Bootstrap では、bootstrap token を利用して一時的に Kubernetes API Server の認証を行い、ノード用の CSR リクエストを送信します。 CSR リクエストは自動的に承認され、ノードのクライアント証明書や kubelet.conf が生成されて、ノードのクラスターへの参加処理が完了します。

[kubelet-start] Writing kubelet configuration to file "/var/lib/kubelet/config.yaml"
[kubelet-start] Writing kubelet environment file with flags to file "/var/lib/kubelet/kubeadm-flags.env"
[kubelet-start] Starting the kubelet
[kubelet-start] Waiting for the kubelet to perform the TLS Bootstrap...
I0226 05:39:25.111213    6279 loader.go:372] Config loaded from file:  /etc/kubernetes/kubelet.conf

kubeadm join 完了時のメッセージ。

This node has joined the cluster:
* Certificate signing request was sent to apiserver and a response was received.
* The Kubelet was informed of the new secure connection details.

Run 'kubectl get nodes' on the control-plane to see this node join the cluster.

TLS Bootstrapping をより詳しく見ていく

実際のクラスター コンポーネントの設定やリソース内容を観察してみます。 さらに、kubeadm join コマンドと同じステップを手動で行うことで、ノードが追加されるのか試してみよう。

コントロール プレーン コンポーネントのフラグ

kube-apiserver には、Bootstrap Token による認証が利用できるように --enable-bootstrap-token-auth=true フラグが設定されています。

$ ps aux | grep kube-apiserver
root        7139  2.8 15.5 1112196 315216 ?      Ssl  14:21  14:55 kube-apiserver
... --enable-bootstrap-token-auth=true ...

CSR の署名は kube-controller-manager によって行われます。 --cluster-signing-cert-file--cluster-signing-key-file フラグで、CA 証明書と鍵ファイルが指定されていまする。

$ ps aux | grep kube-controller-manager
root        7149  0.8  5.1 825164 104044 ?       Ssl  14:21   4:18 kube-controller-manager
... --cluster-signing-cert-file=/etc/kubernetes/pki/ca.crt --cluster-signing-key-file=/etc/kubernetes/pki/ca.key ...

Bootstrap Token

Bootstrap Token Secret Format

Bootstrap Token は kube-system ネームスペース内に Secret として配置されています。

$ kubeadm token list
TOKEN                     TTL         EXPIRES                USAGES                   DESCRIPTION                                                EXTRA GROUPS
w7b0wx.ekyppyi0gpy54eb7   6h          2022-02-27T05:28:24Z   authentication,signing   The default bootstrap token generated by 'kubeadm init'.   system:bootstrappers:kubeadm:default-node-token

$ kubectl get secret -n kube-system bootstrap-token-w7b0wx
NAME                     TYPE                            DATA   AGE
bootstrap-token-w7b0wx   bootstrap.kubernetes.io/token   7      17h

kubectl get secret -n kube-system bootstrap-token-w7b0wx -o yaml で Secret の中身を確認すると以下のようになっています (各値をデコード済み)。

apiVersion: v1
kind: Secret
metadata:
  creationTimestamp: "2022-02-26T05:28:24Z"
  name: bootstrap-token-w7b0wx
  namespace: kube-system
  resourceVersion: "219"
  uid: 0d01669e-65cb-413c-add4-c6ced5219283
type: bootstrap.kubernetes.io/token
data:
  auth-extra-groups: system:bootstrappers:kubeadm:default-node-token
  description: The default bootstrap token generated by 'kubeadm init'.
  expiration: 2022-02-27T05:28:24Z
  token-id: w7b0wx
  token-secret: ekyppyi0gpy54eb7
  usage-bootstrap-authentication: true
  usage-bootstrap-signing: true

token-idtoken-secret を連結して、この場合 w7b0wx.ekyppyi0gpy54eb7 が token となります。 また、auth-extra-groups が CSR リクエストを行う際のグループ名となります。

RBAC による CSR 作成/自動承認/自動更新の許可

Approval

Bootstrap Token で設定された auth-extra-groups のグループに対して、CSR リクエストを作成できる権限が付与されます。 system:bootstrappers:kubeadm:default-node-token Group に対して system:node-bootstrapper が割り当てられています。

$ kubectl get clusterrolebinding kubeadm:kubelet-bootstrap
NAME                        ROLE                                   AGE
kubeadm:kubelet-bootstrap   ClusterRole/system:node-bootstrapper   17h
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  creationTimestamp: "2022-02-26T05:28:25Z"
  name: kubeadm:kubelet-bootstrap
  resourceVersion: "222"
  uid: 4f53106c-7925-40ec-a578-de39bca774d1
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:node-bootstrapper
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:bootstrappers:kubeadm:default-node-token

こちらは kubelet が CSR を Auto-Approve できるように許可する ClusterRoleBinding。

$ kubectl get clusterrolebinding kubeadm:node-autoapprove-bootstrap
NAME                                 ROLE                                                                           AGE
kubeadm:node-autoapprove-bootstrap   ClusterRole/system:certificates.k8s.io:certificatesigningrequests:nodeclient   17h
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  creationTimestamp: "2022-02-26T05:28:25Z"
  name: kubeadm:node-autoapprove-bootstrap
  resourceVersion: "223"
  uid: e77928e5-8a2c-428c-9c52-6b046cc8c556
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:certificates.k8s.io:certificatesigningrequests:nodeclient
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:bootstrappers:kubeadm:default-node-token

こちらは kubelet が証明書を自動更新することを許可するための ClusterRoleBinding。

$ kubectl get clusterrolebinding kubeadm:node-autoapprove-certificate-rotation
NAME                                            ROLE                                                                               AGE
kubeadm:node-autoapprove-certificate-rotation   ClusterRole/system:certificates.k8s.io:certificatesigningrequests:selfnodeclient   17h
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  creationTimestamp: "2022-02-26T05:28:25Z"
  name: kubeadm:node-autoapprove-certificate-rotation
  resourceVersion: "224"
  uid: 66e996da-dbb3-4012-870d-d2522dc77104
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:certificates.k8s.io:certificatesigningrequests:selfnodeclient
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: system:nodes

CSR 作成や Auto-Approve では、クラスターにノードが参加する段階であるため system:bootstrappers の Group が使用されています。 一方、証明書の自動更新の段階では、ノードの bootstrap は終了しており Node として動作をしているため、system:nodes に対して権限を割り当てます。

新規ノードの追加

それでは実際にクラスターへ新規ノードを追加してみよう。 あらかじめ kubeadm 環境で必要なパッケージをインストールしておきます。

/etc/systemd/system/kubelet.service.d/10-kubeadm.conf から、kubelet に読み込まれる設定を確認し、必要なファイルのパスを確認しておきます。

# Note: This dropin only works with kubeadm and kubelet v1.11+
[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"
# This is a file that "kubeadm init" and "kubeadm join" generates at runtime, populating the KUBELET_KUBEADM_ARGS variable dynamically
EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env
# This is a file that the user can use for overrides of the kubelet args as a last resort. Preferably, the user should use
# the .NodeRegistration.KubeletExtraArgs object in the configuration files instead. KUBELET_EXTRA_ARGS should be sourced from this file.
EnvironmentFile=-/etc/default/kubelet
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS

以下のファイルを用意します:

  • /etc/kubernetes/pki/ca.crt
  • /etc/kubernetes/bootstrap-kubelet.conf
  • /var/lib/kubelet/config.yaml
  • /var/lib/kubelet/kubeadm-flags.env

ca.crt の取得

初回アクセスは TLSVerify なしの insecure な通信になっているよう。

https://github.com/kubernetes/kubernetes/blob/043611723136d30ec76782f96c5bb03fe7a47ca4/cmd/kubeadm/app/discovery/token/token.go#L76-L84

curl --insecure 'https://192.168.56.10:6443/api/v1/namespaces/kube-public/configmaps/cluster-info' | jq -r '.data.kubeconfig' > cluster-info

certificate-authority-data フィールドからエンコードされた証明書データを取り出し、デコードする。

% echo 'LS0tLS...' | base64 -d > ca.crt
% file ca.crt
ca.crt: PEM certificate

% mkdir /etc/kubernetes/pki
% mv ca.crt /etc/kubernetes/pki/

取得した ca.crt を使って API Server にアクセスができることを確認します。

curl --cacert /etc/kubernetes/pki/ca.crt -v \
  'https://192.168.56.10:6443/api/v1/namespaces/kube-public/configmaps/cluster-info'

Kubelet Config の取得

kubelet-config-1.23 ConfigMap を取得します。

API Server へアクセスする際の cacert には、先程取得した ca.crt ファイルを使用し、Authorization ヘッダーには Bootstrap Token を使用します。

% mkdir /var/lib/kubelet
% curl -s --cacert /etc/kubernetes/pki/ca.crt \
  -H "Authorization: Bearer w7b0wx.ekyppyi0gpy54eb7" \
  https://192.168.56.10:6443/api/v1/namespaces/kube-system/configmaps/kubelet-config-1.23 | jq -r '.data.kubelet' > /var/lib/kubelet/config.yaml

bootstrap-kubelet.conf の作成

kubelet configuration

kubectl コマンドで bootstrap-kubelet.conf ファイルを生成します。

sudo su -

kubectl config --kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
  set-cluster bootstrap \
  --server='https://192.168.56.10:6443' \
  --certificate-authority=/etc/kubernetes/pki/ca.crt

kubectl config --kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
  set-credentials kubelet-bootstrap \
  --token=w7b0wx.ekyppyi0gpy54eb7

kubectl config --kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
  set-context bootstrap \
  --user=kubelet-bootstrap \
  --cluster=bootstrap

kubectl config --kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf \
  use-context bootstrap

kubeadm-flags.env の作成

以下の内容で /var/lib/kubelet/kubeadm-flags.env ファイルを作成します。

KUBELET_KUBEADM_ARGS="--container-runtime=remote --container-runtime-endpoint=/run/containerd/containerd.sock --pod-infra-container-image=k8s.gcr.io/pause:3.6"

kubelet を開始する

あとは kubelet サービスを開始するだけです。 kubelet が開始されたときに、--kubeconfig で指定されたファイルが存在しない場合は、--bootstrap-kubeconfig で指定された kubeconfig を利用して TLS Bootstrapping のプロセスが行われます。

CSR リクエストが完了しノードの証明書が発行されると、証明書ファイルが /var/lib/kubelet/pki に配置され、その証明書を参照する --kubeconfig ファイルが生成されます。

systemctl start kubelet
journalctl -u kubelet -f

node03 が追加されました。

% kubectl get nodes
NAME     STATUS   ROLES                  AGE    VERSION
node01   Ready    control-plane,master   19h    v1.23.4
node02   Ready    <none>                 18h    v1.23.4
node03   Ready    <none>                 107s   v1.23.4

参考リンク

kubeadmって何やってるの?公式ページをちゃんと読む - Qiita

はじめに つい先日kubeadmを使ってKubernetes環境を構築しましたが、実際kubeadmは何をやっているのかよく分かっていませんでしたので、何か説明してくれているものはないか、と探しました。すると公式ページで説明して...

kubeadm join Deep Dive

KubeCon 参加中にKubernetes Advent Calendarの7日目の記事投稿!注: ほぼドキュメントベースで記事を書いているので、バージョン混在してるかも。背景もともと、OpenStack Magnumというプロジェクトで OpenStack 上に Kubernetes をデプロイするサービスの開発を行っていたこともあって、Kubernetes のデプロイ周りは結構気になることが多い。最近だと、自宅クラスター用ベアメタル上に Kubernetes をデプロイするRemoraという

TLS Bootstrapping · YOKOHEI.COM

<h1><a class="anchor" aria-hidden="true" id="tls-bootstrapping"></a><a href="#tls-bootstrapping" aria-hidden="true" class="hash-link"><svg class="hash-link-icon" aria-hidden="true" height="16" version="1.1" viewBox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>TLS Bootstrapping?</h1>


comments powered by Disqus