
背景
コンテナが運用に利用されるようになり、Kubernetesのようなオーケストレーションプラットフォームへの注目が集まっているなか、コンテナランタイムの脆弱性についても課題が出てきました。
特にDockerとKubernetesのスタンダードなランタイムであるruncの脆弱性が発見されたのは記憶に新しいと思います。
・runCによるDockerコンテナブレークアウト(CVE-2019-5736)-2019/2/11
そのため現在は、コンテナでのサービス提供にあたって、低レベルコンテナランタイムをrunc以外のものに変更することや、独自にカスタマイズすることが積極的に検討されています。
gVisorとは
gVisorは、Googleが開発する低レベルコンテナランタイムです。
ホストのカーネルから高度に分離されたユーザモードカーネル機構を提供するという特徴を持ちます。
できないこともたくさんありますが、セキュリティ的にはruncよりも有利であると言えます。
やろうとしたこと
Kubernetesクラスタの低レベルコンテナランタイムを[runc]から[runsc(gVisor)]に変更する。
やったこと
- WorkerノードをK8sクラスタから外す
- 外したノードのkubelet, kube-proxy, containerdサービスを停止する
- gVisorをインストールする ※参考 Installation/gVisor
- Containerdの利用する低レベルコンテナランタイムをruncからrunsc(gVisor)に変更する
- 外したノードのkubelet, kube-proxy, containerdサービスを起動する
1~3は一般的なので説明は省略します。
低レベルコンテナランタイムをruncからrunsc(gVisor)に変更する
/etc/containerd/config.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 = ""
↓
【変更後】
[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クラスタに再追加されます。
$ sudo systemctl start containerd kubelet kube-proxy
対象ノードにポッド作成します。
ノード指定のマニフェストnginx-2.ymlを適用します。
# nginx-2.yml
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
containers:
- name: nginx-2
image: nginx:latest
nodeSelector:
kubernetes.io/hostname: worker-2 #ノード名
$ kubectl apply -f nginx-2.yml
pod/nginx created
ポッドの起動を確認します。
$ kubectl get po nginx
NAME READY STATUS RESTARTS AGE
nginx 0/1 ContainerCreating 0 3m36s
ポッドが起動していません!
ログを見ると
# 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がダウンしてもコンテナは止まらないという、超重要なやつらしい!
containerd-shimは、[containerd]と[runc]の連携に対応したshimなので、[containerd]と[gVisor]の間ではやり取りができません。
これがポッドが起動しない原因でした!
shimも[gVisor]との連携に対応した[gvisor-containerd-shim]に替えてみます。
gvisor-containerd-shimを導入する
公式ページを参考にgvisor-containerd-shimをインストールします。
公式Git: gvisor-containerd-shim
筆者の環境はcontainerd 1.2.9 なので、[shim v1]をインストールしました。
gvisor-containerd-shimをインストールします。
$ 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の場所は環境によって違うので注意してください。
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を再度編集します。
[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を再起動します。
$ sudo systemctl restart containerd
ポッドの起動確認
先ほど失敗したnginx-2.ymlを適用して、gVisor上でポッドが起動するかテストします。
$ 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で起動していることを確認します。
$ k exec -it nginx -- dmesg | grep gVisor
[ 0.000000] Starting gVisor...
ポッドがgVisorで起動できるようになりました!
完成
おわりに
今回はノードのデフォルトランタイムを変更する際にハマった個所を紹介しました。
ノードにコンテナランタイムを複数入れて、K8sクラスタのruntime.classなどで指定する方法もあるようです。
是非いろいろチャレンジしてみてください!