はじめに
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>
で対象のコンテナのネットワークにアタッチできます。
デバッグ用のコンテナを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名前空間にアタッチします。
とりあえず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
が明示的に許可が必要です。
$ 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イメージがあります。
何が入ってるかは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
ワンライナーを組み立てる
まぁ仕事で使ってる環境に公式イメージじゃない野良のイメージを使うのも若干抵抗があるので、自分がよく使うものだけ適当に入れたイメージを作って使うのがよいとは思います。
ただ個人的にはイメージのメンテすらあまりしたくないので、オンデマンドでビルドして使う技も思いつきました。
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
おわりに
ワンライナーでデバッグ用のツールをインストールして、デバッグ用のコンテナを立てる方法は、使いたいツールを変えればいろいろ応用が効くので、シェルのヒストリやスニペットに保存してシュッと呼び出すとよさそうです。
これであんなコンテナやこんなコンテナをデバッグし放題ですねー。