触って覚える runC

軽量コンテナを探して jailing を試したり droot を試したりしているのですが、上手く言えないけどやりたいことが出来なさそうな気がする。なら自分で作るのよ!やだ!!!という状態になっていて、なんかこう、コンテナ管理だけやってくれて、レイヤーとかないシンプルな何か……と探していたら runC を見つけたので、実用に耐えるのか、やりたいことは実現できるのかを見ていきます。

runC とは

runC

runC is a CLI tool for spawning and running containers according to the OCP specification. The code can be found on Github.

OCP / Open Container Project の仕様に則って開発されたコンテナランタイム。Docker では v1.11 からこのコンテナランタイムで稼働しているらしい?

(libcontainer が runC になったって認識であってるのかな? libnetwork は今だ元気に開発中)

もうちょい詳しいところが気になる人はこの辺の URL を漁るといいと思います。僕はユーザーとしてしか興味ないのでそこまで追っかけてない。

現在のバージョン

この記事を執筆している時点では runC は v1.0.0-rc2 でした。

なお、検証に使った VM は Ubuntu 16.04 です。

まずはコンテナを作って稼働させる

とくに悩むこともなく、さくっと Docker イメージから rootfs を作って試します。

$ sudo su -
$ mkdir /container
$ mkdir /container/alpine
$ cd /container/alpine
$ docker export $(docker create alpine:latest - ) | gzip -cq > rootfs.tar.gz
$ mkdir rootfs
$ tar -C rootfs -zxf rootfs.tar.gz

alpine イメージから作ってるのは、コンテナ環境であることを確認するため。特に他意はないです(小さくて展開が手早いとかはある)。

では早速動かすための config.json を用意しましょう。

$ runc spec
$ cat config.json

runc specconfig.json が生成されます。とりあえず内容を確認しましょう。

{
    "ociVersion": "1.0.0-rc2-dev",
    "platform": {
        "os": "linux",
        "arch": "amd64"
    },
    "process": {
        "terminal": true,
        "consoleSize": {
            "height": 0,
            "width": 0
        },
        "user": {
            "uid": 0,
            "gid": 0
        },
        "args": [
            "sh"
        ],
        "env": [
            "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
            "TERM=xterm"
        ],
        "cwd": "/",
        "capabilities": [
            "CAP_AUDIT_WRITE",
            "CAP_KILL",
            "CAP_NET_BIND_SERVICE"
        ],
        "rlimits": [
            {
                "type": "RLIMIT_NOFILE",
                "hard": 1024,
                "soft": 1024
            }
        ],
        "noNewPrivileges": true
    },
    "root": {
        "path": "rootfs",
        "readonly": true
    },
    "hostname": "runc",
    "mounts": [
        {
            "destination": "/proc",
            "type": "proc",
            "source": "proc"
        },
        {
            "destination": "/dev",
            "type": "tmpfs",
            "source": "tmpfs",
            "options": [
                "nosuid",
                "strictatime",
                "mode=755",
                "size=65536k"
            ]
        },
        {
            "destination": "/dev/pts",
            "type": "devpts",
            "source": "devpts",
            "options": [
                "nosuid",
                "noexec",
                "newinstance",
                "ptmxmode=0666",
                "mode=0620",
                "gid=5"
            ]
        },
        {
            "destination": "/dev/shm",
            "type": "tmpfs",
            "source": "shm",
            "options": [
                "nosuid",
                "noexec",
                "nodev",
                "mode=1777",
                "size=65536k"
            ]
        },
        {
            "destination": "/dev/mqueue",
            "type": "mqueue",
            "source": "mqueue",
            "options": [
                "nosuid",
                "noexec",
                "nodev"
            ]
        },
        {
            "destination": "/sys",
            "type": "sysfs",
            "source": "sysfs",
            "options": [
                "nosuid",
                "noexec",
                "nodev",
                "ro"
            ]
        },
        {
            "destination": "/sys/fs/cgroup",
            "type": "cgroup",
            "source": "cgroup",
            "options": [
                "nosuid",
                "noexec",
                "nodev",
                "relatime",
                "ro"
            ]
        }
    ],
    "hooks": {},
    "linux": {
        "resources": {
            "devices": [
                {
                    "allow": false,
                    "access": "rwm"
                }
            ]
        },
        "namespaces": [
            {
                "type": "pid"
            },
            {
                "type": "network"
            },
            {
                "type": "ipc"
            },
            {
                "type": "uts"
            },
            {
                "type": "mount"
            }
        ],
        "maskedPaths": [
            "/proc/kcore",
            "/proc/latency_stats",
            "/proc/timer_list",
            "/proc/timer_stats",
            "/proc/sched_debug",
            "/sys/firmware"
        ],
        "readonlyPaths": [
            "/proc/asound",
            "/proc/bus",
            "/proc/fs",
            "/proc/irq",
            "/proc/sys",
            "/proc/sysrq-trigger"
        ]
    }
}

ウッ、長い!もう無理!やだ!となって諦めたくなりますが、ひとつひとつはそんなに難しくないですから、落ち着いて挙動を確認していきます。とりあえずまだこのファイルをいじらずにコンテナを立ち上げます。

$ runc run alpine
/ #

runc run [container name] でコンテナが起動できます。かんたんらくちん!

起動したコンテナの中身を確認する

/ # cat /etc/alpine-release
3.4.4

お、おおー、 alpine だ! ruby でもインストールしてみましょう。

/ # apk --update add ruby
ERROR: Unable to lock database: Read-only file system
ERROR: Failed to open apk database: Read-only file system

おっと、そうでした。このコンテナ氏は書き込み権限をお持ちでない。

コンテナの設定を変更する

とりあえず一旦コンテナを終了して、 config.json をいじっていきます。

"root": {
  "path": "rootfs",
  "readonly": false # from true
},

まずは、 rootfs への書き込みを可能に。このまま実行すると

/ # apk --update add ruby
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/main/x86_64/APKINDEX.tar.gz
ERROR: http://dl-cdn.alpinelinux.org/alpine/v3.4/main: temporary error (try again later)
WARNING: Ignoring APKINDEX.167438ca.tar.gz: No such file or directory
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/community/x86_64/APKINDEX.tar.gz
ERROR: http://dl-cdn.alpinelinux.org/alpine/v3.4/community: temporary error (try again later)
WARNING: Ignoring APKINDEX.a2e6dac0.tar.gz: No such file or directory
ERROR: unsatisfiable constraints:
  ruby (missing):
    required by: world[ruby]

という具合に失敗します。ネットワークが制限されているので、コンテナの外へ出ることが出来ません。

ということで次は linux.namespaces にある "type": "network" を外して、ネットワークをホストのものと共用させます。

"linux": {
  "namespaces": [
    { # 削除
      "type": "network"
    }
  ]
}
/ # ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN qlen 1
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp0s3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 02:c2:b4:4c:cc:47 brd ff:ff:ff:ff:ff:ff
    inet 10.0.2.15/24 brd 10.0.2.255 scope global enp0s3
       valid_lft forever preferred_lft forever
    inet 192.168.33.50/24 brd 192.168.33.255 scope global enp0s3
       valid_lft forever preferred_lft forever
    inet6 fe80::c2:b4ff:fe4c:cc47/64 scope link
       valid_lft forever preferred_lft forever
3: enp0s8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
    link/ether 08:00:27:0f:80:9c brd ff:ff:ff:ff:ff:ff
    inet 192.168.33.50/24 brd 192.168.33.255 scope global enp0s8
       valid_lft forever preferred_lft forever
    inet6 fe80::a00:27ff:fe0f:809c/64 scope link
       valid_lft forever preferred_lft forever
4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN
    link/ether 02:42:a8:ad:0a:f9 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 scope global docker0
       valid_lft forever preferred_lft forever

ちゃんとホストのネットワークがみえるようになりました。が、まだ外と通信できません。 Linux capability が絞られているので、許可して上げる必要があります。

process.capabilities で設定できるので、今回はいくつか付与します。

"process": {
  "capabilities": [
    "CAP_AUDIT_WRITE",
    "CAP_KILL",
    "CAP_NET_BIND_SERVICE",
    # 以降追加
    "CAP_NET_RAW",
    "CAP_SETUID",
    "CAP_SETGID"
  ]
}

これで外と通信できるようになったはずです!いざ ruby インストール!

と思ったらまだつながりません……それもそのはず、 /etc/resolv.conf がないんです。ということでホストからコピーしてきます。

$ cp /etc/resolv.conf rootfs/etc/

今度こそ本当に準備完了です。

/ # apk --update add ruby
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/main/x86_64/APKINDEX.tar.gz
fetch http://dl-cdn.alpinelinux.org/alpine/v3.4/community/x86_64/APKINDEX.tar.gz
(1/10) Installing ncurses-terminfo-base (6.0-r7)
(2/10) Installing ncurses-terminfo (6.0-r7)
(3/10) Installing ncurses-libs (6.0-r7)
(4/10) Installing libedit (20150325.3.1-r3)
(5/10) Installing libffi (3.2.1-r2)
(6/10) Installing gdbm (1.11-r1)
(7/10) Installing gmp (6.1.0-r0)
(8/10) Installing yaml (0.1.6-r1)
(9/10) Installing ruby-libs (2.3.1-r0)
(10/10) Installing ruby (2.3.1-r0)
Executing busybox-1.24.2-r11.trigger
ERROR: busybox-1.24.2-r11.trigger: script exited with error 1
OK: 27 MiB in 21 packages
/ # ruby -v
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux-musl]

なんか busybox がエラー吐いてる……けど通りました! ruby インストール成功!

とりあえず動かすところまでは出来た

出来たのはいいんだけど、いくつか欲しい機能が実現できるかわからないので、引き続き調べていきます。が、とりあえず気力がつきたのでここまで……