Edited at

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


ワンライナーを組み立てる

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

ただ個人的にはイメージのメンテすらあまりしたくないので、オンデマンドでビルドして使う技も思いつきました。

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


おわりに

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

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