Linux
docker
busybox

dockerでbusybox一個だけのファイルを含むイメージを作って動かしたらどうなるか

動機

dockerのようなコンテナでは通常のLinuxシステムではinitなどがやってくれる処理をdocker自体が面倒を見てくれる。実際にどこまでやってくれるのかを調べるために、busybox一個だけを含むイメージを作って動かして、どんなファイルが自動生成されているのか調べた。

使用したdockerは、Ubuntu 16.04 で sudo apt install docker.io でインストールしたもの。

$ docker version
Client:
 Version:      1.13.1
 API version:  1.26
 Go version:   go1.6.2
 Git commit:   092cba3
 Built:        Thu Nov  2 20:40:23 2017
 OS/Arch:      linux/amd64

Server:
 Version:      1.13.1
 API version:  1.26 (minimum version 1.12)
 Go version:   go1.6.2
 Git commit:   092cba3
 Built:        Thu Nov  2 20:40:23 2017
 OS/Arch:      linux/amd64
 Experimental: false

busyboxのビルド

libcとしてuclibcを使ってbusyboxをstatic linkで作成する。
これをする一番簡単な方法はbuildrootを使うこと。
ちなみに、glibcではうまくいかない。これについては後述。

docker のイメージの作成

~/work/amd64/busybox にあるファイルを1個だけ使ってdockerにイメージを作る場合。

$ mkdir /tmp/a
$ cd /tmp/a
$ cp ~/work/amd64/busybox .
$ file ./busybox 
./busybox: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped
$ tar cf - . | docker import - busyboxonly:latest
sha256:1b97070bb9fdf71d3f72777886f1b8a4ff38c77948d0a8452093142590e100c0

確認。

$ docker images |grep busyboxonly
busyboxonly         latest              1b97070bb9fd        12 seconds ago      797 kB

実行

docker run -it busyboxonly /busybox sh

イメージに入れたのは/busybox 一個だけなので、それ以外のファイルはdockerが自動生成したもの。
/dev, /etc, /proc, /sys のディレクトリが自動生成されている。

/ # /busybox ls -l
total 784
-rwxr-xr-x    1 1001     1002        797176 Apr 12 05:09 busybox
drwxr-xr-x    5 0        0              360 Apr 12 05:13 dev
drwxr-xr-x    2 0        0             4096 Apr 12 05:13 etc
dr-xr-xr-x  103 0        0                0 Apr 12 05:13 proc
dr-xr-xr-x   13 0        0                0 Apr 12 05:13 sys

自動でmountされているディレクトリは以下の通り。

/ # /busybox mount
none on / type aufs (rw,relatime,si=1c98a1c72fd2dcb8,dio,dirperm1)
proc on /proc type proc (rw,nosuid,nodev,noexec,relatime)
tmpfs on /dev type tmpfs (rw,nosuid,mode=755)
devpts on /dev/pts type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666)
sysfs on /sys type sysfs (ro,nosuid,nodev,noexec,relatime)
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,relatime,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (ro,nosuid,nodev,noexec,relatime,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (ro,nosuid,nodev,noexec,relatime,cpu,cpuacct)
cgroup on /sys/fs/cgroup/rdma type cgroup (ro,nosuid,nodev,noexec,relatime,rdma)
cgroup on /sys/fs/cgroup/perf_event type cgroup (ro,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/devices type cgroup (ro,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/blkio type cgroup (ro,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/freezer type cgroup (ro,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (ro,nosuid,nodev,noexec,relatime,net_cls,net_prio)
cgroup on /sys/fs/cgroup/memory type cgroup (ro,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/pids type cgroup (ro,nosuid,nodev,noexec,relatime,pids)
cgroup on /sys/fs/cgroup/cpuset type cgroup (ro,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (ro,nosuid,nodev,noexec,relatime,hugetlb)
mqueue on /dev/mqueue type mqueue (rw,nosuid,nodev,noexec,relatime)
/dev/sda1 on /etc/resolv.conf type ext4 (rw,relatime,data=ordered)
/dev/sda1 on /etc/hostname type ext4 (rw,relatime,data=ordered)
/dev/sda1 on /etc/hosts type ext4 (rw,relatime,data=ordered)
shm on /dev/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=65536k)
devpts on /dev/console type devpts (rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
proc on /proc/bus type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/fs type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/irq type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/sys type proc (ro,nosuid,nodev,noexec,relatime)
proc on /proc/sysrq-trigger type proc (ro,nosuid,nodev,noexec,relatime)
tmpfs on /proc/kcore type tmpfs (rw,nosuid,mode=755)
tmpfs on /proc/timer_list type tmpfs (rw,nosuid,mode=755)
tmpfs on /proc/sched_debug type tmpfs (rw,nosuid,mode=755)
tmpfs on /sys/firmware type tmpfs (ro,relatime)

/etcの4つのファイルが自動生成されている。

/ # /busybox ls -l /etc
total 12
-rw-r--r--    1 0        0               13 Apr 12 05:13 hostname
-rw-r--r--    1 0        0              174 Apr 12 05:13 hosts
lrwxrwxrwx    1 0        0               12 Apr 12 05:13 mtab -> /proc/mounts
-rw-r--r--    1 0        0              237 Apr 12 05:13 resolv.conf
/ # 

現在時刻は設定されている。(1970年1月1日とかではない)

/ # /busybox date
Thu Apr 12 05:18:50 UTC 2018

ネットワークは使えるし、ホスト名も解決できるようになっている。

/ # /busybox ping -c 5 twitter.com
PING twitter.com (104.244.42.193): 56 data bytes
64 bytes from 104.244.42.193: seq=0 ttl=53 time=35.088 ms
64 bytes from 104.244.42.193: seq=1 ttl=53 time=34.797 ms
64 bytes from 104.244.42.193: seq=2 ttl=53 time=34.845 ms
64 bytes from 104.244.42.193: seq=3 ttl=53 time=34.876 ms
64 bytes from 104.244.42.193: seq=4 ttl=53 time=34.925 ms

--- twitter.com ping statistics ---
5 packets transmitted, 5 packets received, 0% packet loss
round-trip min/avg/max = 34.797/34.906/35.088 ms

まとめ

dockerでは以下のことは自動で行われる。

  • /dev, /proc, /sys はカーネルの生成するファイルシステムにマウントされる。
    • /tmp は作成されないので注意。/var も。
  • ネットワーク利用可能になっている。/etcの設定ファイルが自動生成される。
    • /etc/hostname, /etc/hosts, /etc/resolv.conf
  • 現在時刻は設定されている。(UTCのみ)
    • ローカルタイムや、タイムゾーンの情報は無し。

glibc を使ったbusybox ではダメだった理由

最初は先日の記事でビルドしたbusyboxを使った。これだとビルドしている環境のlibc、つまりglibcがリンクされる。
これで試したが、ネットワークのホスト名の解決ができない。
その理由は、glibcはstatic link したとしてもホスト名の解決に関してはlibnss_files.so, libnss_dns.so などをダイナミックにロードして使用しているため。つまりbusybox単体では足りないということ。

(root権限で)

# strace -o /tmp/strace.log ./busybox ping twitter.com 
# grep open /tmp/strace.log 
open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
open("/etc/host.conf", O_RDONLY|O_CLOEXEC) = 3
open("/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 3
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib/x86_64-linux-gnu/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/etc/hosts", O_RDONLY|O_CLOEXEC)  = 3
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib/x86_64-linux-gnu/libnss_dns.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib/x86_64-linux-gnu/libresolv.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/etc/resolv.conf", O_RDONLY|O_CLOEXEC) = 3
open("/etc/gai.conf", O_RDONLY|O_CLOEXEC) = 3

ついでの話。pingコマンドにはSUIDがついている。

ビルドしたばかりのbusyboxを使って通常ユーザーの権限で ping を実行するとpermission denied のエラーになる。

$ ls -l ./busybox
-rwxrwxr-x 1 koba koba 2630040 Apr 12 01:17 ./busybox
koba@instance-1:~/work/busybox/busybox-1.28.3$ ./busybox ping twitter.com
PING twitter.com (104.244.42.193): 56 data bytes
ping: permission denied (are you root?)

実は通常のping コマンドにはSUIDビットが立っていて、通常ユーザが実行してもルート権限で実行されるようになっている。

$ ls -l /bin/ping
-rwsr-xr-x 1 root root 44168 May  7  2014 /bin/ping

これにならって、busyboxにもSUIDビットを立ててみる。

$ sudo chown root:root ./busybox
$ sudo chmod u+s ./busybox
$ ls -l ./busybox
-rwsrwxr-x 1 root root 2630040 Apr 12 01:17 ./busybox

今度はpermission deniedにならずに実行できる。

$ ./busybox ping twitter.com
PING twitter.com (104.244.42.129): 56 data bytes
64 bytes from 104.244.42.129: seq=0 ttl=54 time=35.598 ms
64 bytes from 104.244.42.129: seq=1 ttl=54 time=34.738 ms
64 bytes from 104.244.42.129: seq=2 ttl=54 time=34.703 ms
64 bytes from 104.244.42.129: seq=3 ttl=54 time=34.739 ms
^C
--- twitter.com ping statistics ---
4 packets transmitted, 4 packets received, 0% packet loss
round-trip min/avg/max = 34.703/34.944/35.598 ms