kubectl execは禁止!? kubectl debugの勧め
2022年07月04日 月曜日
CONTENTS
Kubernetes 1.23から標準で利用できるようになったエフェメラルコンテナ(Ephemeral Container)をご存じですか? セキュアにKubernetesを運用していくには必須の機能になることは間違いなく、IKE(IIJ Kubernetes Engine)では早速活用が始まっています。実利用に耐える環境が整うにはまだ少し時間がかかるかもしれませんが、今のうちから使いこなせるように紹介します。
エフェメラルコンテナ
一般的には聞きなれないかもしれませんが、IaaSを使っていればエフェメラル(Ephemeral。短命)という単語はむしろ日常的に耳にしていることでしょう。例えば、IaaSのインスタンスにデフォルトで用意されているストレージはエフェメラルストレージと呼ばれます(インスタンスストアと呼ばれることもあります)。これは、インスタンスを停止するとともにデータがすべて削除され、インスタンスを再び起動したときには初期化された状態でプロビジョニングされることから、このような名前で呼ばれています。ストレージに書き込まれたデータは永続化されるのが普通なので、永続的ではないことを強調するためにPersistentの対義語としてEphemeralが用いられるわけです。
Kubernetes 1.23から標準機能となったエフェメラルコンテナも同じような意味合いで、一時的にコンテナを起動し、役目を終えたら削除される、そんな使い方のために設けられた仕組みです。その用途は特に限定されるわけではありませんが、トラブルシュートのために利用されるのが一般的です。というのは、Podリソースはほとんどのフィールドを変更することができないわりと特殊な性質を持っており、従来ならば変更を加えるにはPodの再作成=コンテナの再移動が必要だったのですが、エフェメラルコンテナを使うと「既存のPodを止めることなく、コンテナを追加起動できる」からです。
そして、Kubernetesでは同一Podに共存するコンテナは
- localhost(127.0.0.1)でお互いに疎通できる
- 同一のストレージボリュームをマウントできる(RWOであっても)
という特徴を備えています。したがって、既存コンテナが状態を失わないように動かしたまま、便利なツールを含んだ新しいコンテナをすぐ隣に起動し、トラブルシュートに利用できるのです。
試しにエフェメラルコンテナを起動してみましょう。ここではまずnginxを起動し、そこにエフェメラルコンテナとしてbusyboxを起動しています。そして、http://localhostへアクセスすると、確かにnginxへアクセスできることがわかります。
$ # nginxを起動する $ kubectl create deploy nginx --image=nginx:1.21 $ # エフェメラルコンテナとしてbusyboxを起動し、シェルにアタッチする $ PODNAME=$(kubectl get pod -o name --selector=app=nginx) $ kubectl debug -it --image=busybox ${PODNAME} # # エフェメラルコンテナからnginxへlocalhostでアクセスする # wget -O - http://localhost <h1>Welcome to nginx!</h1> $ exit # シェルから抜けるとエフェメラルコンテナが停止する
エフェメラルコンテナを起動した痕跡は、停止した後でも確認できます。エフェメラルコンテナを起動するたびに追記されていくので、繰り返し使っているとかなりうるさいことになってしまうのは、どうにかして欲しいところです。
$ # Podリソースのspec以下に追加された情報 $ kubectl get pod --selector=app=nginx -ojsonpath='{.items[].spec.ephemeralContainers}' | jq . [ { "image": "busybox", "imagePullPolicy": "Always", "name": "debugger-fk9dk", "resources": {}, "stdin": true, "terminationMessagePath": "/dev/termination-log", "terminationMessagePolicy": "File", "tty": true } ] $ # PodリソースのStatusに記録されたステータス $ kubectl get pod --selector=app=nginx -ojsonpath='{.items[].status.ephemeralContainerStatuses}' | jq . [ { "containerID": "docker://caf554da7b89542fe35101614b14710613fb13b252b3ed82831356d5ea1029c5", "image": "busybox:latest", "imageID": "docker-pullable://busybox@sha256:3614ca5eacf0a3a1bcc361c939202a974b4902b9334ff36eb29ffe9011aaad83", "lastState": {}, "name": "debugger-fk9dk", "ready": false, "restartCount": 0, "state": { "terminated": { "containerID": "docker://caf554da7b89542fe35101614b14710613fb13b252b3ed82831356d5ea1029c5", "exitCode": 0, "finishedAt": "2022-06-25T11:21:35Z", "reason": "Completed", "startedAt": "2022-06-25T11:20:01Z" } } } ]
今後はkubectl exec禁止がトレンドに?
すでにKubernetesを活用している方なら、エフェメラルコンテナを使わずとも「kubectl execして、既存コンテナにシェルを起動すればいいのでは」と思うことでしょう。確かにその通りなのですが、それはコンテナイメージにシェルやツール類があらかじめ含まれている場合の話です。でも、それはセキュリティを考えると好ましいことではありません。余計なパッケージをインストールすればするほど脆弱性のリスクを抱えることになりますし、なんらかのクレデンシャルをSecretリソースを通じてコンテナに設定している場合、kubectl execできれば容易に平文でアクセスできてしまいます。
逆に、極端な話コンテナの中に/bin、/usr/bin以下が何もなければ、万が一脆弱性を突かれてコンテナを操作する権限を奪われてしまったとしても、そこそこ行動を制限することができるでしょう。実際、商用に配布されているコンテナイメージでは珍しくありません。理想的には、コンテナイメージには必要最低限のパッケージだけがインストールされているべきなのです。
IKEでは、今後トラブルシュートツールはそれ専用のコンテナイメージに用意しておき、必要な時にだけエフェメラルコンテナとして実行するようにルールを設けていきたいと考えています(すぐにではありません)。そして、それで十分運用が回ることが確認できたら、kubectl execを禁止するようなポリシーを適用します(ホワイトリストで例外は管理すると思いますが)。
既存コンテナのファイルシステムへアクセスする
ただし、エフェメラルコンテナを使いこなすには、今のところツールの整備が追い付いていないようです。というのは、kubectl debugコマンドを使ってエフェメラルコンテナを起動しても、ファイルシステムはそれ自身のものしか見えません。つまり、トラブルシュートしたい別コンテナのファイルシステムへアクセスできないということです。それはトラブルシュート用途の機能としては、かなりの制約と言えるでしょう。
もっとも、簡単にアクセスできないのは、単にkubectl debugの機能が乏しいからでしかなく、アクセスすることは可能です。少々面倒な手順ではありますが、試しにエフェメラルコンテナに既存コンテナと同じPersistent Volumeをマウントしてみましょう。ツールを使わずにどうやってエフェメラルコンテナへPodをマウントするかといえば、Kubernetes APIをHTTPクライアントで直接叩きます。このとき使うAPIは下記に解説されているので、詳細は割愛します。
まず、Persistent Volumeをマウントしたnginxを一つ普通に起動しましょう。 以下コマンドを実行すると、/usr/share/nginx/htmlにPVがマウントされた状態でnginxが起動されます。
こういうことが簡単にコマンドラインで実行できない時点で、kubectl debugが不便なのは当然と言えましょう
$ # PersistentVolumeClaimを作成する $ kubectl create -f - <<EOF kind: PersistentVolumeClaim apiVersion: v1 metadata: name: html spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi EOF $ kubectl create deploy nginx --image=nginx:1.21 $ kubectl patch deploy nginx -p '{"spec": {"template": {"spec": { "volumes":[ {"name": "html", "persistentVolumesClaim": { "claimName": "html" }}]}}}}' $ kubectl patch deploy nginx -p '{"spec": {"template": {"spec": { "containers":[ { "name": "nginx", "volumeMounts": [ {"name": "html", "mountPath": "/usr/share/nginx/html"}]}]}}}}'
そして、次にエフェメラルコンテナをこのPodに追加します。Kubernetes APIのエンドポイントへメソッドPATCHでAPI仕様に沿ったJSONファイルをリクエストすれば、コンテナ名debuggerでbusyboxのコンテナが起動します。
$ PODNAME=$(kubectl get pod -o name --selector=app=nginx | sed -e 's/^pod/pods/') $ echo $PODNAME pods/nginx-65475c99b6-rnw7q $ cat patch.json { "spec": { "ephemeralContainers": [ { "image": "busybox", "name": "debugger", "stdin": true, "tty": true, "volumeMounts": [ { "mountPath": "/usr/share/nginx/html", "name": "html" } ] } ] } } $ kubectl proxy & $ http PATCH http://localhost:8001/api/v1/namespaces/default/${PODNAME}/ephemeralcontainers \ Content-Type:application/strategic-merge-patch+json \ @patch.json
それでは、エフェメラルコンテナに入って、/usr/share/nginx/htmlにHTMLファイルを作成しましょう。これがnginx(http://localhost)を通して見えれば、エフェメラルコンテナにマウントされたPVがnginxにマウントされたそれと同一であることがわかります。
ここでは、起動中のエフェメラルコンテナへターミナルをアタッチするため、kubectl attachコマンドを利用しています。
$ kubectl attach -it $PODNAME -c debugger / # cat > /usr/share/nginx/html/index.html <html> <body> <p>kubectl debug</p> </body> </html> / # wget -O - http://localhost Connecting to localhost (127.0.0.1:80) writing to stdout <html> <body> <p> kubectl debug </p> </body> <html>
見えましたね!実際には、このオペレーションを前提に運用に乗せるのは無理がありますが、ツールの整備が進めばいいだけなので時間が解決してくれることでしょう(もしくは、あなたがツールを開発してもよいでしょう)。
最後にちょっとしたTIPSですが、こうしてアタッチしたセッションはCtrl+P, Ctrl+Qと順番にタイプすることで、エフェメラルコンテナを残したままデタッチが可能です。エフェメラルコンテナを起動したままにしておくのはお勧めできませんが、繰り返し起動するとリソース情報が膨れ上がってしまうので、うまく使い分けてください。
それでは皆さん、来るべき日に備えてトラブルシュート用に便利なツールを詰め込んだアーミーナイフコンテナを洗練させておいてください。そして、kubectl execがなくても運用が回るように、エフェメラルコンテナに慣れておいてください。