KubernetesノードにgVisorを入れるときにハマった話

<figure><img src="https://github.com/cncf/artwork/raw/master/projects/kubernetes/horizontal/all-blue-color/kubernetes-horizontal-all-blue-color.png" alt=""/></figure>
## はじめに
仮想環境上に構築したKubernetesクラスタのWorkerノードの低レベルコンテナランタイムを[gVisor]に変更したときにつまずいたポイントを紹介します。
containerd-shimという伏兵に刺されました。
全手順はこちらで公開しています。
[オンプレKubernetes(K8s)のランタイムをgVisorに変えてみる](http://c.itdo.jp/?p=2646 "Title")

## 背景
<p>コンテナが運用に利用されるようになり、Kubernetesのようなオーケストレーションプラットフォームへの注目が集まっているなか、コンテナランタイムの脆弱性についても課題が出てきました。<br>特にDockerとKubernetesのスタンダードなランタイムであるruncの脆弱性が発見されたのは記憶に新しいと思います。<br>・<a href="https://nvd.nist.gov/vuln/detail/CVE-2019-5736" class="aioseop-link">runCによるDockerコンテナブレークアウト(CVE-2019-5736)-2019/2/11</a><br>そのため現在は、コンテナでのサービス提供にあたって、低レベルコンテナランタイムをrunc以外のものに変更することや、独自にカスタマイズすることが積極的に検討されています。</p>

## gVisorとは
![20190505233414.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627785/bbf36d23-54af-809e-4b0a-0747086c2ef9.png)
gVisorは、Googleが開発する低レベルコンテナランタイムです。
ホストのカーネルから高度に分離されたユーザモードカーネル機構を提供するという特徴を持ちます。
できないこともたくさんありますが、セキュリティ的にはruncよりも有利であると言えます。

## やろうとしたこと
Kubernetesクラスタの低レベルコンテナランタイムを[runc]から[runsc(gVisor)]に変更する。

当初の理解は以下の通りでした。※間違ってます。
![image5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627785/154a4550-5b99-8e9d-290e-9e9e48c3b816.png)


## やったこと

1. WorkerノードをK8sクラスタから外す
2. 外したノードのkubelet, kube-proxy, containerdサービスを停止する
3. gVisorをインストールする ※参考 [Installation/gVisor](https://gvisor.dev/docs/user_guide/install/ "Title")
4. Containerdの利用する低レベルコンテナランタイムをruncからrunsc(gVisor)に変更する  
5. 外したノードのkubelet, kube-proxy, containerdサービスを起動する

1~3は一般的なので説明は省略します。

## 低レベルコンテナランタイムをruncからrunsc(gVisor)に変更する

/etc/containerd/config.tomlを以下のように変更しました。

【変更前】

```toml

[plugins]
  [plugins.cri]
  # stream_server_address is the ip address streaming server is listening on.
  stream_server_address = "xxx.xxx.xxx.222" 
  [plugins.cri.containerd]
    snapshotter = "overlayfs"
    [plugins.cri.containerd.default_runtime]
      runtime_type = "io.containerd.runtime.v1.linux"
      runtime_engine = "/usr/local/bin/runc"   #←ここを書き変える
      runtime_root = ""
```
↓

【変更後】

```toml
[plugins]
  [plugins.cri]
  # stream_server_address is the ip address streaming server is listening on.
  stream_server_address = "xxx.xxx.xxx.222"
  [plugins.cri.containerd]
    snapshotter = "overlayfs"
    [plugins.cri.containerd.default_runtime]
      runtime_type = "io.containerd.runtime.v1.linux"
      runtime_engine = "/usr/local/bin/runsc"  #←ここを書き変えた 
      runtime_root = ""
```
　
外したworkerノードのサービスを起動します。kubeletを起動することで、自動的にK8sクラスタに再追加されます。

```bash

$ sudo systemctl start containerd kubelet kube-proxy
```
　
対象ノードにポッド作成します。
ノード指定のマニフェストnginx-2.ymlを適用します。

```yaml

# nginx-2.yml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  containers:
    - name: nginx-2
      image: nginx:latest
  nodeSelector:
    kubernetes.io/hostname: worker-2 #ノード名
```

```bash
$ kubectl apply -f nginx-2.yml
pod/nginx created
```
　
ポッドの起動を確認します。

```bash
$ kubectl get po nginx
NAME        READY   STATUS              RESTARTS   AGE
nginx   0/1     ContainerCreating   0          3m36s
```
### ポッドが起動していません！

ログを見ると

```bash

# journalctl |grep kubelet | tail -10
Apr 24 02:38:47 worker-2 kubelet[1685]: E0424 02:38:47.850141    1685 remote_runtime.go:243] StopContainer "e1334987c0f747afe983628265f0635802661fd398440373cef9f721c92980a3" from runtime service failed: rpc error: code = Unknown desc = failed to stop container "e1334987c0f747afe983628265f0635802661fd398440373cef9f721c92980a3": unknown error after kill: /usr/local/bin/runsc did not terminate sucessfully: sandbox is not running
Apr 24 02:38:47 worker-2 kubelet[1685]: : unknown
Apr 24 02:38:47 worker-2 kubelet[1685]: E0424 02:38:47.850200    1685 kuberuntime_container.go:585] Container "containerd://e1334987c0f747afe983628265f0635802661fd398440373cef9f721c92980a3" termination failed with gracePeriod 30: rpc error: code = Unknown desc = failed to stop container "e1334987c0f747afe983628265f0635802661fd398440373cef9f721c92980a3": unknown error after kill: /usr/local/bin/runsc did not terminate sucessfully: sandbox is not running
Apr 24 02:38:47 worker-2 kubelet[1685]: : unknown
Apr 24 02:38:47 worker-2 kubelet[1685]: E0424 02:38:47.899961    1685 remote_runtime.go:128] StopPodSandbox "60e4b9b989664e7e137565dae0e30a937b963b65913b1f2d46653c69c4d1aa63" from runtime service failed: rpc error: code = Unknown desc = failed to stop container "e1334987c0f747afe983628265f0635802661fd398440373cef9f721c92980a3": failed to kill container "e1334987c0f747afe983628265f0635802661fd398440373cef9f721c92980a3": unknown error after kill: /usr/local/bin/runsc did not terminate sucessfully: sandbox is not running
Apr 24 02:38:47 worker-2 kubelet[1685]: : unknown
Apr 24 02:38:47 worker-2 kubelet[1685]: E0424 02:38:47.900020    1685 kuberuntime_manager.go:845] Failed to stop sandbox {"containerd" "60e4b9b989664e7e137565dae0e30a937b963b65913b1f2d46653c69c4d1aa63"}
Apr 24 02:38:47 worker-2 kubelet[1685]: E0424 02:38:47.900108    1685 kubelet_pods.go:1093] Failed killing the pod "nginx-2": [failed to "KillContainer" for "nginx-2" with KillContainerError: "rpc error: code = Unknown desc = failed to stop container \"e1334987c0f747afe983628265f0635802661fd398440373cef9f721c92980a3\": unknown error after kill: /usr/local/bin/runsc did not terminate sucessfully: sandbox is not running\n: unknown"
Apr 24 02:38:47 worker-2 kubelet[1685]: , failed to "KillPodSandbox" for "ba8664ee-0b9e-48f8-a460-25d886a2f1dc" with KillPodSandboxError: "rpc error: code = Unknown desc = failed to stop container \"e1334987c0f747afe983628265f0635802661fd398440373cef9f721c92980a3\": failed to kill container \"e1334987c0f747afe983628265f0635802661fd398440373cef9f721c92980a3\": unknown error after kill: /usr/local/bin/runsc did not terminate sucessfully: sandbox is not running\n: unknown"
Apr 24 02:38:47 worker-2 kubelet[1685]: ]
#
```
何やらSandBoxやContainerがうまく操作できていないようです。


## 原因はcontainerd-shimだった！
実は高レベルコンテナランタイムと低レベルコンテナランタイムの間でcontainerd-shimというのが動いているらしい。

[役割]
・高レベルコンテナランタイム(containerd)： コンテナイメージ、ネットワーク、ストレージを管理するデーモン
・★shim(containerd-shim)：低レベルコンテナランタイムが起動したコンテナの管理
・低レベルコンテナランタイム(runc)：コンテナを作成、起動、削除を行うバイナリ

こいつがいるからcontainerdがダウンしてもコンテナは止まらないという、超重要なやつらしい！

![image6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627785/d6dddd3f-19a4-25cc-8bef-ce4227b418a0.png)
containerd-shimは、[containerd]と[runc]の連携に対応したshimなので、[containerd]と[gVisor]の間ではやり取りができません。
##### これがポッドが起動しない原因でした！
shimも[gVisor]との連携に対応した[gvisor-containerd-shim]に替えてみます。

## gvisor-containerd-shimを導入する
公式ページを参考にgvisor-containerd-shimをインストールします。
公式Git: [gvisor-containerd-shim](https://github.com/google/gvisor-containerd-shim "Title")

筆者の環境はcontainerd 1.2.9 なので、[shim v1]をインストールしました。

gvisor-containerd-shimをインストールします。

```bash
$ LATEST_RELEASE=$(wget -qO - https://api.github.com/repos/google/gvisor-containerd-shim/releases | grep -oP '(?<="browser_download_url": ")https://[^"]*gvisor-containerd-shim.linux-amd64' | head -1)
$ wget -O gvisor-containerd-shim ${LATEST_RELEASE}
$ chmod +x gvisor-containerd-shim
$ sudo mv gvisor-containerd-shim /usr/local/bin/gvisor-containerd-shim
```
　
 gvisor-containerd-shimの設定ファイルを作成します。containerd-shimの場所は環境によって違うので注意してください。

```bash
cat <<EOF | sudo tee /etc/containerd/gvisor-containerd-shim.toml
# This is the path to the default runc containerd-shim.
runc_shim = "/bin/containerd-shim"
EOF
```
　
/etc/containerd/config.tomlを再度編集します。

```toml

[plugins]
  [plugins.cri]
  # stream_server_address is the ip address streaming server is listening on.
  stream_server_address = "xxx.xxx.xxx.222"
  [plugins.linux]
    shim = "/usr/local/bin/gvisor-containerd-shim"   ←この行を追加します
    shim_debug = true                                ←
  [plugins.cri.containerd]
    snapshotter = "overlayfs"
    [plugins.cri.containerd.default_runtime]
      runtime_type = "io.containerd.runtime.v1.linux"
      runtime_engine = "/usr/local/bin/runsc"
      runtime_root = "/run/containerd/runsc"         ←ココを追加します
```

containerdを再起動します。

```bash

$ sudo systemctl restart containerd
```

## ポッドの起動確認
先ほど失敗したnginx-2.ymlを適用して、gVisor上でポッドが起動するかテストします。

```bash

$ kubectl apply -f nginx-2.yaml
pod/nginx created
$ kubectl get po nginx
NAME    READY   STATUS    RESTARTS   AGE
nginx   1/1     Running   0          5s
```
　
ポッドがgVisorで起動していることを確認します。

```bash

$ k exec -it nginx -- dmesg | grep gVisor
[    0.000000] Starting gVisor...
```

##### ポッドがgVisorで起動できるようになりました！

## 完成
最終的にはこんな感じになりました。
![image7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/627785/d11bfff9-63f5-f10f-e49b-5658168f2881.png)

## おわりに
今回はノードのデフォルトランタイムを変更する際にハマった個所を紹介しました。
ノードにコンテナランタイムを複数入れて、K8sクラスタのruntime.classなどで指定する方法もあるようです。
是非いろいろチャレンジしてみてください！