Kubelet の Dynamic Kubelet Configuration について

# Kubelet の Dynamic Kubelet Configuration の概要

Dynamic Kubelet Configuration は kubelet の設定を kubernetes の API ( `ConfigMap` ) を利用して変更できるようにする機能です。

![Dynamic Kubelet Configuration](https://d33wubrfki0l68.cloudfront.net/8e1bf66acc68b79849f6d7dc687d3d3fc5c5f5d8/925f3/images/blog/2018-07-11-dynamic-kubelet-configuration/kubelet-diagram.png)
[Dynamic Kubelet Configuration](https://kubernetes.io/blog/2018/07/11/dynamic-kubelet-configuration/)

Dynamic Kubelet Configuration による Kubelet の設定変更の流れは下記の様になります。

1. Dynamic Kubelet Configuration を有効化する（設定ファイルのパスを指定し、Kubelet の ConfigMap を参照するように Node のマニフェストを変更する）
2. Kubelet の設定を持つ ConfigMap を新規に作成、または変更する
3. Kubelet は ConfigMap の変更を検知し、設定を1で指定したパスに展開させる
4. Kubelet が再起動され設定が反映される

# なぜ必要？
元々 Kubelet の設定は起動時のオプションでしか設定できなかったので、設定を変更するためには VM に ssh してオプションを直接変更して再起動させるなどの方法しかありませんでした。 kubernetes の v1.10 で Kubelet の設定をファイルで指定できるようになり ( `--config` )、設定のバージョン管理などは可能となりましたが、設定ファイルを各 VM に反映させる必要がありました。
Dynamic Kubelet Configuration は、Kubelet の設定を ConfigMap に持たせることでKubelet を kubernetes の他のリソースと同じ用に扱うことができるようにする機能で、これにより Kubelet の設定のバージョン管理ができるようなるだけでなく、設定の変更の適用や設定に不備があった場合のロールバックが容易になりました。

# 準備
kubeadm を使い kubernetes の環境を作成します。（参考: [Self-managed Kubernetes in Google Compute Engine (GCE)](https://docs.projectcalico.org/getting-started/kubernetes/self-managed-public-cloud/gce)）

```sh
$ kubectl get node
NAME         STATUS   ROLES    AGE     VERSION
controller   Ready    master   4h54m   v1.19.4
worker       Ready    <none>   4h50m   v1.19.4
```


# Dynamic Kubelet Configuration の有効化

Dynamic Kubelet Configuration を利用するには↓の２つの flag を設定する必要があります。
- `--feature-gates="DynamicKubeletConfig=true"`
  - Dynamic Kubelet Configuration の有効化
- `--dynamic-config-dir=<path>`
  - Kubelet の ConfigMap の設定を保存する場所の指定

`--feature-gates="DynamicKubeletConfig=true"` については、デフォルトで true になってるので問題ないです。

`--dynamic-config-dir=<path>` では、↑の概要で説明したように Kubelet の ConfigMap の変更を Kubelet で検知し、変更があった場合に一度その変更をローカルのストレージに保存し再起動時に参照するさいのパスを指定します。

kubeadm のインストール時に生成された Kubelet のユニットファイルを見てみると、 Kubelet の起動時に指定されるフラグが `/var/lib/kubelet/kubeadm-flags.env` に記載されてることがわかる。

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
```

worker のインスタンスで `/var/lib/kubelet/kubeadm-flags.env` を編集して `--dynamic-config-dir=<path>` のフラグを下記のように追加する。

```
KUBELET_KUBEADM_ARGS="--network-plugin=cni --pod-infra-container-image=k8s.gcr.io/pause:3.2 --dynamic-config-dir=/var/lib/kubelet-dynamic" 
```

Kubelet の service を再起動する

```
sudo systemctl restart kubelet
```

Kubelet のプロセスを確認すると `--dynamic-config-dir=/var/lib/kubelet-dynamic` のフラグが付いた状態で起動していることが確認できる。

```sh
$ ps aux | grep kubelet
root     28593 15.2  1.2 1717092 94224 ?       Ssl  14:43   0:00 /usr/bin/kubelet --bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf --config=/var/lib/kubelet/config.yaml --network-plugin=cni --pod-infra-container-image=k8s.gcr.io/pause:3.2 --dynamic-config-dir=/var/lib/kubelet-dynamic
```

# Kubelet の設定を変更する

概要で説明したように Dynamic Kubelet Configuration は稼働中の Kubelet の設定を持つ ConfigMap の変更を検知してるので、Kubelet の設定を ConfigMap に持たせる必要がある。

現状の Node の設定は apiserver に対して `/api/v1/nodes/[NODE_NAME]/proxy/configz` の GET リクエストを送ることで取得できるので、apiserver にアクセスするために `kubectl proxy` をバックグラウンドで実行し jq コマンドを利用して設定ファイルを作成する。

```sh
$ curl -sSL "http://localhost:8001/api/v1/nodes/worker/proxy/configz" | jq '.kubeletconfig|.kind="KubeletConfiguration"|.apiVersion="kubelet.config.k8s.io/v1beta1"' > kubelet_configz_worker
```

kubelet_configz_worker という名前で設定ファイルが作成された。

```json
$ cat kubelet_configz_worker
{
  "enableServer": true,
  "staticPodPath": "/etc/kubernetes/manifests",
  "syncFrequency": "1m0s",
  "fileCheckFrequency": "20s",
  "httpCheckFrequency": "20s",
  "address": "0.0.0.0",
  "port": 10250,
  "tlsCertFile": "/var/lib/kubelet/pki/kubelet.crt",
  "tlsPrivateKeyFile": "/var/lib/kubelet/pki/kubelet.key",
  "rotateCertificates": true,
  "authentication": {
    "x509": {
      "clientCAFile": "/etc/kubernetes/pki/ca.crt"
    },
    "webhook": {
      "enabled": true,
      "cacheTTL": "2m0s"
    },
    "anonymous": {
      "enabled": false
    }
  },
  "authorization": {
    "mode": "Webhook",
    "webhook": {
      "cacheAuthorizedTTL": "5m0s",
      "cacheUnauthorizedTTL": "30s"
    }
  },
  "registryPullQPS": 5,
  "registryBurst": 10,
  "eventRecordQPS": 5,
  "eventBurst": 10,
  "enableDebuggingHandlers": true,
  "healthzPort": 10248,
  "healthzBindAddress": "127.0.0.1",
  "oomScoreAdj": -999,
  "clusterDomain": "cluster.local",
  "clusterDNS": [
    "10.96.0.10"
  ],
  "streamingConnectionIdleTimeout": "4h0m0s",
  "nodeStatusUpdateFrequency": "10s",
  "nodeStatusReportFrequency": "5m0s",
  "nodeLeaseDurationSeconds": 40,
  "imageMinimumGCAge": "2m0s",
  "imageGCHighThresholdPercent": 85,
  "imageGCLowThresholdPercent": 80,
  "volumeStatsAggPeriod": "1m0s",
  "cgroupsPerQOS": true,
  "cgroupDriver": "cgroupfs",
  "cpuManagerPolicy": "none",
  "cpuManagerReconcilePeriod": "10s",
  "topologyManagerPolicy": "none",
  "topologyManagerScope": "container",
  "runtimeRequestTimeout": "2m0s",
  "hairpinMode": "promiscuous-bridge",
  "maxPods": 110,
  "podPidsLimit": -1,
  "resolvConf": "/run/systemd/resolve/resolv.conf",
  "cpuCFSQuota": true,
  "cpuCFSQuotaPeriod": "100ms",
  "nodeStatusMaxImages": 50,
  "maxOpenFiles": 1000000,
  "contentType": "application/vnd.kubernetes.protobuf",
  "kubeAPIQPS": 5,
  "kubeAPIBurst": 10,
  "serializeImagePulls": true,
  "evictionHard": {
    "imagefs.available": "15%",
    "memory.available": "100Mi",
    "nodefs.available": "10%",
    "nodefs.inodesFree": "5%"
  },
  "evictionPressureTransitionPeriod": "5m0s",
  "enableControllerAttachDetach": true,
  "makeIPTablesUtilChains": true,
  "iptablesMasqueradeBit": 14,
  "iptablesDropBit": 15,
  "failSwapOn": true,
  "containerLogMaxSize": "10Mi",
  "containerLogMaxFiles": 5,
  "configMapAndSecretChangeDetectionStrategy": "Watch",
  "enforceNodeAllocatable": [
    "pods"
  ],
  "volumePluginDir": "/usr/libexec/kubernetes/kubelet-plugins/volume/exec/",
  "logging": {
    "format": "text"
  },
  "enableSystemLogHandler": true,
  "shutdownGracePeriod": "0s",
  "shutdownGracePeriodCriticalPods": "0s",
  "kind": "KubeletConfiguration",
  "apiVersion": "kubelet.config.k8s.io/v1beta1"
}
```

↑の設定ファイルから ConfigMap を作成する

```sh
$ kubectl -n kube-system create configmap my-node-config --from-file=kubelet=kubelet_configz_worker
configmap/my-node-config created

$ kubectl -n kube-system get cm my-node-config -o yaml
apiVersion: v1
data:
  kubelet: |
    {
      "enableServer": true,
      "staticPodPath": "/etc/kubernetes/manifests",
      "syncFrequency": "1m0s",
      ...
    }
kind: ConfigMap
metadata:
  creationTimestamp: "2020-12-10T16:48:20Z"
  managedFields:
  - apiVersion: v1
    fieldsType: FieldsV1
    fieldsV1:
      f:data:
        .: {}
        f:kubelet: {}
    manager: kubectl-create
    operation: Update
    time: "2020-12-10T16:48:20Z"
  name: my-node-config
  namespace: kube-system
  resourceVersion: "12622"
  uid: 23771797-73b5-49b7-84f9-15135b03ba5a
```

作成した ConfigMap を参照するように Node のマニフェストの `spec` 配下に `kubectl edit node worker` で↓を追加します。

```yaml
configSource:
  configMap:
    name: my-node-config
    namespace: kube-system
    kubeletConfigKey: kubelet
```

`journalctl -u kubelet` で Kubelet のログを見ると Node のマニフェストの変更を終了したタイミングで `Node.Spec.ConfigSource` が追加されたことが検知されて Kubelet が再起動されたことがわかる。

```
Dec 10 16:55:02 worker kubelet[28593]: I1210 16:55:02.911350   28593 watch.go:85] kubelet config controller: Node.Spec.ConfigSource was updated
Dec 10 16:55:03 worker kubelet[28593]: I1210 16:55:03.298865   28593 configsync.go:96] kubelet config controller: Node.Spec.ConfigSource is non-empty, will checkpoint source and update config if necessary
Dec 10 16:55:03 worker kubelet[28593]: I1210 16:55:03.299263   28593 download.go:194] kubelet config controller: attempting to download /api/v1/namespaces/kube-system/configmaps/my-node-config
Dec 10 16:55:03 worker kubelet[28593]: I1210 16:55:03.312233   28593 download.go:199] kubelet config controller: successfully downloaded /api/v1/namespaces/kube-system/configmaps/my-node-config, UID: 23771797-73b5-49b7-84f9-15135b03ba5a, ResourceVersion: 12622
Dec 10 16:55:03 worker kubelet[28593]: I1210 16:55:03.413807   28593 configsync.go:205] kubelet config controller: Kubelet restarting to use /api/v1/namespaces/kube-system/configmaps/my-node-config, UID: 23771797-73b5-49b7-84f9-15135b03ba5a, ResourceVersion: 12622, KubeletConfigKey: kubelet
Dec 10 16:55:13 worker systemd[1]: kubelet.service: Service hold-off time over, scheduling restart.
Dec 10 16:55:13 worker systemd[1]: kubelet.service: Scheduled restart job, restart counter is at 1.
Dec 10 16:55:13 worker systemd[1]: Stopped kubelet: The Kubernetes Node Agent.
Dec 10 16:55:13 worker systemd[1]: Started kubelet: The Kubernetes Node Agent.
...
```

Node が my-node-config の ConfigMap を参照してるかを確認するには `kubectl get node worker -o yaml` で status の config を見ればわかる。

```yaml
config:
  active:
    configMap:
      kubeletConfigKey: kubelet
      name: my-node-config
      namespace: kube-system
      resourceVersion: "12622"
      uid: 23771797-73b5-49b7-84f9-15135b03ba5a
    assigned:
      configMap:
      kubeletConfigKey: kubelet
      name: my-node-config
      namespace: kube-system
      resourceVersion: "12622"
      uid: 23771797-73b5-49b7-84f9-15135b03ba5a
    lastKnownGood:
      configMap:
        kubeletConfigKey: kubelet
        name: my-node-config
        namespace: kube-system
        resourceVersion: "12622"
        uid: 23771797-73b5-49b7-84f9-15135b03ba5a
```

これで稼働中の Node が ConfigMap を参照するようになったので、次は ConfigMap を変更して Node の設定を変更してみる。

試しに registryPullQPS を変更してみる。

```sh
$ kubectl -n kube-system get cm my-node-config -o yaml | grep registryPullQPS
      "registryPullQPS": 10,
```

Kubelet のログを確認すると ConfigMap を追加したときと同じく変更を検知して Kubelet が再起動していることがわかる。

```
Dec 10 17:17:35 worker kubelet[20353]: I1210 17:17:35.715928   20353 watch.go:128] kubelet config controller: assigned ConfigMap was updated
Dec 10 17:17:36 worker kubelet[20353]: I1210 17:17:36.324327   20353 configsync.go:96] kubelet config controller: Node.Spec.ConfigSource is non-empty, will checkpoint source and update config if necessary
Dec 10 17:17:36 worker kubelet[20353]: I1210 17:17:36.324384   20353 download.go:181] kubelet config controller: checking in-memory store for /api/v1/namespaces/kube-system/configmaps/my-node-config
Dec 10 17:17:36 worker kubelet[20353]: I1210 17:17:36.324407   20353 download.go:187] kubelet config controller: found /api/v1/namespaces/kube-system/configmaps/my-node-config in in-memory store, UID: 23771797-73b5-49b7-84f9-15135b03ba5a, ResourceVersion: 14953
Dec 10 17:17:36 worker kubelet[20353]: I1210 17:17:36.366558   20353 configsync.go:205] kubelet config controller: Kubelet restarting to use /api/v1/namespaces/kube-system/configmaps/my-node-config, UID: 23771797-73b5-49b7-84f9-15135b03ba5a, ResourceVersion: 14953, KubeletConfigKey: kubelet
Dec 10 17:17:46 worker systemd[1]: kubelet.service: Service hold-off time over, scheduling restart.
Dec 10 17:17:46 worker systemd[1]: kubelet.service: Scheduled restart job, restart counter is at 2.
Dec 10 17:17:46 worker systemd[1]: Stopped kubelet: The Kubernetes Node Agent.
Dec 10 17:17:46 worker systemd[1]: Started kubelet: The Kubernetes Node Agent.
```

Kubelet の設定を確認すると registryPullQPS が正常に変更されてることが確認できました。

```sh
$ curl -sSL "http://localhost:8001/api/v1/nodes/worker/proxy/configz" | jq '.kubeletconfig|.kind="KubeletConfiguration"|.apiVersion="kubelet.config.k8s.io/v1beta1"' | grep registryPullQPS
  "registryPullQPS": 10,
```

# 参考
- [Dynamic Kubelet Configuration](https://kubernetes.io/blog/2018/07/11/dynamic-kubelet-configuration/)
- [Reconfigure a Node's Kubelet in a Live Cluster](https://kubernetes.io/docs/tasks/administer-cluster/reconfigure-kubelet/)
- [Set Kubelet parameters via a config file](https://kubernetes.io/docs/tasks/administer-cluster/kubelet-config-file/)
- [Configuring each kubelet in your cluster using kubeadm](https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/kubelet-integration/)
- [Dynamic Kubelet Configuration (Design proposal)](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/dynamic-kubelet-configuration.md)