Kubernetes: アプリケーションのデバッグ方法 (kubectl exec など)

# はじめに

Kubernetes 上のアプリケーションに対して、curl や tcpdump など使い慣れたツールを使ってデバッグを行いたいと思う場合があるかと思います。`kubectl exec` を利用するとコンテナ内のコマンドを実行することができ、従来 ssh で行っていたデバッグに近いことが可能になります。一方、コンテナには必要最低限のものしか含めないことがベストプラクティスとなっているため、使いたいコマンドが含まれていないこともあるでしょう。

本記事では、`kubectl exec` を主としたデバッグの方法と、コンテナに使いたいコマンドが含まれていない場合や `kubectl exec` が利用できない場合の対応方法などについて説明します。確認は Kubernetes v1.8 で行い、コンテナランタイムは Docker を前提としています。


# kubectl exec を使ったデバッグ

`kubectl exec` を使うと動作中の Pod に対してコンテナ内の任意のコマンドを実行することができます。シェルを使うと ssh のようにインタラクティブに操作可能なため、デバッグの際に非常に便利です。 ただしコンテナ内に含まれていないコマンドは利用できません。この場合の対応方法については後述します。なお、本記事では開発環境でのデバッグを想定しています。本番稼働中の Pod の操作は可能ですが、危険を伴うため充分な注意が必要です。


まずはデバッグしたい Pod の名前を調べます。

```bash
$ kubectl get pods
NAME                     READY     STATUS    RESTARTS   AGE
myapp-7b77b8d9c6-m4jmr   1/1       Running   0          55m
#^^^^^^^^^^^^^^^^^^^^^ Pod 名
```

Pod に対して `kubectl exec` でコマンドを実行します。コマンドがオプションを取る際は `--` を使って kubectl 自体のオプションと区別する必要があるので注意してください。

```bash
# Pod 上 で "ls /" を実行
$ kubectl exec myapp-7b77b8d9c6-m4jmr ls /
bin
boot
dev
# ... 省略

# Pod 内に複数コンテナがある場合は -c でコンテナ名を指定する
$ kubectl exec -it myapp-7b77b8d9c6-m4jmr -c myapp ls

# オプションを使いたい場合は -- を置いてからコマンドを続ける
$ kubectl exec myapp-7b77b8d9c6-m4jmr -- ls -l
total 64
drwxr-xr-x   2 root root 4096 Dec 10 00:00 bin
drwxr-xr-x   2 root root 4096 Nov 19 15:25 boot
drwxr-xr-x   5 root root  360 Jan 15 01:49 dev
```

## インタラクティブな操作

以下のよう`-i`, `-t` オプションを指定し、シェルを実行することでインタラクティブに操作が可能です。

```bash
# -it オプションを指定。イメージに含まれていれば sh など他のシェルでも問題ない
$ kubectl exec -it myapp-7b77b8d9c6-m4jmr bash
# 以下はコンテナ内のシェル

# コンテナ内の ls の結果
root@myapp-7b77b8d9c6-m4jmr:/# ls
bin  boot  dev	etc  home  lib	lib64  media  mnt  opt	proc  root  run  sbin  srv  sys  tmp  usr  var

# コンテナ内にあるコマンドならどれでも利用可能
root@myapp-7b77b8d9c6-m4jmr:/# uname -a
Linux myapp-7b77b8d9c6-m4jmr4.9.13 #1 SMP Thu Oct 19 17:14:00 UTC 2017 x86_64 GNU/Linux
```

## コンテナに含まれていないツールを使う

コンテナイメージには[必要最低限のものしか含めないことがベストプラクティス](https://docs.docker.com/engine/userguide/eng-image/dockerfile_best-practices/#avoid-installing-unnecessary-packages)となっているため、使いたいコマンドがコンテナイメージに含まれていないことも多いでしょう。ここでは以下 3 つの方法を紹介します。状況に応じて使い分けるのが良いでしょう。

方法 | Mount *1 | Network *2 | PID *3 | 備考
--- | --- | --- | --- | ---
方法1: Pod にパッケージをインストールする | :white_check_mark: | :white_check_mark: | :white_check_mark: | 一番手軽。イメージにパッケージマネージャーが必要
方法2: デバッグ用イメージを作る | :white_check_mark: | :white_check_mark: | :white_check_mark: | 開発中に便利。本番環境とイメージが分かれてしまう。
方法3: デバッグ用サイドカーを入れる | - |  :white_check_mark: |  *4 | イメージにパッケージマネージャーが無くても利用できる
docker run で名前空間を共有して実行 (後述) | - | :white_check_mark: | :white_check_mark: | kubectl exec を使わない方法。ノードに ssh する必要がある

- *1: Mount はマウントの名前空間。コンテナ上のファイルシステムが見れる
- *2: Network のネットワークの名前空間。ローカルからのアクセスやtcpdump などができる
- *3: PID の PID の名前空間。strace ができる (権限の設定が必要)
- *4: Pod のコンテナ間で PID 名前空間が共有されるかは kubelet の `--docker-disable-shared-pid` の設定次第
  - kubernetes v1.7 では Docker のバージョンが 1.13.1 以降の場合デフォルト有効
  - kubernetes v1.8 ではデフォルトで無効になり、`--docker-disable-shared-pid` を false に設定すれば共有される

 
### 方法1: Pod にパッケージをインストールする

コンテナイメージのベース OS がパッケージマネージャーを持つ場合は、コマンドを直接インストールして利用することが可能です。scratch イメージをベースとしている場合などはこの方法は取れないので、方法2, 3 を使う必要があります。

以下の例では kubectl exec でシェルを実行し、ベース OS を確認してからパッケージマネージャーでデバッグ用ツール (tcpdump) をインストールしています。

```bash
$ kubectl exec -it myapp-7b77b8d9c6-jxxlm /bin/bash
# 以下はコンテナのシェル

# ベース OS が debian であることがわかる
root@myapp-7b77b8d9c6-jxxlm:/# cat /etc/debian_version
9.3

# apt-get でデバッグ用ツール (ここでは tcpdump) をインストール
root@myapp-7b77b8d9c6-jxxlm:/# apt-get update && apt-get install -y tcpdump
```

引き続きコンテナのシェルでインストールした tcpdump を実行してみます。

```bash
# tcpdump を実行
root@myapp-7b77b8d9c6-jxxlm:/# tcpdump -i any -X
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
...
05:28:40.535986 IP localhost.46490 > localhost.80: Flags [P.], seq 1:530, ack 1, win 342, options [nop,nop,TS val 417807
947 ecr 417807946], length 529: HTTP: GET / HTTP/1.1
        0x0000:  4500 0245 83c6 4000 4006 b6ea 7f00 0001  E..E..@.@.......
        0x0010:  7f00 0001 b59a 0050 2869 bd8f c108 48d6  .......P(i....H.
        0x0020:  8018 0156 003a 0000 0101 080a 18e7 3e4b  ...V.:........>K
        0x0030:  18e7 3e4a 4745 5420 2f20 4854 5450 2f31  ..>JGET./.HTTP/1
        0x0040:  2e31 0d0a 486f 7374 3a20 6c6f 6361 6c68  .1..Host:.localh
        0x0050:  6f73 743a 3830 3830 0d0a 436f 6e6e 6563  ost:8080..Connec
        0x0060:  7469 6f6e 3a20 6b65 6570 2d61 6c69 7665  tion:.keep-alive
```

パッケージマネージャーが使えればこの方法が一番お手軽かと思います。この方法は Pod の不変性を破ってしまうので、デバッグが終わったら対象の Pod は削除しておくのが良いでしょう。

### 方法2: デバッグ用イメージを作る

開発の最中などで、デバッグ用のツールを頻繁に利用する場合は、デバッグ用のツールを入れたコンテナイメージを作るのも選択肢の一つです。インストールが不要なこと以外は「パッケージをインストールする方法」とかわりません。

例えば [istio](https://istio.io/) というプロダクトでは[リリース用のイメージ](https://github.com/istio/proxy/blob/master/docker/Dockerfile.release)の他に、[デバッグ用のイメージ](https://github.com/istio/proxy/blob/master/docker/Dockerfile.debug)が用意されています。istio のデバッグ用イメージには tcpdump や netcat, curl といったツール群がインストールされています。

既存のコンテナイメージであれば、以下のように元のイメージを FROM にして、RUN や COPY で必要なデバッグツールを入れてデバッグ用イメージを作ることができます。

```dockerfile:Dockerfile
# 元のイメージ
FROM nginx:1.13

# デバッグに必要なパッケージをインストール
RUN apt-get update && apt-get install -y curl tcpdump && rm -rf /var/lib/apt/lists/*
```

### 方法3: デバッグ用サイドカーを入れる

scratch をベースとしたコンテナイメージのように、シェルやパッケージマネージャーが含まれていない場合は、サイドカーとしてデバッグ用のコンテナをデプロイする形になります。ただし、マウント名前空間と設定によっては PID 名前空間が異なるため、コンテナ内のファイルが見れなかったり、ps や strace でプロセスの状況を見れなかったりと、前述の 2 つの方法より制限があります。

まずは対象の Pod にサイドカーを入れます。デバッグに用いるコンテナイメージは任意ですが、個人的には Alpine Linux が軽量で気に入っています。以下は myapp という Deployment の場合を kubectl edit を使って直接書き換える例です。

```bash
$ kubectl edit deploy myapp
# エディタで編集
```
```yaml
    # ...
    spec:
      containers:
      - image: nginx:1.13
        # ...
      # 以下を追加
      - image: alpine:3.7
        name: debug
        # 終了しないように無限に sleep させておく
        command: ["/bin/ash", "-c", "while true; do sleep 3600; done"]
```

このデバッグ用コンテナイメージに kubectl exec してデバッグを行います。

```bash
# -c でデバッグ用のコンテナ名を指定
$ kubectl exec -it myapp-74999d7bd5-zg8cw -c debug /bin/sh
# 以下はコンテナ内のシェル

# tcpdump をインストール
$ apk add --update tcpdump

# tcpdump を実行
$ tcpdump -i any -X
```

ネットワークの名前空間は Pod 内で共有されているため tcpdump が可能です。

## Tips: 標準出力を活用する

kubectl exec は標準出力に出力されるので、パイプで繋いでローカルのツールと組み合わせることができます。例えば以下のように tcpdump をインストール・実行して、ローカルの wireshark でリアルタイムに結果を確認するといったことが可能です。

```bash
$ kubectl exec myapp-58d7c45686-gl4rk -- sh -c "apt-get update > /dev/null && apt-get install -y tcpdump > /dev/null && tcpdump -i any -w -" | wireshark -k -i -
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100377/125c7125-5ecf-78af-83e7-dd0e80b03e13.png)

## Tips: strace を使いたい場合

システムコールをトレースできる strace はデバッグ時に非常に便利です。kubectl exec で strace を利用したい場合は、Pod のマニフェストに [Linux Capabilities](http://man7.org/linux/man-pages/man7/capabilities.7.html) の `CAP_SYS_PTRACE` を設定する必要があるのでご注意ください

```bash
# 直接 deployment に設定した例
$ kubectl edit deploy myapp
```

```yaml
# エディタで編集
      containers:
      - image: nginx:1.13
        imagePullPolicy: IfNotPresent
        name: myapp
        resources: {}
        # 以下を追加
        securityContext:
          capabilities:
            add:
            - SYS_PTRACE
```

Capbilities の設定ができたら、kubectl exec で strace をインストールしトレースを行うことができます:tada:


```bash
$ kubectl exec -it myapp-7b77b8d9c6-jxxlm /bin/bash
# 以下はコンテナのシェル

# strace と procsps (ps コマンド用) をインストール
root@myapp-7b77b8d9c6-jxxlm:/# apt-get install -y strace procps

# trace したいプロセスの PID を確認 (ここでは nginx: worker process)
root@myapp-7b77b8d9c6-jxxlm:/# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  32428  1164 ?        Ss   05:22   0:00 nginx: master process nginx -g daemon off;
nginx        5  0.0  0.0  32900  1064 ?        S    05:22   0:00 nginx: worker process
root        10  0.0  0.1  18140  2424 pts/0    Ss   05:24   0:00 /bin/bash
root       640  0.0  0.1  36636  2688 pts/0    R+   05:33   0:00 ps aux

# strace で nginx の worker の動きをトレース
root@myapp-58d7c45686-968b2:/# strace -p 5 -e trace=network
strace: Process 5 attached
accept4(6, {sa_family=AF_INET, sin_port=htons(58482), sin_addr=inet_addr("127.0.0.1")}, [112->16], SOCK_NONBLOCK) = 3
recvfrom(3, "GET / HTTP/1.1\r\nHost: localhost:"..., 1024, 0, NULL, NULL) = 465
sendfile(3, 11, [0] => [612], 612)      = 612
setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
```

# その他のサブコマンド

`kubectl exec` 以外にもデバッグに役立つサブコマンドがあるので紹介します。

- `kubectl logs` コンテナのログを確認する
- `kubectl port-forward` Pod のコンテナ内に対してポートフォワードを行う
- `kubectl cp` Pod のコンテナ内のファイルを手元に転送する。
- `kubectl run` Pod を作成する簡易コマンド

## kubectl logs
`kubectl logs` はコンテナが標準出力、標準エラーに出すログを見ることができる機能です。ログの保持期間はコンテナランタイム(例えば Docker) の設定次第です。デフォルトでは保存されているログを全て表示するため、`--since` オプションで表示する期間(例えば 1m だと直近 1分)を指定して直近のログに絞り込むのがオススメです。

```bash
# ログを全部表示して終了する
$ kubectl logs myapp-7b77b8d9c6-jxxlm
127.0.0.1 - - [15/Jan/2018:05:27:27 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" "-"
127.0.0.1 - - [15/Jan/2018:05:27:27 +0000] "GET /favicon.ico HTTP/1.1" 404 571 "http://localhost:8080/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" "-"

# -f オプションで tail -f のようにログを follow できる
$ kubectl logs -f myapp-7b77b8d9c6-jxxlm

# --since オプションを指定すると指定した時間内のログから表示
$ kubectl logs -f --since 1m myapp-7b77b8d9c6-jxxlm

# --timestamps オプションを指定するとログの時間が表示される
$ kubectl logs --timestamps myapp-7b77b8d9c6-jxxlm
2018-01-15T05:27:27.216783197Z 127.0.0.1 - - [15/Jan/2018:05:27:27 +0000] "GET / HTTP/1.1" 200 612 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" "-"
2018-01-15T05:27:27.322968956Z 127.0.0.1 - - [15/Jan/2018:05:27:27 +0000] "GET /favicon.ico HTTP/1.1" 404 571 "http://localhost:8080/" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36" "-"
```

外部のツールでは [stern](https://github.com/wercker/stern) という kubectl logs より便利にログを見ることができるツールがあります。複数の Pod のログをまとめてみたり、Pod を正規表現で絞込むなど便利な機能が実装されています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100377/ff66dc05-cff1-e955-df67-ff32001f75ce.png)


## kubectl port-forward

kubectl port-forward を使うと Pod のコンテナ内に対してポートフォワードを行う。ローカルのみ listen しているポートにもアクセスできます。Pod 内のエンドポイントにアクセスして動作を確認したいような場合は port-forward だけで解決するかもしれません。

```bash
# ローカルの 8080 に Pod の 80 ポートをフォーワードする
$ kubectl port-forward myapp-7b77b8d9c6-m4jmr 8080:80

# 別のターミナルで実行
# Pod の 80 ポートにアクセスできる
$ curl http://localhost:8080/
```

## kubectl cp
`kubectl cp` を使うと Pod のコンテナ内のファイルを手元に転送することができます。

```bash
# コンテナ上のファイルをローカルの /tmp/ にコピーする
$ kubectl cp myapp-7b77b8d9c6-m4jmr:/usr/share/nginx/html/index.html /tmp/

# ファイルがローカルにコピーされた
$ head /tmp/index.html
<!DOCTYPE html>
<html>
<head>
```

なお、`kubectl exec` で cat の結果をリダイレクトする形でもコピーは可能です。`kubectl cp` はコンテナイメージに tar コマンドが必要なため、含まれていない場合は以下のように cat でリダイレクトする方法を利用すると良いでしょう。

```bash
# cat をリダイレクトしてファイルをコピーする
$ kubectl exec myapp-7b77b8d9c6-m4jmr -- cat /usr/share/nginx/html/index.html > /tmp/index.html

# ファイルがローカルにコピーされた
$ head /tmp/index.html
<!DOCTYPE html>
<html>
<head>
```

## kubectl run

クラスタ内から Service を経由したアクセスを確認したい場合などは、kubectl run でテンポラリな Pod を作成して確認するのが便利です。下記のように `-it` を付けて実行するとインタラクティブに操作ができます。コンテナイメージは任意です。

```bash
$ kubectl run -it debug --image=alpine:3.7 --rm --restart=Never -- /bin/sh
# 以下はコンテナ内のシェル

# curl をインストール
$ apk add --update curl

# service 経由で curl でアクセス
$ curl http://nginx/
<!DOCTYPE html>
<html>
....
```

# kubectl exec が利用できない場合
`kubectl exec` は非常に強力な機能であるため、本番環境では禁止されている場合もあるかもしれません。ここでは `kubectl exec` を使わず、ノードに ssh して docker コマンドを使ってデバッグする方法を紹介します。

## Pod の動いているノードとコンテナ ID を探す

Pod の動いているノードは Pod の `.status.hostIP` というフィールドに入っています。また、コンテナ ID は `.status.containerStatuses.containerID` に入っています。

```bash
$ kubectl get pods myapp -o yaml | less
```

```yaml
status:
  # ...
  containerStatuses:
  # デバッグしたいコンテナの containerID
  - containerID: docker://b20ebfa080acf8107403d52e3a74b4763780f5e53d7e0164609193ba3364fd4e
    image: nginx:1.13
    # ...
  # Pod の動いているノードの IP
  hostIP: 192.168.99.100
```

## そのノードに ssh する

hostIP で表示された Pod の動いているノードに ssh します。

```
$ ssh 192.168.99.100
```

以降は ssh したノードで行うデバッグの方法です。

## 方法1: docker exec でコンテナ内のコマンドを実行

`kubectl exec` と同等な、`docker exec` でそのコンテナイメージ内のコマンドを実行することができます。

```bash
# コンテナID を指定する
$ CONTAINER=b20ebfa080acf8107403d52e3a74b4763780f5e53d7e0164609193ba3364fd4e
$ docker exec $CONTAINER ls
bin
boot
dev
```

`-it` オプションでインタラクティブな操作も可能です。

```bash
$ CONTAINER=b20ebfa080acf8107403d52e3a74b4763780f5e53d7e0164609193ba3364fd4e
$ docker exec -it $CONTAINER /bin/sh
# 以下はコンテナ内のシェル

$ ls
bin  boot  dev	etc  home  lib	lib64  media  mnt  opt	proc  root  run  sbin  srv  sys  tmp  usr  var

$ uname -a
Linux myapp-74999d7bd5-zg8cw 4.9.13 #1 SMP Thu Oct 19 17:14:00 UTC 2017 x86_64 GNU/Linux
```

## 方法2: docker run で名前空間を共有して実行

docker run で対象のコンテナの名前空間(PID, Network) を共有したコンテナを立ち上げて、デバッグする方法もあります。この方法だと対象のコンテナにシェルやコマンドがない場合でも柔軟に対応できます。起動するコンテナイメージは任意です。

```bash
$ CONTAINER=b20ebfa080acf8107403d52e3a74b4763780f5e53d7e0164609193ba3364fd4e
# PID, Network 名前空間を共有して起動。 --privileged は必須ではない
$ docker run -it --privileged --pid container:$CONTAINER --network container:$CONTAINER alpine:3.7
```

立ち上げたコンテナのシェルを使ってデバッグを行うことができます。

```bash
# 以下は alpine 内のシェル

# 対象のコンテナのプロセスが見れる
$ ps aux
PID   USER     TIME   COMMAND
    1 root       0:00 nginx: master process nginx -g daemon off;
    7 101        0:00 nginx: worker process
   75 root       0:00 /bin/sh
   79 root       0:00 ps aux

# デバッグ用に tcpdump, strace をインストールする
$ apk add --update tcpdump strace

# strace 実行
$ strace -p 7 -e trace=network

accept4(6, {sa_family=AF_INET, sin_port=htons(44524), sin_addr=inet_addr("172.17.0.10")}, [112->16], SOCK_NONBLOCK) = 3
recvfrom(3, "GET / HTTP/1.1\r\nHost: myapp\r\nUse"..., 1024, 0, NULL, NULL) = 69

# tcpdump 実行
$ tcpdump -i any -X

10:55:43.914779 IP 172.17.0.10.44632 > myapp-74999d7bd5-zg8cw.80: Flags [S], seq 24373974, win 29200, options [mss 1460,sackOK,TS val 2698787 ecr 0,nop,wscale 7], length 0
	0x0000:  4500 003c c4f5 4000 4006 1d92 ac11 000a  E..<..@.@.......
	0x0010:  ac11 0008 ae58 0050 0173 ead6 0000 0000  .....X.P.s......
	0x0020:  a002 7210 5863 0000 0204 05b4 0402 080a  ..r.Xc..........
	0x0030:  0029 2e23 0000 0000 0103 0307            .).#........
```

# 補足: kubectl debug

`kubectl debug` という機能が [Debug Containers #277](https://github.com/kubernetes/features/issues/277) という issue で扱われています。この機能が実装されればより簡単にデバッグが可能になるかもしれません。興味があれば [proposal](https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/troubleshoot-running-pods.md) のドキュメントをご覧ください。

# まとめ

`kubectl exec` を使うと従来 ssh で行っていたようなデバッグが可能です。コマンドがコンテナイメージに含まれていない場合には以下の方法が考えられます。

- 方法1: Pod にパッケージをインストールする
- 方法2: デバッグ用イメージを作る
- 方法3: デバッグ用サイドカーを入れる方法

デバッグに便利な他のサブコマンドとして以下のコマンドがあります。

- `kubectl logs` コンテナのログを確認する
- `kubectl port-forward` Pod のコンテナ内に対してポートフォワードを行う
- `kubectl cp` Pod のコンテナ内のファイルを手元に転送する。
- `kubectl run` Pod を作成する簡易コマンド

kubectl exec が使えない場合には、ノードに ssh してデバッグする方法があります。

- 方法1: docker exec でコンテナ内のコマンドを実行
- 方法2: docker run で名前空間を共有して実行

本記事が Kubernetes でデバッグする際の参考になれば幸いです。