Initializersを使ってK8s + NVIDIA device pluginでの意図しないGPUの割当を防止してみる

# 背景
* Kubernetes + NVIDIA device plugin for Kubernetes + nvidia-dockerの環境でGPUをリクエストせずにコンテナを建てると、すべてのGPUがアタッチされてしまう（例えば悪意のあるユーザがQuota等を無視してコンテナが立ったノードにあるGPUをすべて使えてしまう）という問題がある。 <br>
https://github.com/NVIDIA/k8s-device-plugin#running-gpu-jobs

## 指定した場合（正常動作）
```yaml:gpu-request.yaml
apiVersion: v1
kind: Pod
metadata:
  name: gpu-pod
spec:
  restartPolicy: OnFailure
  containers:
    - name: cuda-container
      image: nvidia/cuda:9.0-devel
      command: ["nvidia-smi"]
      resources:
        limits:
          nvidia.com/gpu: 2 # requesting 2 GPUs
```
```bash

$ kubectl apply -f gpu-request.yaml
$ kubectl logs gpu-pod
Sat Dec  1 08:39:42 2018
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 410.72       Driver Version: 410.72       CUDA Version: 10.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla P100-SXM2...  On   | 00000000:0B:00.0 Off |                    0 |
| N/A   35C    P0    31W / 300W |      0MiB / 16280MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   1  Tesla P100-SXM2...  On   | 00000000:85:00.0 Off |                    0 |
| N/A   38C    P0    36W / 300W |      0MiB / 16280MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+
```
## 指定しない場合（問題になるとき）
```yaml:gpu-norequest.yaml
apiVersion: v1
kind: Pod
metadata:
  name: gpu-pod-norequest
spec:
  restartPolicy: OnFailure
  containers:
    - name: cuda-container
      image: nvidia/cuda:9.0-devel
      command: ["nvidia-smi"]
```
```bash

$ kubectl apply -f gpu-norequest.yaml
$ kubectl logs gpu-pod-norequest
Sat Dec  1 08:46:25 2018
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 410.72       Driver Version: 410.72       CUDA Version: 10.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla P100-SXM2...  On   | 00000000:06:00.0 Off |                    0 |
| N/A   35C    P0    35W / 300W |      0MiB / 16280MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   1  Tesla P100-SXM2...  On   | 00000000:07:00.0 Off |                    0 |
| N/A   35C    P0    34W / 300W |      0MiB / 16280MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   2  Tesla P100-SXM2...  On   | 00000000:0A:00.0 Off |                    0 |
| N/A   35C    P0    32W / 300W |      0MiB / 16280MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   3  Tesla P100-SXM2...  On   | 00000000:0B:00.0 Off |                    0 |
| N/A   35C    P0    31W / 300W |      0MiB / 16280MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   4  Tesla P100-SXM2...  On   | 00000000:85:00.0 Off |                    0 |
| N/A   38C    P0    36W / 300W |      0MiB / 16280MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   5  Tesla P100-SXM2...  On   | 00000000:86:00.0 Off |                    0 |
| N/A   37C    P0    33W / 300W |      0MiB / 16280MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   6  Tesla P100-SXM2...  On   | 00000000:89:00.0 Off |                    0 |
| N/A   38C    P0    33W / 300W |      0MiB / 16280MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   7  Tesla P100-SXM2...  On   | 00000000:8A:00.0 Off |                    0 |
| N/A   37C    P0    32W / 300W |      0MiB / 16280MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+
```
# 解決例
* KDDI: TaintsをNodeに付加し、GPUをリクエストしないPodがGPU搭載ノード上に作成されないようにする <br>
http://openstackdays.com/wp-content/uploads/e_2_6.pdf (p.41)
* PFN: Dynamic Admission Controlを利用してNVIDIA_VISIBLE_DEVICESを強制的にPodに注入する (おそらくMutating Admission Webhook) <br>
https://www.slideshare.net/pfi/tech-inmlcluster20180729-julytechfesta2018 (p.42)

# 今回の方針
基本的にはPFN ~~のパクリ~~ を参考に

* nvidia.com/gpuのresourcesが書いてないPod
* nvidia.com/gpu:0のPod
    * これもDevice Pluginがフックできないため全GPU見える

に対して強制的に環境変数NVIDIA_VISIBLE_DEVICES=noneを注入する。

相違点として、今回はDynamic Admission ControlにAdmission Webhooks ではなく、[Initializers](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#initializers)を使う。

#Initializerとは
> Initializers are useful for admins to force policies (e.g., the AlwaysPullImages admission controller), or to inject defaults (e.g., the DefaultStorageClass admission controller), etc.

[Dynamic Admission Control](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/)の一種で、ユーザのリソースのリクエストと実際に立ち上がるまでの間に介入してリクエストを書き換えたりすることができる。アドミンがポリシーを強制したいときなどに使う。
# Initializerの動き方
Initializerの実装には大きく分けて

* InitializerConfiguration
* Initializer Controller

の２つが必要になる。
## InitializerConfiguration
InitializerConfigurationは単純にK8sのリソース。
以下のようなマッチ条件を書いたInitializerConfigurationを作ると、マッチしたリソースのmetadata.initializers[]に自動で指定したinitializerが登録される。

```yaml
apiVersion: admissionregistration.k8s.io/v1alpha1
kind: InitializerConfiguration
metadata:
  name: gpu
initializers:
  - name: gpu.initializer.kubernetes.io
    rules:
      - apiGroups:
          - "*"
        apiVersions:
          - "v1"
        resources:
          - pods
```
今回だと、すべてのpodsのmetadata.initializers[]にgpu.initializer.kubernetes.ioが追加される。
metadata.initializers[]が存在するリソースはInitializerが処理を行うまでPendingされる。

## Initializer Controller
Initializer Controllerは自分で実装する必要がある。
どうやって実装してもいいが、基本的にはClusterAdmin権限のServiceAccountを使ってすべての対象リソース（今回の場合はPod）を監視して

* リソースの内容を書き換え
    * （NVIDIA_VISIBLE_DEVICES=noneを注入）
* metadata.initializers[]から自分を削除
    * （gpu.initializer.kubernetes.ioを削除）

して、applyするようなプログラムを常駐させる。

## 全体の流れ
![initializers.png](https://qiita-image-store.s3.amazonaws.com/0/91672/61f55c3f-bbfd-e26e-9476-85e372c97899.png)
ちなみにInitializerConfigurationは直接リソースを作った場合だけでなく、ReplicaSetやDaemonSetなどから関節的にPodが作られた場合などでも適用される。
※実際にinitializersのmetadataを入れるのはInitializerConfigurationを読み取ったkube-apiserver

# 動かしてみる
## Initializersの有効化
Initializersはalpha機能なので、デフォルトではオンになっていない。
[Enable initializers alpha feature](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#enable-initializers-alpha-feature)を参考に有効にする。
GKEの場合にはアルファ機能を有効にしたクラスタを作成する。

## デプロイ
```bash
$ kubectl apply -f https://raw.githubusercontent.com/takmatsu/gpu-initializer/master/manifests/rolebindings.yaml
$ kubectl apply -f https://raw.githubusercontent.com/takmatsu/gpu-initializer/master/manifests/configmaps.yaml
$ kubectl apply -f https://raw.githubusercontent.com/takmatsu/gpu-initializer/master/manifests/deployment.yaml
$ kubectl apply -f https://raw.githubusercontent.com/takmatsu/gpu-initializer/master/manifests/initializer-configuration.yaml
```
## 確認
```
$ kubectl apply -f gpu-norequest.yaml
```
```
$ kubectl describe pod gpu-pod-norequest
~
Containers:
  cuda-container:
    Container ID:  docker://b4db40e7c6d23d0e8a982fcfd8dee590c3fa517208a4e340a3cfc2880c859e7b
    Image:         nvidia/cuda:9.0-devel
    Image ID:      docker-pullable://nvidia/cuda@sha256:3cdf1b5becfde8772e15dab594bc76de1cbbefd6c0f8533748854ab47e109ad1
    Port:          <none>
    Host Port:     <none>
    Command:
      nvidia-smi
    State:          Terminated
      Reason:       Error
      Exit Code:    6
      Started:      Sat, 01 Dec 2018 18:20:25 +0900
      Finished:     Sat, 01 Dec 2018 18:20:25 +0900
    Last State:     Terminated
      Reason:       Error
      Exit Code:    6
      Started:      Sat, 01 Dec 2018 18:19:55 +0900
      Finished:     Sat, 01 Dec 2018 18:19:55 +0900
    Ready:          False
    Restart Count:  3
    Environment:
      NVIDIA_VISIBLE_DEVICES:  none
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-8z4dk (ro)
~
```
```bash

$ kubectl logs gpu-pod-norequest
No devices were found
```
自動で```NVIDIA_VISIBLE_DEVICES:  none```が入っている🎉

# しかし・・・
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">k8s 1.12 で主に取り組まれる事項。initializersが削除されるのまじか。istio sidecar-injector も MutatingAdmissionWebhook に移行したし、一向に beta にならないからそんな予感はしていたけど。 <a href="https://t.co/4eSnheehmu">https://t.co/4eSnheehmu</a> <a href="https://t.co/uWizXLllhf">pic.twitter.com/uWizXLllhf</a></p>&mdash; Kazuki Suda / すぱぶら (@superbrothers) <a href="https://twitter.com/superbrothers/status/1017950646819946496?ref_src=twsrc%5Etfw">2018年7月14日</a></blockquote>
<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">It is clear that Initializers are on their way out and are unlikely to make it to beta.</p>&mdash; Joe Beda (@jbeda) <a href="https://twitter.com/jbeda/status/1020330953980325891?ref_src=twsrc%5Etfw">2018年7月20日</a></blockquote>
おいおいおい〜〜〜〜〜〜！！！

# まとめ
* Mutating Admission Webhook(1.9で既にbetaでデフォルト有効)を使おう！
* alpha機能を使うときはよく考えよう！
* コミュニティの動向を追うのは大事！

# Appendix
* https://github.com/kelseyhightower/kubernetes-initializer-tutorial
* https://github.com/takmatsu/gpu-initializer
* https://github.com/takmatsu/gpu-mutating-webhook (Mutating Admission Webhook版のサンプル)