Dockerでデバッグ対象のコンテナにツールを入れずにtcpdump/straceなどを使うワンライナー

# はじめに
Dockerであんなコンテナやこんなコンテナを動かしてると、なんかうまく動かなくて、デバッグのためにtcpdumpとかstraceなどのツールが使いたくなることが稀によくあります。
そんな時、デバッグ対象のコンテナ内にツールを一時的にインストールしちゃうというのが、まぁ簡単で分かりやすいんですが、デバッグ対象のコンテナを汚すのはできれば避けたいところです。

Dockerのコンテナの分離というのは、結局のところLinuxのリソースの名前空間の分離であるので、逆に同じ名前空間を共有すれば、デバッグ用に立てた隣のコンテナから、デバッグ対象のコンテナのネットワークやプロセスの状態を観察することも可能です。

また、docker buildはDockerfileを標準入力から受け取ることもできるので、ワンライナーにしてデバッグ用のコンテナをシュッと呼び出せるようにしてみました。

# TL;DR

結論だけ知りたい人はこちらをどうぞ。
`<target>` のところはデバッグ対象のコンテナ名またはコンテナIDで読み替えて下さい。

tcpdump

```
echo 'FROM alpine\nRUN apk add --no-cache tcpdump' \
 | docker build -t debug -f - . \
 && docker run -it --rm --net container:<target> debug tcpdump -nn -X port 80
```

strace

```
$ echo 'FROM alpine\nRUN apk add --no-cache strace' \
| docker build -t debug -f - . \
&& docker run -it --rm --pid container:<target> --cap-add sys_ptrace debug strace -fp 1
```

ワンライナーでデバッグ用のツールをインストールして、デバッグ用のコンテナを立てる方法は、使いたいツールを変えればいろいろ応用が効くので、シェルのヒストリやスニペットに保存してシュッと呼び出すとよさそうです。

# 解説
## 環境

稼働確認した手元のDockerのバージョンは 19.03.0-rc2です。

```
$ docker version
Client: Docker Engine - Community
 Version:           19.03.0-rc2
 API version:       1.40
 Go version:        go1.12.5
 Git commit:        f97efcc
 Built:             Wed Jun  5 01:37:53 2019
 OS/Arch:           darwin/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          19.03.0-rc2
  API version:      1.40 (minimum version 1.12)
  Go version:       go1.12.5
  Git commit:       f97efcc
  Built:            Wed Jun  5 01:42:10 2019
  OS/Arch:          linux/amd64
  Experimental:     true
 containerd:
  Version:          v1.2.6
  GitCommit:        894b81a4b802e4eb2a91d1ce216b8817763c29fb
 runc:
  Version:          1.0.0-rc8
  GitCommit:        425e105d5a03fabd737a126ad93d62a9eeede87f
 docker-init:
  Version:          0.18.0
  GitCommit:        fec3683
```

## 事前準備

ここではデバッグ対象として、適当なnginxのコンテナをtargetという名前で起動しておきます。

```
$ docker run -it --rm -p 8080:80 --name target nginx:alpine
```

## tcpdumpを使ってみる


tcpdumpだけ入ったDockerfileを作って、debugというタグを付けてビルドします。

```
$ cat Dockerfile
FROM alpine
RUN apk add --no-cache tcpdump

$ docker build -t debug .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM alpine
 ---> 3f53bb00af94
Step 2/2 : RUN apk add --no-cache tcpdump
 ---> Using cache
 ---> a000cadec8f5
Successfully built a000cadec8f5
Successfully tagged debug:latest
```

`docker run --network container:<name|id>` で対象のコンテナのネットワークにアタッチできます。

https://docs.docker.com/engine/reference/run/#network-settings

デバッグ用のコンテナをtargetのコンテナのネットワークにアタッチしつつ、tcpdumpを実行します。

```
$ docker run -it --rm --network container:target debug tcpdump -nn -X port 80                           tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
```

試しに手元からcurlしてみると

```
$ curl http://localhost:8080/
```

tcpdumpでパケットキャプチャできてることが分かります。

```
15:01:16.352790 IP 172.17.0.1.58354 > 172.17.0.2.80: Flags [S], seq 178914825, win 29200, options [mss 1460,sackOK,TS val 13258980 ecr 0,nop,wscale 7], length 0
        0x0000:  4500 003c 56ed 4000 4006 8ba9 ac11 0001  E..<V.@.@.......
        0x0010:  ac11 0002 e3f2 0050 0aaa 0609 0000 0000  .......P........
        0x0020:  a002 7210 5854 0000 0204 05b4 0402 080a  ..r.XT..........
        0x0030:  00ca 50e4 0000 0000 0103 0307            ..P.........
15:01:16.352899 IP 172.17.0.2.80 > 172.17.0.1.58354: Flags [S.], seq 1290103227, ack 178914826, win 28960, options [mss 1460,sackOK,TS val 13258980 ecr 13258980,nop,wscale 7], length 0
        0x0000:  4500 003c 0000 4000 4006 e296 ac11 0002  E..<..@.@.......
        0x0010:  ac11 0001 0050 e3f2 4ce5 69bb 0aaa 060a  .....P..L.i.....
        0x0020:  a012 7120 5854 0000 0204 05b4 0402 080a  ..q.XT..........
        0x0030:  00ca 50e4 00ca 50e4 0103 0307            ..P...P.....
```

## staceを使ってみる

同様にstraceをインストールしたDockerfileを用意してビルドします。

```
$ cat Dockerfile
FROM alpine
RUN apk add --no-cache strace

$ docker build -t debug .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM alpine
 ---> 3f53bb00af94
Step 2/2 : RUN apk add --no-cache strace
 ---> Using cache
 ---> b357653376b3
Successfully built b357653376b3
Successfully tagged debug:latest
```

straceの場合は、 `docker run --pid container:<name|id>` で対象のコンテナのPID名前空間にアタッチします。

https://docs.docker.com/engine/reference/run/#pid-settings---pid

とりあえずpsを打ってみると、プロセスが見えてるのが分かります。

```
$ docker run -it --rm --pid container:target debug ps
PID   USER     TIME  COMMAND
    1 root      0:00 nginx: master process nginx -g daemon off;
    6 101       0:00 nginx: worker process
    7 101       0:00 nginx: worker process
    8 101       0:00 nginx: worker process
    9 101       0:00 nginx: worker process
   16 root      0:00 ps
```

ただstraceを起動してみると、権限エラーが出ます。

```
$ docker run -it --rm --pid container:target debug strace -fp 1
strace: attach: ptrace(PTRACE_SEIZE, 1): Operation not permitted
```

Dockerはデフォルトではいくつかの特権操作は許可されておらず、 straceには `docker run --cap-add sys_ptrace` が明示的に許可が必要です。

https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities

```
$ docker run -it --rm --pid container:target --cap-add sys_ptrace debug strace -fp 1
strace: Process 1 attached
rt_sigsuspend([], 8
```

こんどはうまくstraceが起動できました。ためしに `nginx -s reload` でプロセスにシグナルを送ってみましょう。

```
$ docker exec -it target nginx -s reload
2019/07/13 15:26:45 [notice] 87#87: signal process started
```

straceでシステムコールトレースができてることが分かります。

```
strace: Process 1 attached
rt_sigsuspend([], 8)                    = ? ERESTARTNOHAND (To be restarted if no handler)
--- SIGHUP {si_signo=SIGHUP, si_code=SI_USER, si_pid=87, si_uid=0} ---
clock_gettime(CLOCK_REALTIME, {tv_sec=1563031605, tv_nsec=559042300}) = 0
rt_sigreturn({mask=[HUP INT QUIT USR1 USR2 ALRM TERM CHLD WINCH IO]}) = -1 EINTR (Interrupted system call)
clock_gettime(CLOCK_REALTIME, {tv_sec=1563031605, tv_nsec=559133400}) = 0
clock_gettime(CLOCK_REALTIME, {tv_sec=1563031605, tv_nsec=559208800}) = 0
clock_gettime(CLOCK_REALTIME, {tv_sec=1563031605, tv_nsec=559260400}) = 0
uname({sysname="Linux", nodename="976678564bed", ...}) = 0
open("/etc/nginx/nginx.conf", O_RDONLY) = 3
```

## (参考) デバッグ用のイメージnicolaka/netshoot

毎回デバッグ用のツールをインストールするのがめんどくさければ、デバッグによく使うコマンドを全部入りしたnicolaka/netshootというDockerイメージがあります。

https://hub.docker.com/r/nicolaka/netshoot

何が入ってるかはDockerfileこれです。

https://github.com/nicolaka/netshoot/blob/master/Dockerfile

* apache2-utils
* bash
* bind-tools
* bird
* bridge-utils
* busybox-extras
* calicoctl
* conntrack-tools
* ctop
* curl
* dhcping
* drill
* ethtool
* file
* fping
* iftop
* iperf
* iproute2
* iptables
* iptraf-ng
* iputils
* ipvsadm
* libc6-compat
* liboping
* mtr
* net-snmp-tools
* netcat-openbsd
* netgen
* nftables
* ngrep
* nmap
* nmap-nping
* openssl
* py-crypto
* py2-virtualenv
* python2
* scapy
* socat
* strace
* tcpdump
* tcptraceroute
* util-linux
* vim

ちなみにREADMEに貼ってある、どのレイヤを調査するのにどのツールが必要かの画像の元ネタはこれのようです。まじすげーなこのサイト。

引用元: http://www.brendangregg.com/linuxperf.html

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61152/3482551f-e396-2434-3269-c96680420a63.png)

## ワンライナーを組み立てる
まぁ仕事で使ってる環境に公式イメージじゃない野良のイメージを使うのも若干抵抗があるので、自分がよく使うものだけ適当に入れたイメージを作って使うのがよいとは思います。

ただ個人的にはイメージのメンテすらあまりしたくないので、オンデマンドでビルドして使う技も思いつきました。
docker buildはDockerfileを標準入力から受け取ることもできるので、 tcpdumpの例をワンライナーで書くとこんなかんじです。

```
$ echo 'FROM alpine\nRUN apk add --no-cache tcpdump' \
 | docker build -t debug -f - . \
 && docker run -it --rm --net container:target debug tcpdump -nn -X port 80

Sending build context to Docker daemon  2.607kB
Step 1/2 : FROM alpine
 ---> 3f53bb00af94
Step 2/2 : RUN apk add --no-cache tcpdump
 ---> Using cache
 ---> a000cadec8f5
Successfully built a000cadec8f5
Successfully tagged debug:latest
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes

```

straceだとこんなかんじでしょうか。

```
$ echo 'FROM alpine\nRUN apk add --no-cache strace' \
| docker build -t debug -f - . \
&& docker run -it --rm --pid container:target --cap-add sys_ptrace debug strace -fp 1

Sending build context to Docker daemon  2.607kB
Step 1/2 : FROM alpine
 ---> 3f53bb00af94
Step 2/2 : RUN apk add --no-cache strace
 ---> Using cache
 ---> b357653376b3
Successfully built b357653376b3
Successfully tagged debug:latest
strace: Process 1 attached
rt_sigsuspend([], 8
```


# おわりに

ワンライナーでデバッグ用のツールをインストールして、デバッグ用のコンテナを立てる方法は、使いたいツールを変えればいろいろ応用が効くので、シェルのヒストリやスニペットに保存してシュッと呼び出すとよさそうです。

これであんなコンテナやこんなコンテナをデバッグし放題ですねー。