エフェメラルコンテナでJDKのデバッグツールを使う夢を見つつ、Podとコンテナのライフサイクルに思いを馳せる
気づいたらだいぶブログをサボっていました…。
これはKubernetesアドベントカレンダーの19日目のエントリーです。メリー・クリスマス!
エフェメラルコンテナ x JVMのデバッグツールという、誰得な話を書きたいと思います。
1 . エフェメラルコンテナとは
エフェメラルコンテナってなに?という解説は、@superbrothersさんが公開してくださっている以下の資料を見るのが一番ですので、ここでは簡単な説明に留めます。
- Qiita: Kubernetes 1.16: Ephemeral Containers (alpha)
- SpeakerDeck: Kubernetes 1.16 アルファ機能を先取り! エフェメラルコンテナ
エフェメラルコンテナは、実行中のPodにエフェメラルな(揮発的な、一時的な)コンテナを後から追加する機能です。 一般的には、コンテナには、動かしたいアプリケーションが必要としないコマンドやモジュールは極力含めないようにするのが良いとされています。 エフェメラルコンテナがあると、デバッグのためのコマンド等をそちらに入れて実行できるようになります。このため、アプリケーションのコンテナからデバッグのためだけに必要なコマンドやモジュールを排除することができ、理想的なコンテナが作成できるというわけです。
2 . エフェメラルコンテナからJDKのデバッグツールを使ってみる
ここから本題ですが、Java, Scala, Kotolinなど、JVMベースのアプリケーションをKubernetesで動かすことを考えてみます。 代用的なJDKディストリビューションのひとつであるOpenJDKには、jps, jmcd, jstack, jfrといった、JVMアプリのデバッグやトラブルシューティングのための便利なツール群が同梱されています。1
しかし、これらは JDK(Java Development Kit) の一部であって、アプリケーションの実行に必要なランタイムにあたる(Java Runtime Environment)には含まれません。アプリケーションを実行するコンテナには当然JREだけを入れおきたいわけですが、そうしてしまうと上記のツール群は利用できないということです。
ここでエフェメラルコンテナの登場ということになります。JVMアプリのコンテナ(JREを使って動いている)を含むPodに対して、JDKのエフェメラルコンテナを追加して、JDKに同梱されているデバッグツールを使っていこうというわけです。
それではやってみましょう…!
エフェメラルコンテナを利用可能なクラスターの作成
(!!!注意: ここから手順は期待通りに動かなかったというオチなので参考程度に御覧ください!!!)
@superbrothersさんの記事ではminikubeを使っていますが、せっかくなので最近覚えたkindを使ってクラスターを作っていきたいと思います。
エフェメラルコンテナは、最新のKubernetes 1.16から追加されたAlphaの機能です。このため、該当するバージョンで、かつ EphemeralContainers
というfeature gateを有効にする必要があります。
kindではクラスターの構成をyaml形式の設定ファイルで定義することができ、ここでfeature gateを指定することが可能です。
kind: Cluster apiVersion: kind.sigs.k8s.io/v1alpha3 kubeadmConfigPatches: - | apiVersion: kubeadm.k8s.io/v1beta2 kind: ClusterConfiguration metadata: name: config apiServer: extraArgs: "feature-gates": "EphemeralContainers=true" scheduler: extraArgs: "feature-gates": "EphemeralContainers=true" controllerManager: extraArgs: "feature-gates": "EphemeralContainers=true" - | apiVersion: kubeadm.k8s.io/v1beta2 kind: InitConfiguration metadata: name: config nodeRegistration: kubeletExtraArgs: "feature-gates": "EphemeralContainers=true" nodes: - role: control-plane - role: worker
このようなファイルを作成したら、kindでクラスターの作成を行います。
kind create cluster --config kind-config.yaml
Creating cluster "kind" ...
✓ Ensuring node image (kindest/node:v1.16.3) 🖼
✓ Preparing nodes 📦
✓ Writing configuration 📜
… (snip) …
クラスターが出来上がったら、feature gateが有効になっていることを確認してみます。
$ kubectl get po -n kube-system kube-apiserver-kind-control-plane -o yaml | grep feature-gates
- --feature-gates=EphemeralContainers=true
これでクラスターは準備OKです。
kubectl-debugプラグインのインストール
kubectlでエフェメラルコンテナを追加するには、 kubectl debug
サブコマンドが必要ですが、これを利用にするために、kubectl-debugプラグインを導入します。
(linux_amd64環境での実行例です。環境に合わせて読み替えをお願いします)
$ cd $(mktemp -d)
$ curl -LO https://github.com/verb/kubectl-debug/releases/download/v0.1.2/kubectl-debug_linux_amd64.tar.gz
$ tar xvzf kubectl-debug_linux_amd64.tar.gz
$ sudo mv kubectl-debug /usr/local/bin/
$ kubectl debug -h
JVMアプリのPodを動かす
サンプルのJVMアプリのPodを動かしておきます。
この記事のケースではOpenJDKのJREで動いているアプリであればなんでもOKです。例えば、以下のようなmanifestを作ります。
apiVersion: v1 kind: Pod metadata: name: cowweb spec: shareProcessNamespace: true containers: - name: cowweb image: registry.hub.docker.com/hhayakaw/cowweb:v2.1 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080
shareProcessNamespace: true
となっているところが大切なポイントの1つで、これによってPod内のコンテナがプロセス空間を共有することができます。つまり ps
コマンドなどで、Pod内の他のコンテナで動いているプロセスが見えることになります。
それではこのmanifestをクラスターに適用します。
$ kubectl apply -f cowweb.yaml $ kubectl get pod $ NAME READY STATUS RESTARTS AGE $ cowweb 1/1 Running 0 9s
Podにエフェメラルコンテナを追加する
それでは、エフェメラルコンテナとしてJDKのコンテナをPodに追加します…。と言いたいところなのですが、公式のOpenJDKのイメージはENTRYPOINTがjShellというツールになっていて、通常のシェルではありません。 今回のような使い方では通常のシェルからデバッグツールを実行したいので、以下のようなENTRYPOINTだけを書き換えたコンテナを作って、あらかじめレジストリにPushしておく必要がありました。
FROM openjdk:11-jdk-slim ENTRYPOINT ["/bin/sh"]
それではいよいよ、Podにエフェメラルコンテナを追加します。 -m
は追加するエフェメラルコンテナのイメージ名(この前の手順でPushしたものを指定)、 -c
はコンテナの name
を指定するオプションです。
kubectl debug cowweb -m hhayakaw/jdk11-debug -c jdebug
Podの内容を見てみると、エフェメラルコンテナが追加されていることがわかります。
kubectl describe pod cowweb | grep -A 5 Ephemeral
このエフェメラルコンテナに kubectl attach
します。
kubectl attach -it cowweb -c jdebug
これでエフェメラルコンテナ(OpenJDK 11が入っている)のシェルプロンプトにアクセスできました。
デバッグツールを使う…!(うまく行かない)
いよいよ、デバッグツールを使うときが来ました。
Podには shareProcessNamespace: true
がつけてあるので jps
を打てばアプリのJVMのプロセスが見えるはず。手始めに jps
からの jcmd
でJVMの起動フラグの一覧でも見てみるか、と思うわけですが…。
# jps
29 Jps
jpsそのもののプロセスしか見えない…?あれー、なんで?
...
調べてみたところ、jpsを利用するには ptrace
システムコールが必要になるようで、エフェメラルコンテナにこれを実行できる権限をつけないといけません。しかし、 kubectl debug
コマンドにはそんな権限を追加するオプションはない…。
仕方ないので、JSONで securityContext
で SYS_PTRACE
が有効になるように記述したエフェメラルコンテナを作って、 kubectl replace
でPodに追加してみます。 kubectl replace
でエフェメラルコンテナを追加する方法は、公式ドキュメントにサンプルがあったので、それに securityContext
の記述を追加してあげる感じです。
まずはJSON。
{ "apiVersion": "v1", "kind": "EphemeralContainers", "metadata": { "name": "cowweb" }, "ephemeralContainers": [ { "name": "jdebug", "image": "openjdk:11-jdk-slim", "imagePullPolicy": "IfNotPresent", "command": [ "/bin/sh" ], "stdin": true, "tty": true, "terminationMessagePolicy": "File", "securityContext": { "capabilities": { "add": [ "SYS_PTRACE" ] } } } ] }
エフェメラルコンテナ追加!
kubectl replace --raw /api/v1/namespaces/default/pods/cowweb/ephemeralcontainers -f jdebug.yaml
え…。
The Pod "cowweb" is invalid: spec.ephemeralContainers[0].securityContext: Forbidden: cannot be set for an Ephemeral Container
securityContext: Forbidden
。どうやらエフェメラルコンテナではやろうとしている権限追加は制限されているようです。
残念ながら、JVMのデバッグツールは、エフェメラルコンテナからでは使えないという結論に…。
3 . KubernetesでJDKのデバッグツールを利用する現状可能な方法
ということで、似たようなことを現状のKubernetesの機能でやろうとするとどうなるかというと、エフェメラルコンテナの代わりにサイドカーコンテナを使うことが考えられます。
つまり、下のように、JDK入りのコンテナをあらかじめPodに入れておくというやり方です。
apiVersion: v1 kind: Pod metadata: name: cowweb spec: shareProcessNamespace: true containers: - name: cowweb image: registry.hub.docker.com/hhayakaw/cowweb:v2.1 imagePullPolicy: IfNotPresent ports: - name: http containerPort: 8080 - name: jdebug image: openjdk:11-jdk-slim imagePullPolicy: IfNotPresent command: ['/bin/sh'] stdin: true tty: true terminationMessagePolicy: File securityContext: capabilities: add: - SYS_PTRACE
このようなPodを作った上でサイドカーコンテナ(jdebug)にアタッチすれば、JVMツールを利用することが可能です。
kubectl attach -it cowweb -c jdebug
以下のように、 jps
でJVMアプリのプロセスIDを確認して、 jcmd
でJVMのフラグを表示する、といったことができます。
# jps 7 cowweb.jar <-- アプリのプロセスが見えている 29 Jps # jcmd 7 VM.flags 7: -XX:CICompilerCount=2 -XX:InitialHeapSize=264241152 -XX:MaxHeapSize=4204789760 -XX:MaxNewSize=1401552896 -XX:MinHeapDeltaBytes=196608 -XX:NewSize=88080384 -XX:NonNMethodCodeHeapSize=5825164 -XX:NonProfiledCodeHeapSize=122916538 -XX:OldSize=176160768 -XX:ProfiledCodeHeapSize=122916538 -XX:ReservedCodeCacheSize=251658240 -XX:+SegmentedCodeCache -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:+UseSerialGC
しかし、サイドカーコンテナをデバッグのために使う場合、必要なときだけこのコンテナを起動するということはできません。 Kubernetes的には本体(ここではJVMアプリ)のコンテナもサイドカーコンテナも取り扱いに区別はなく、どちらかのコンテナが動作していないPodは異常な状態とみなされて、再起動がかけられてしまいます。
このため、JDKの入ったコンテナを、普段は使わないのに上げっぱなしにしておかなければいけないわけです。もちろん、コンテナが動いていると言ってもJVMまで起動しているわけではないので、リソース(メモリやCPU)の消費は大したことはありません。しかしPodの中に余計なものがあるのは好ましくありませんし、権限の強いコンテナが置きっぱなしなのはセキュリティ的にもいいことではないでしょう。
4 . まとめ - Podとコンテナのライフサイクルについて
サイドカーコンテナを使えばアプリのコンテナに対してJVMツールを実行することが可能ですが、本体のコンテナと起動・停止のライフサイクルが同じになるように強制されてしまうため、デバッグという用途だけを考えると理想的ではありません。エフェメラルコンテナは、Podに後から追加できるなど、デバッグに適した本体のコンテナとは別のライフサイクルを持っていますが、JVMのデバッグツールを実行するには制限がありました。
いまのところ、JVMのデバッグツールという限られた用途に関していうと、エフェメラルコンテナは機能が足りていないということになりそうです。
ただ、今回の検証は残念な結果に終わってしまったものの、エフェメラルコンテナの特徴である「本体のコンテナと別のライフサイクルを持つ追加コンテナ」というコンセプトには、大きな可能性を感じました。
例えば、1)アプリ本体のコンテナに対して、起動初期に暖機運転のトラフィックを送るだけのコンテナを設けたり、2)ログエージェントのような、それが死んでもPodを落とすまではしてほしくない、といったケースは現実に存在すると思います。これらをカバーするには、本体のコンテナとは独立したライフサイクルが求められてくると思うのです。
こういった機能が徐々にサポートされて、Kubernetesがより便利に発展していってくれるといいですね。自分もできる働きかけしていければと思います。
深夜3:00を回ってだいぶ取り留めがなくなってきたので、このへんで…。
皆さん良いお年をお迎えください。メリー・クリスマス!
-
他にも同じものを含むディストリビューションもあるかもしれません↩