はじめに
今回は、仮想環境上に構築したKubernetesクラスタのWorkerノードの低レベルコンテナランタイムを[gVisor]に変更してみました。
Kubernetesクラスタの構築についてはこちらで紹介しています。
「Kubernetes(K8s)を仮想環境でHard Wayに構築してみた」
背景
コンテナが運用に利用されるようになり、Kubernetesのようなオーケストレーションプラットフォームへの注目が集まっているなか、コンテナランタイムの脆弱性についても課題が出てきました。
特にDockerとKubernetesのスタンダードなランタイムであるruncの脆弱性が発見されたのは記憶に新しいと思います。
「runCによるDockerコンテナブレークアウト(CVE-2019-5736)-2019/2/11」
ホストとカーネルを共有するコンテナ技術にセキュリティの課題は切っても切り離せないテーマとなっています。
そのため現在は、コンテナでのサービス提供にあたって、低レベルコンテナランタイムをrunc以外のものに変更することや、独自にカスタマイズすることが積極的に検討されています。
gVisorとは
gVisorは、Googleが開発する低レベルコンテナランタイムです。
ホストのカーネルから高度に分離されたユーザモードカーネル機構を提供するという特徴を持ちます。
できないこともたくさんありますが、セキュリティ的にはruncよりも有利であると言えます。
詳しくは記事「このアプリ、gVisor上でも動きますか?」にて紹介しています。
Kubernetesクラスタ環境
環境は以下の通りです。
- Kubernetesクラスタ基盤 仮想マシン
- Kubernetes 1.15.3
- 高レベルコンテナランタイム containerd v1.2.9
- 低レベルコンテナランタイム runc 1.0.0-rc8
やってみる
作業はいたってシンプルです。
以下の5ステップからなっています。
- ノードをK8sクラスタから外す
- ノードにgVisorをインストールする
- ノードにgvisor-containerd-shimをインストールする
- crictlでcontainerdとgVisorの正常性を確認する
- ノードをK8sクラスタに再度参加させる
今回はworker-2のランタイムをgVisorに変更してみます。
1.ノードをKubernetesクラスタから外す
まずランタイムを入れ替えるノード、worker-2をクラスタから削除します。
クラスタ上のnodeオブジェクト[worker-2]を削除することで、クラスタから削除されます。
(1)worker-2へのスケジューリングを停止します。
# kubectl cordon worker-2
node"worker-2" cordoned
(2)動作してるポッドを退避します。
# kubectl drain worker-2 --ignore-daemonsets
node/worker-2 already cordoned
evicting pod "coredns-5fb99965-xcx7t"
pod/coredns-5fb99965-xcx7t evicted
node/worker-2 evicted
(3)worker-2ノードオブジェクトを削除します。
# kubectl delete node worker-2
node "worker-2" deleted
# kubectl get nodes
NAME STATUS ROLES AGE VERSION
worker-0 Ready <none> 13d v1.15.3
worker-1 Ready <none> 13d v1.15.3
2.ノードにgVisorをインストールする
worker-2上で作業します。
(1)kubelet, kube-proxy, containerdを停止します。
# systemctl stop kubelet kube-proxy containerd
(2)公式ページの手順に従って最新バージョンのgVisorをインストールします。
(公式ページ)
インストール| gVisor
# set -e
# URL=https://storage.googleapis.com/gvisor/releases/release/latest
# wget ${URL}/runsc
# wget ${URL}/runsc.sha512
# sha512sum -c runsc.sha512
# rm -f runsc.sha512
# chmod +x runsc
# mv runsc /usr/local/bin
3.ノードにgvisor-containerd-shimをインストールする
containerdのランタイム指定設定をgVisor(runsc)に変更しただけでは、gVisorは正常に機能しません。
実は低レベルコンテナランタイムと高レベルコンテナランタイムの間を取り持つ「shim」というものが、コンテナの管理に重要な役割を持っていたんです。
というわけで、containerdからruncを取り持つcontainerd-shimではgVisorは動作せず、gVisor(runsc)用のgvisor-containerd-shimをインストールする必要があります。
(gvisor-containerd-shim公式GitHub)
(1) 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
# mv gvisor-containerd-shim /usr/local/bin/
(2)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
※containerd-shimの場所は環境によって違うので注意が必要です。
(3)containerdの設定ファイルを編集します。
/etc/containerd/config.toml
[plugins]
[plugins.cri]
# stream_server_address is the ip address streaming server is listening on.
stream_server_address = "172.16.20.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 = "/usr/local/bin/runsc"
(4)containerdを起動します。
# systemctl start containerd
4.crictlでcontainerdとgVisorの正常性を確認する
containerdとgVisorの正常性を確認するためにcrictlというバイナリを利用します。
(1)crictlをインストールします。
# wget https://github.com/kubernetes-sigs/cri-tools/releases/download/v1.13.0/crictl-v1.13.0-linux-amd64.tar.gz
# tar xf crictl-v1.13.0-linux-amd64.tar.gz
# mv crictl /usr/local/bin
(2)crictlからcontainerdを利用するための設定を行います。
# cat <<EOF | tee /etc/crictl.yaml
runtime-endpoint: unix:///run/containerd/containerd.sock
EOF
(3)nginxイメージがpullできることを確認します。
# crictl pull nginx
Image is up to date for sha256:602e111c06b6934013578ad80554a074049c59441d9bcd963cb4a7feccede7a5
(4)SandBOXコンテナが作成できることを確認します。
# cat <<EOF | tee sandbox.json
{
"metadata": {
"name": "nginx-sandbox",
"namespace": "default",
"attempt": 1,
"uid": "hdishd83djaidwnduwk28bcsb"
},
"linux": {
},
"log_directory": "/tmp"
}
EOF
# SANDBOX_ID=$(crictl runp sandbox.json)
# echo $SANDBOX_ID
df330c804762142a4119183dc945ad706268264cfa788f78a768ca8e5bcf8495
(5)nginxコンテナをgVisorで起動できることを確認します。
# cat <<EOF > container.json
{
"metadata": {
"name": "nginx"
},
"image":{
"image": "nginx"
},
"log_path":"nginx.0.log",
"linux": {
}
}
EOF
# CONTAINER_ID=$(crictl create ${SANDBOX_ID} container.json sandbox.json)
# echo $CONTAINER_ID
468c715572ba37893f3d08594483aee87ee89b20b9a55ddf6c43b72587b4244a
# crictl start $CONTAINER_ID
468c715572ba37893f3d08594483aee87ee89b20b9a55ddf6c43b72587b4244a
# crictl exec ${CONTAINER_ID} dmesg | grep -i gvisor
[ 0.000000] Starting gVisor...
これでcontainerdからgVisorに正常に接続できていることが確認できました。
5.ノードをKubernetesクラスタに再度参加させる
ノードの準備が出来たのでKubernetesクラスタに再度参加させます。
クラスタにノードの情報を通知して、ノードオブジェクトを再度作成する必要があります。
通常、ノードのkubeletサービスを起動すると、自動的にkube-apiserverへノードの情報とノードオブジェクト作成の要求が送られ、ノードがクラスタへ追加されます。
(1) kubelet, kube-proxyサービスを起動します。
# systemctl start kubelet kube-proxy
(2) worker-2がKubernetesクラスタに追加されていることを確認します。
# k get nodes
NAME STATUS ROLES AGE VERSION
worker-0 Ready <none> 13d v1.15.3
worker-1 Ready <none> 13d v1.15.3
worker-2 Ready <none> 3m49s v1.15.3
(3) worker-2上にnginxポッドが起動することを確認します。
# vi nginx.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
NAME READY STATUS RESTARTS AGE
nginx 1/1 Running 0 8s
# kubectl describe po |grep -e Node -e Name
Name: nginx
Namespace: default
Node: worker-2/192.168.184.222
SecretName: default-token-5jpg7
Node-Selectors: kubernetes.io/hostname=worker-2
#
(4) worker-2上のポッドがgVisorで起動していることを確認します。
# kubectl exec -it nginx -- dmesg | grep gVisor
[ 0.000000] Starting gVisor...
これでworker-2のデフォルトランタイムはgVisor(runsc)になりました!
お好みでworker-2にノードラベルなどを付与して、通常ポッドとgVisorポッドのスケジューリングを使い分けてみてください。
おわりに
今回は特定のノードのコンテナランタイムを変更する方法を紹介しました。
ノードにコンテナランタイムを複数入れて、K8sクラスタのruntime.classなどで指定する方法もあるようです。
当記事で理解できたら、是非応用としてチャレンジしてみてください。