microk8sでエフェメラルコンテナをさわってみた

microk8s 1.18.0(latest/stable 4/13時点)の環境でエフェメラルコンテナをさわってプロセステーブルの共有を確認しました。
動かすまでそこそこ苦労したので記録を残しておきます。


# kubelet/kube-apiserverにオプションを追加

--feature-gatesというオプションでEphemeralContainers=trueと、kubeletならびにkube-apiserverに対してカツを入れてやる必要があります。
microk8s v1.18では、

~~~ bash
> /snap/microk8s/current/kube-apiserver --help
<snip>
      --feature-gates mapStringBool
<snip>
                EphemeralContainers=true|false (ALPHA - default=false)
~~~

~~~ bash
> /snap/microk8s/current/kubelet --help
<snip>
      --feature-gates mapStringBool
<snip>
                EphemeralContainers=true|false (ALPHA - default=false)
~~~
とどちらもデフォルトでfalseになってるんですな。

今回は(正直よくわからんかったので)コマンド引数の設定ファイルを編集することで対応しました。
対象ファイルはどこかいなと探してみると、/snap以下に/snap/microk8s/current/default-args/というディレクトリがあり、kube-apiserverとkubeletというファイルが収められています。中を見るとまんま引数が書いてあるので、喜び勇んでrootで編集しようとするとパーミッションで撥ねられます。
今まで全然知らなかったんですけど、snapのディレクトリは

~~~ bash
> mount -l
<snip>
/var/lib/snapd/snaps/microk8s_1320.snap on /snap/microk8s/1320 type squashfs (ro,nodev,relatime,x-gdu.hide)
~~~
と、イメージをroでマウントしとるんですな。
/snap以下をremountするとか恐ろしい方法もなくはないんでしょうが、他にファイルがあるんじゃないかと探してみると/var/snap/microk8s/current/argsにファイルがあるのでこいつでトライしてみます。

~~~ bash
--feature-gates=EphemeralContainers=true
~~~
という行を、ファイルkube-apiserverとkubeletにそれぞれ追加し、systemctlでsnap.microk8s.daemon-apiserver.serviceとsnap.microk8s.daemon-kubelet.serviceを再起動します。(関係ないがこのオプションの書き方、最近よく見かけるけどすげえ気持ち悪い・・・)
すると、

~~~ bash
> pgrep -a kubelet 
134465 /snap/microk8s/1320/kubelet --kubeconfig=/var/snap/microk8s/1320/credentials/kubelet.config \
 --cert-dir=/var/snap/microk8s/1320/certs --client-ca-file=/var/snap/microk8s/1320/certs/ca.crt \
 --anonymous-auth=false --network-plugin=cni --root-dir=/var/snap/microk8s/common/var/lib/kubelet \
 --fail-swap-on=false --cni-conf-dir=/var/snap/microk8s/1320/args/cni-network/ \
 --cni-bin-dir=/snap/microk8s/1320/opt/cni/bin/ --feature-gates=DevicePlugins=true \
 --eviction-hard=memory.available<100Mi,nodefs.available<1Gi,imagefs.available<1Gi \
 --container-runtime=remote --container-runtime-endpoint=/var/snap/microk8s/common/run/containerd.sock\
 --node-labels=microk8s.io/cluster=true --cluster-domain=cluster.local --cluster-dns=10.152.183.10 \ 
 --feature-gates=EphemeralContainers=true
~~~
と最終行に反映されていたので、きっと対処としては間違ってなかったのでしょう・・・

ちなみに、kube-apiserverにオプションつけないときは後述のkubectl replace実行時に

~~~ bash
Error from server (NotFound): the server could not find the requested resource
~~~
と怒られ、kubeletにつけないときは特にエラーを出さずにimage pull以降をサボタージュするという素敵な振る舞いをします。


# Pod起動

今回は以下の定義でnginxを起動しました。

~~~ yaml
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  shareProcessNamespace: true
  containers:
   - name: nginx-container
     image: nginx:latest
     ports:
      - containerPort: 80
~~~
shareProcessNamespaceってのが、Pod内のコンテナがプロセステーブルを共有する設定になります。

# エフェメラルコンテナの起動

以下のファイルを用意します。なんでこっちはjsonなんだと自分でも思いますがとりあえず気にせず進めましょう。

~~~ json
{
    "apiVersion": "v1",
    "kind": "EphemeralContainers",
    "metadata": {
            "name": "nginx"
    },
    "ephemeralContainers": [{
        "command": [
            "sh"
        ],
        "image": "ubuntu:latest",
        "imagePullPolicy": "IfNotPresent",
        "name": "debugger",
        "stdin": true,
        "tty": true,
        "terminationMessagePolicy": "File"
    }]
}
~~~
このファイルをkubectl replaceに食わせます。

~~~ bash
> kubectl replace --raw /api/v1/namespaces/default/pods/nginx/ephemeralcontainers -f ec.json
{"kind":"EphemeralContainers","apiVersion":"v1",
"metadata":
{"name":"nginx",
"namespace":"default",
"selfLink":"/api/v1/namespaces/default/pods/nginx/ephemeralcontainers",
"uid":"359938c9-6053-47af-96a1-8d2bc2be9948",
"resourceVersion":"606335",
"creationTimestamp":"2020-04-13T09:40:59Z"},
"ephemeralContainers":
[{"name":"debugger",
"image":"ubuntu:latest",
"command":["sh"],
"resources":{},
"terminationMessagePolicy":"File",
"imagePullPolicy":"IfNotPresent",
"stdin":true,
"tty":true}]}
~~~
(適当に改行してます)
podを確認してみると、

~~~ bash
> kubectl describe pods nginx
Name:         nginx
Namespace:    default
Priority:     0
Node:         xxx/192.168.1.13
Start Time:   Mon, 13 Apr 2020 18:40:59 +0900
Labels:       <none>
Annotations:  Status:  Running
IP:           10.1.2.7
IPs:
  IP:  10.1.2.7
Containers:
  nginx-container:
    Container ID:   containerd://d399885861114d018c8cf51d89ee5c8a45c8ef9ae948d494092b8a63e57d850c
    Image:          nginx:latest
    Image ID:       docker.io/library/nginx@sha256:282530fcb7cd19f3848c7b611043f82ae4be3781cb00105a1d593d7e6286b596
    Port:           80/TCP
    Host Port:      0/TCP
    State:          Running
      Started:      Mon, 13 Apr 2020 18:41:05 +0900
    Ready:          True
    Restart Count:  0
    Environment:    <none>
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from default-token-z6rfv (ro)
Ephemeral Containers:
  debugger:
    Container ID:  containerd://ab534051c891ea7daf9d1d6042dd8956acabb4021f79dacda3a4c3846c092377
    Image:         ubuntu:latest
    Image ID:      docker.io/library/ubuntu@sha256:bec5a2727be7fff3d308193cfde3491f8fba1a2ba392b7546b43a051853a341d
    Port:          <none>
    Host Port:     <none>
    Command:
      sh
    State:          Running
      Started:      Mon, 13 Apr 2020 18:41:14 +0900
    Ready:          False
    Restart Count:  0
<snip>
~~~

という感じで、エフェメラルコンテナの状態がState: Runningで表示されました。Ready: Falseというのが何事かと思いますが、公式のドキュメントを見ても同様でしたのでとりあえず気にしないことにします。


# エフェメラルコンテナの利用

kubectl attachを使ってシェルログインしてみます。

~~~ bash
> kubectl attach -it nginx -c debugger
If you dont see a command prompt, try pressing enter.

# ps aux
USER   PID %CPU %MEM    VSZ   RSS TTY    STAT START   TIME COMMAND
root     1  0.0  0.0   1020     4 ?      Ss   12:01   0:00 /pause
root     6  0.0  0.0  10628  5468 ?      Ss   12:01   0:00 nginx: master process nginx -g daemon off;
101     11  0.0  0.0  11084  2588 ?      S    12:01   0:00 nginx: worker process
root    12  0.0  0.0   4624  1604 pts/0  Ss   12:05   0:00 sh
root   617  0.0  0.0  18504  3464 pts/0  S    12:26   0:00 bash
root   624  0.0  0.0  34400  2912 pts/0  R+   12:27   0:00 ps aux

~~~
という感じで、psでお隣のコンテナのnginxプロセスを捕捉できます。
調子に乗ってサーバ側で

~~~ bash
# echo 0 > /proc/sys/kernel/yama/ptrace_scope
~~~
した上でdebugコンテナにgdbをインストールして動かしてみましたが、

~~~ bash
# gdb --pid 6
<snip>
Attaching to process 6

warning: "target:/usr/sbin/nginx": could not open as an executable file: Operation not permitted.
<snip>
~~~
と怒られてしまいました。記憶域は共有してないんでそりゃそうですね。nginxの実行環境をdebugコンテナに用意してやればよさそうですが完全に同一な環境を作るのは結構めんどくさそうですね。そうでもないかな？
gcoreでcoreを吐かせることもできました。

# プロセス状態
ホスト側からプロセス状態を見ると

~~~ bash
> pstree -ATp
(snip)
           |-containerd(14073)-+-containerd-shim(15655)---pause(15676)
(snip)
           |                   |-containerd-shim(22806)---nginx(22837)---nginx(22853)   (nginxコンテナ)
           |                   `-containerd-shim(29248)---sh(29265)                     (debugコンテナ)
~~~
という感じで、まぁ別コンテナなので当たり前といえば当たり前の状態でした。
同様にホスト側で/proc下を見たところ、

~~~ bash
# ll /proc/{15676,22837,29265}/ns/pid
lrwxrwxrwx 1 root root 0 Apr 13 21:22 /proc/15676/ns/pid -> 'pid:[4026532745]'
lrwxrwxrwx 1 root root 0 Apr 13 21:22 /proc/22837/ns/pid -> 'pid:[4026534072]'
lrwxrwxrwx 1 root root 0 Apr 13 21:22 /proc/29265/ns/pid -> 'pid:[4026534072]'
~~~
となっており、nginxコンテナとdebugコンテナで別コンテナでありながら同じプロセステーブルを見ていることがわかります。

というわけで簡単ですが以上です

# 追記(2020/11/20)

gdbつかって解析する場合は当然、gcore使ってのコア出力の場合でも、対象コンテナとエフェメラルコンテナのglibcバージョンは揃える必要があります。アプリごとに好きな実行環境(ベースイメージ)を使っていいというのがマイクロサービスのいいところだと思うのですが、あまりバラバラだと面倒ですね。対象コンテナと同一イメージつかってエフェメラルコンテナ作るのが手っ取り早いんでしょうね


参考：
https://kubernetes.io/docs/concepts/workloads/pods/ephemeral-containers/
https://kubernetes.io/ja/docs/tasks/configure-pod-container/share-process-namespace/