Kubernetes 1.16: Ephemeral Containers (alpha)

## はじめに

ここでは、Kubernetes 1.16 で実装された Ephemeral Containers (エフェメラルコンテナ) をみていきます。

- :warning: **alpha レベルは実験段階の機能です。検証目的でのみ使用し、プロダクションで使用することはやめましょう。**
- :warning: **1.16 時点での情報が記載されています。その後に仕様が変更されている可能性があることに注意してください。**

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

エフェメラルコンテナは、実行中の Pod に対してエフェメラルな（揮発的な、一時的な）コンテナを後から追加する機能です。何がうれしいかというと、これまで実行中の Pod のコンテナでデバッグしたいときには `kubectl exec` コマンドを使ってデバッグしたいコンテナ内で任意のコマンドを実行するということをやっていました。この方法での問題は、デバッグ対象のコンテナが scratch イメージ（空の何も含まれていないイメージ）をベースイメージとしているとシェルさえもない場合もあり、あとからコマンドをインストールすることもできず、デバッグが困難です。デバッグ用のコンテナをサイドカーとして一緒にデプロイしておくこともできますが、全ての Pod のサイドカーとして追加しておくのはコストがかかります。あとから必要なときにデバッグ用のコンテナを Pod に追加できれば、アプリケーションコンテナにデバッグ用のコマンドを含む必要もなく、デメリットなしに最小のイメージを作成できます。

エフェメラルコンテナは、Kubernetes 1.16 で全体を構成する一部の機能が alpha として提供されました。全ての機能が alpha ステージで出揃うのは 1.18 が予定されています。より詳しい情報は、[KEP](https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/20190212-ephemeral-containers.md) を参照してください。



## エフェメラルコンテナを有効にしたクラスタの作成

ここでは、minikube を使用してエフェメラルコンテナを有効にしたクラスタを用意します。エフェメラルコンテナは、alpha ステージの機能のため、デフォルトでは有効化されていません。使用するには kube-apiserver の Feture Gates `EphemeralContainers` を明示的に有効にする必要があります。

```
$ minikube version
minikube version: v1.4.0
commit: 7969c25a98a018b94ea87d949350f3271e9d64b6
$ minikube start --kubernetes-version=v1.16.0 --feature-gates=EphemeralContainers=true
$ kubectl version
Client Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.0", GitCommit:"2bd9643cee5b3b3a5ecbd3af49d09018f0773c77", GitTreeState:"clean", BuildDate:"2019-09-19T14:00:14Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"darwin/amd64"}
Server Version: version.Info{Major:"1", Minor:"16", GitVersion:"v1.16.0", GitCommit:"2bd9643cee5b3b3a5ecbd3af49d09018f0773c77", GitTreeState:"clean", BuildDate:"2019-09-18T14:27:17Z", GoVersion:"go1.12.9", Compiler:"gc", Platform:"linux/amd64"}
$ kubectl get po -n kube-system kube-apiserver-minikube -o yaml | grep feature-gates
    - --feature-gates=EphemeralContainers=true
```

## `kubectl-debug` プラグインのインストール

1.16 で提供されたのはエフェメラルコンテナの機能を構成する一部分で、実際に kubectl コマンドを使ってエフェメラルコンテナを追加するサブコマンドはまだ提供されていません。このサブコマンドは `kubectl debug` コマンドとして 1.17 で提供予定となっていますが、PoC 実装である `kubectl-debug` プラグインが用意されています。ここではこの kubectl プラグインを使用します。

- [verb/kubectl\-debug: kubectl debug plugin](https://github.com/verb/kubectl-debug)

```
$ cd $(mktemp -d)
$ curl -LO https://github.com/verb/kubectl-debug/releases/download/v0.1.0/kubectl-debug_darwin_amd64.tar.gz
$ tar xvzf kubectl-debug_darwin_amd64.tar.gz
$ mkdir -p ~/bin
$ mv kubectl-debug ~/bin/
$ export PATH="$HOME/bin:$PATH"
$ kubectl debug -h
```

kubectl プラグインの動作について知りたい方は、[kubectl のプラグイン機能 kubectl plugin を使おう！](https://qiita.com/superbrothers/items/b4a0aab0575ca6d65739)を参照してください。

## デバッグ対象の Pod の作成

次に今回デバッグ対象とする Pod を作成します。ここでは Node.js ランタイムのみを含む [distroless/nodejs](https://github.com/GoogleContainerTools/distroless) をベースイメージにしたコンテナを含む `debugtest` Pod を作成します。

```
$ cat <<EOL | kubectl apply -f-
apiVersion: v1
kind: Pod
metadata:
  name: debugtest
spec:
  shareProcessNamespace: true
  containers:
  - name: myapp
    image: docker.io/superbrothers/distroless-examples-nodejs-hello-http
EOL
pod/debugtest created
$ kubectl exec -it debugtest /bin/sh
OCI runtime exec failed: exec failed: container_linux.go:345: starting container process caused "exec: \"/bin/sh\": stat /bin/sh: no such file or directory": unknown
command terminated with exit code 126
```

Node.js ランタイムしか含まないイメージをベースとしているコンテナなので、`kubectl exec` コマンドで `/bin/sh` を実行しようとしても存在しないため、実行できずデバッグできない状態です。

ここで注意する点として、1.16 時点でデバッグ対象とする Pod は「プロセスネームスペースの共有」機能（Pod `spec.shareProcessNamespace`）を有効にしておく必要があります。これはあとから追加されるエフェメラルコンテナから Pod に含まれるデバッグ対象のコンテナのプロセスをみられるようにしなければならないためです。将来的には「プロセスネームスペースの共有」機能を使用せず、あとからデバッグ対象のコンテナのプロセスネームスペース明示的に共有する機能が提供予定です（1.17）。

## エフェメラルコンテナを追加する

では、最後に実際にエフェメラルコンテナを追加してみます。それには先にインストールした `kubectl-debug` プラグインを使用します。現時点でデバッグ用コンテナに利用されるイメージは busybox がデフォルトとなっていますが、`--image` フラグで任意のイメージに変更できます。

ここでは、デバッグ対象のアプリケーションコンテナで名前解決ができていないかもしれないことを確認するというシナリオとします。このアプリケーションコンテナには nslook コマンドはおろか、sh コマンドでさえインストールされていないため、そのままではデバッグが困難です。

```
$ kubectl debug debugtest --attach
If you don't see a command prompt, try pressing enter.
/ # cat --help 2>&1 | head -n 1
BusyBox v1.31.0 (2019-09-04 17:25:45 UTC) multi-call binary.
/ # ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    6 root      0:00 /nodejs/bin/node hello_http.js
   48 root      0:00 sh
   53 root      0:00 ps aux
/ # cat /proc/6/root/etc/resolv.conf
nameserver 10.96.0.10
search default.svc.cluster.local svc.cluster.local cluster.local
options ndots:5
/ # nslookup kubernetes.default.svc.cluster.local
Server:         10.96.0.10
Address:        10.96.0.10:53

Name:   kubernetes.default.svc.cluster.local
Address: 10.96.0.1

*** Can't find kubernetes.default.svc.cluster.local: No answer

/ # exit
Session ended, resume using 'kubectl attach debugtest -c debugger -i -t' command when the pod is running
```

以上から、アプリケーションコンテナの `/etc/resolv.conf` の内容に問題はなく、busybox コンテナに含まれる nslook コマンドで名前解決できることを確認できました。

## エフェメラルコンテナの情報を確認する

ここからは少し詳細についてみていきます。 エフェメラルコンテナを追加した Pod はその情報が `spec.ephemeralContainers` フィールドとしてマニフェストに追加されます。また、エフェメラルコンテナのステータスは `status.ephemeralContainerStatuses` に追加されます。

```
$ kubectl get po debugtest -o yaml
(略)
spec:
  (略)
  ephemeralContainers:
  - image: busybox
    imagePullPolicy: IfNotPresent
    name: debugger
    resources: {}
    stdin: true
    terminationMessagePolicy: File
    tty: true
  (略)
status:
  (略)
  ephemeralContainerStatuses:
  - containerID: docker://c123e9f7e000f00d5a4660a845616af78f7a6d9470aeebee4a9a4ad8c0a88dac
    image: busybox:latest
    imageID: docker-pullable://busybox@sha256:fe301db49df08c384001ed752dff6d52b4305a73a7f608f21528048e8a08b51e
    lastState: {}
    name: debugger
    ready: false
    restartCount: 0
    state:
      terminated:
        containerID: docker://c123e9f7e000f00d5a4660a845616af78f7a6d9470aeebee4a9a4ad8c0a88dac
        exitCode: 0
        finishedAt: "2019-09-20T06:36:21Z"
        reason: Completed
        startedAt: "2019-09-20T06:35:34Z"
```

## エフェメラルコンテナを操作する Kubernetes API

エフェメラルコンテナの操作は Kubernetes API としては Pod の `ephemeralcontainers` サブリソースで表現されています。操作は取得（GET）と更新（PUT）のみです。

```
/api/v1/namespaces/<namespace-name>/pods/<pod-name>/ephemeralcontainers
```

client-go ライブラリからは次のように使用します。これは、エフェメラルコンテナを追加する操作のみなので、アタッチする場合は、`kubectl attach` 相当の処理が必要です。

```go
pods := o.clientset.CoreV1().Pods(info.Namespace)
ec, err := pods.GetEphemeralContainers(info.Name, metav1.GetOptions{})
if err != nil {
	return err
}

ec.EphemeralContainers = append(ec.EphemeralContainers, o.debugContainer)
ec, err = pods.UpdateEphemeralContainers(info.Name, ec)
if err != nil {
	return err
}
```

## まとめ

エフェメラルコンテナは、実行中の Pod に対してエフェメラルな（揮発的な、一時的な）コンテナを後から追加する機能です。この機能によって scratch イメージ等をベースとした最小のイメージをコマンドが含まれておらずデバッグが困難というデメリットなしに使用できるようになります。1.16 リリース時点では、エフェメラルコンテナに関連する全ての機能が alpha レベルで提供されるのが 1.18 となっています。beta、GA での提供はまだまだ先ですが、期待して待ちましょう。