Kubernetesの主要コンポーネントのログレベルを動的に変更する

# TL; DR

Kubernetesの下記の主要コンポーネントは各コンポーネントのendpointの`/debug/flags/v`に`PUT`することで、ログレベルを動的に更新できます。つまり、稼働中のKubernetesのコンポーネントのログレベルが再起動/Manifestの変更なしで動的にログレベルが変更できます！_ただし、各コンポーネントともにprofilingもしくはdebugging-handlersを有効にしている必要があります。_

```shell
# loglevelを N に変更 ※別途認証は必要です。下記参照。
$ curl -XPUT -d 'N' <endpoint>/debug/flags/v 
```

- kube-apiserver
- kube-scheduler
- kubelet
- kube-proxy
- _kube-controller-manager (v1.23で対応予定。[#104571](https://github.com/kubernetes/kubernetes/pull/104571))_ 

# 各コンポーネントの対応状況 (v1.22時点)

|コンポーネント|デフォルトポート|認証認可|`/debug/flags/v`を有効にするために必要な設定|
|---|---|---|---|---|
|[kube-apiserver](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/)|6443|あり|[`--profiling (Default:true)`](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/)|
|[kube-scheduler](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-scheduler/)|10259|あり|[`enableProfiling: true (Default: true)`](https://kubernetes.io/docs/reference/config-api/kube-scheduler-config.v1beta2/#DebuggingConfiguration)|
|[kubelet](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/)| 10250 |あり|[`enableDebuggingHandlers: true (Default: true)`](https://kubernetes.io/docs/reference/config-api/kubelet-config.v1beta1/)|
|[kube-proxy](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-proxy/)|10249|なし|[`enableProfiling: true (Default: false)`](https://kubernetes.io/docs/reference/config-api/kube-proxy-config.v1alpha1/)<br/> _default無効なので有効にしてデプロイしておく必要あり_|
|[kube-controller-manager](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/)<br/>_v1.23で対応予定<br/>(see [#104571](https://github.com/kubernetes/kubernetes/pull/104571))_|10257|あり|[`--profiling　(Default: true)`](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-controller-manager/)|

# 使い所

- プロダクション環境での解析等で稼働中(再起動したくない)のプロセスのログレベルを上げたい
- 開発や検証時にログレベルを変えながらログ出力の挙動をチェックしたいけど、ログレベルのオプション変更してre-applyめんどくさい
- etc.

# やってみる

## 準備

[kind](https://kind.sigs.k8s.io/)でやります。 `v1.22.0`でclusterを作成します。

```shell-session
$ kind create cluster --image kindest/node:v1.22.0
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.22.0) 🖼
...
Thanks for using kind! 😊
```

## 必要な権限

認証認可ありのエンドポイントにアクセスするためには権限の付与が必要です。

基本的には `nonResourceURLs` を使って `put /debug/flags/v` の権限を与えればよいのですが、kubeletだけなぜか[`resource=nodes/proxy, verb=update`](https://github.com/kubernetes/kubernetes/blob/v1.22.0/pkg/kubelet/server/auth.go#L91)の権限チェックになってしまうのでそれも与えておきます。

ここでは便宜的に `default/default` Service Accountに権限を与えています。

```yaml
$ cat << EOT | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: edit-debug-flags-v
rules:
  # kubeletだけこの権限が必要
- apiGroups:
  - ""
  resources:
  - nodes/proxy
  verbs:
  - update
  # 他のcomponentはこれでOK
- nonResourceURLs:
  - /debug/flags/v
  verbs:
  - put
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: edit-debug-flags-v
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: edit-debug-flags-v
subjects:
- kind: ServiceAccount
  name: default
  namespace: default
EOT
clusterrole.rbac.authorization.k8s.io/edit-debug-flags-v created
clusterrolebinding.rbac.authorization.k8s.io/edit-debug-flags-v created
```

`default/default` Service Account権限でアクセスするためにTOKENを取得しておきます。

```shell-session
$ TOKEN=$(kubectl get secrets -o jsonpath="{.items[?(@.metadata.annotations['kubernetes\.io/service-account\.name']=='default')].data.token}"|base64 --decode)
```

## kube-apiserver

さて準備完了です。まずはkube-apiserverから試してみましょう。apiserverは普通にクラスタのエンドポイントにアクセスすれば良いですね。

```shell-session
$ APISERVER=$(kubectl config view -o jsonpath="{.clusters[?(@.name==\"kind-kind\")].cluster.server}")
$ curl -s -X PUT -d '5' $APISERVER/debug/flags/v --header "Authorization: Bearer $TOKEN"  -k
successfully set klog.logging.verbosity to 5
```

## kube-scheduler

schedulerはpodとして動いているのでport-forwardして試します。

```shell-session
$ kubectl -n kube-system port-forward kube-scheduler-kind-control-plane 10259:10259
Forwarding from 127.0.0.1:10259 -> 10259
Forwarding from [::1]:10259 -> 10259

$ curl -s -X PUT -d '5' https://localhost:10259/debug/flags/v --header "Authorization: Bearer $TOKEN"  -k
successfully set klog.logging.verbosity to 5
```

## kubelet

kubeletはpodで動いていないので、直接nodeのdocker containerに入ってアクセスしてみます。

```shell-session
$ docker exec kind-control-plane \
  curl -s -X PUT -d '5' https://localhost:10250/debug/flags/v --header "Authorization: Bearer $TOKEN"  -k
successfully set klog.logging.verbosity to 5
```

## kube-proxy

kube-proxyはdefaultではprofilingが有効ではないので、有効にしておく必要があります。

```shell-session
$ kubectl -n kube-system get configmap kube-proxy -o yaml  | \
  sed -e 's/enableProfiling: false/enableProfiling: true/' | kubectl apply -f -
configmap/kube-proxy configured
$ kubectl -n kube-system rollout restart daemonset/kube-proxy
daemonset.apps/kube-proxy restarted
```

schedulerと同様port-forwardして試します。

```shell-session
$ kubectl port-forward -n kube-system $(k get pod -n kube-system -l k8s-app=kube-proxy -o jsonpath='{.items[0].metadata.name}') 10249:10249
Forwarding from 127.0.0.1:10249 -> 10249
Forwarding from [::1]:10249 -> 10249

# 認証なし
$ curl -s -XPUT -d '5' http://localhost:10249/debug/flags/v
successfully set klog.logging.verbosity to 5
```

# どう実装されているか？

ここではkube-apiserverの場合を見てみましょう。該当部分は[staging/src/k8s.io/apiserver/pkg/server/config.go](https://github.com/kubernetes/kubernetes/blob/v1.22.0/staging/src/k8s.io/apiserver/pkg/server/config.go#L808-L809)です。

```golang
import(
...
    "k8s.io/apiserver/pkg/server/routes"
    "k8s.io/component-base/logs"
...
    if c.EnableProfiling {
...
        routes.DebugFlags{}.Install(                       // "/debug/flags"というbasepathにhandlerをinstallするヘルパ
            s.Handler.NonGoRestfulMux, 
            "v",                                           // "/debug/flags/v" にinstall
            routes.StringFlagPutHandler(logs.GlogSetter),  // PUTのbodyをlogs.GlogSetterに渡すhandler
                                                           //   logs.GlogSetterはglogのloglevelを更新する関数
                                                           //   この関数は -v flagで指定する値を書き換えます
        )
    }
```

各コンポーネントの実装はこちらをご覧ください

https://github.com/kubernetes/kubernetes/search?q=%22routes.StringFlagPutHandler%22

# FAQ

## Structured Loggingの対応が進んでいるけど対応に違いはあるのか？

現在、Kubernetes upstreamでは[Structured Logging](https://github.com/kubernetes/enhancements/issues/1602)への対応が[絶賛進行中](https://github.com/orgs/kubernetes/projects/56)です。[KEP](https://github.com/kubernetes/enhancements/tree/master/keps/sig-instrumentation/1602-structured-logging#motivation)にかかれている通り、以前のklogからの移行容易性にも配慮された設計になっているため、以前のlogも、Structured Logも同じ`klog.V(n)`というインターフェースでロギングが行われるため、 `PUT /debug/flags/v` によるログレベルの動的変更は有効です。 `--logging-format=text,json` も影響は受けません(どちらでもOK)。

```golang
// Classic Logging (non-structured)
klog.V(n).Infof("msg: %d", i)
// Structured Logging
klog.V(n).InfoS("msg", "i", i)
````

## ログレベルっていくつにするとどういうログがでるのかイマイチよくわからない

Kubernetesのコンポーネントのログ規約は [kubernetes/community](https://github.com/kubernetes/community/blob/master/contributors/devel/sig-instrumentation/logging.md) repositoryにまとめられています。Kubernetesの主要コンポーネントはこの規約に沿って実装されていることが期待されます。

>
> - `klog.V(0)`: Generally useful for this to ALWAYS be visible to an operation
> - `klog.V(1)`: A reasonable default log level if you don't want verbosity
> - `klog.V(2)`: Useful steady state information about the service and important log messages that may correlate to significant changes in the system. This is the recommended default log level for most systems
> - `klog.V(3)`: Extended information about changes
> - `klog.V(4)`: Debug level verbosity
> - `klog.V(5)`: Trace level verbosity
> 
> As per the comments, the practical default level is `V(2)`. Developers and QE environments may wish to run at `V(3)` or `V(4)`

# 最後に

[lwkd.info](https://lwkd.info)経由で[#98306](https://github.com/kubernetes/kubernetes/pull/98306)のPRを見た時に初めて知ったのですがこんな機能があったんですね。驚きました。めちゃくちゃ便利だと思います。

`-v`が動的に変更できるとなると、自然と欲が出て`-vmodule`も変更したくなっちゃいますね。ただ、現時点で[#99270](https://github.com/kubernetes/kubernetes/issues/99270)にあるようにJson Loggingで `-vmodule` がサポートされていない(その他のオプションもまだ諸々サポートが足りていない模様)ため、もう少し先になるかもしれませんね。

debug機能とはいえ全くのundocumentedな機能っぽいのでもっと多くの人に周知されると良いと思います。どこかにdocumentされていたら是非教えて下さい🙇‍♂️

また、この手法自体とても有用なので、自分で開発するcontrollerとかでも参考にするととても便利になりそうですね！ 🎉