エフェメラルコンテナでJDKのデバッグツールを使う夢を見つつ、Podとコンテナのライフサイクルに思いを馳せる

気づいたらだいぶブログをサボっていました…。

これはKubernetesアドベントカレンダーの19日目のエントリーです。メリー・クリスマス!

エフェメラルコンテナ x JVMデバッグツールという、誰得な話を書きたいと思います。

1 . エフェメラルコンテナとは

エフェメラルコンテナってなに?という解説は、@superbrothersさんが公開してくださっている以下の資料を見るのが一番ですので、ここでは簡単な説明に留めます。

エフェメラルコンテナは、実行中のPodにエフェメラルな(揮発的な、一時的な)コンテナを後から追加する機能です。 一般的には、コンテナには、動かしたいアプリケーションが必要としないコマンドやモジュールは極力含めないようにするのが良いとされています。 エフェメラルコンテナがあると、デバッグのためのコマンド等をそちらに入れて実行できるようになります。このため、アプリケーションのコンテナからデバッグのためだけに必要なコマンドやモジュールを排除することができ、理想的なコンテナが作成できるというわけです。

f:id:charlier_shoe:20191219033039p:plain
エフェメラルコンテナ

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に同梱されているデバッグツールを使っていこうというわけです。

f:id:charlier_shoe:20191219033112p:plain
エフェメラルコンテナ x 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 からの jcmdJVMの起動フラグの一覧でも見てみるか、と思うわけですが…。

# jps
29 Jps

jpsそのもののプロセスしか見えない…?あれー、なんで?

...

調べてみたところ、jpsを利用するには ptrace システムコールが必要になるようで、エフェメラルコンテナにこれを実行できる権限をつけないといけません。しかし、 kubectl debug コマンドにはそんな権限を追加するオプションはない…。

仕方ないので、JSONsecurityContextSYS_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 . KubernetesJDKデバッグツールを利用する現状可能な方法

ということで、似たようなことを現状の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

以下のように、 jpsJVMアプリのプロセスIDを確認して、 jcmdJVMのフラグを表示する、といったことができます。

# 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を回ってだいぶ取り留めがなくなってきたので、このへんで…。

皆さん良いお年をお迎えください。メリー・クリスマス!


  1. 他にも同じものを含むディストリビューションもあるかもしれません