WIP!WIP!
Docker 1.11になってバックエンドがrunC(docker-runc)になったので、drootのように素朴に運用できないか検討してみました。
検証環境のVagrantfileはつぎのとおりです。
developmentとproductionの2つのホストを作成します。 development環境ではdocker利用してコンテナイメージを作成、production環境では作成したコンテナイメージをrunCで実行します。
# -*- mode: ruby -*-
# vi: set ft=ruby :
RUNC_VERSION = "v0.1.1"
Vagrant.configure(2) do |config|
config.vm.box = "boxcutter/centos72"
config.vm.define "development" do |development|
development.vm.hostname = "development"
development.vm.provision "shell", inline: <<-SHELL
sudo yum install -y epel-release
# dockerをインストール
sudo yum install -y docker-io
# vagrantユーザーでdockerコマンドを実行できるようにグループを設定
sudo groupadd docker
sudo gpasswd -a vagrant docker
# dockerを起動
sudo systemctl enable docker
sudo systemctl restart docker
SHELL
end
config.vm.define "production" do |production|
production.vm.hostname = "production"
production.vm.provision "shell", inline: <<-SHELL
sudo yum install -y epel-release
sudo yum install -y lxc libseccomp-devel golang git # runCビルドのためのツール/ライブラリ一式をインストール
# runcをインストール(/usr/local/sbin/runc)
sudo git clone https://github.com/opencontainers/runc.git /usr/local/src/runc
sudo /bin/sh -c '
cd /usr/local/src/runc &&
git fetch &&
git checkout tags/#{RUNC_VERSION} &&
export GOPAHT=$HOME &&
make &&
make install
'
SHELL
end
end
vagrant up
で検証環境を構築します。
$ vagrant up
$ vagrant ssh development
コンテナイメージを/vagrant/image_alpine.tar.gz
に作成します。
development$ docker pull alpine
development$ docker export $(docker create alpine /bin/sh) | gzip > /vagrant/image_alpine.tar.gz # alpineのイメージはCOMMANDが指定されていないので`/bin/sh`を指定する
development$ exit
$ vagrant ssh production
production$ sudo mkdir -p /var/container/alpine/rootfs
production$ sudo tar xzfv /vagrant/image_alipne.tar.gz -C /var/container/alpine/rootfs
production$ cd /var/container/alpine
production$ sudo /usr/local/sbin/runc spec
production$ sudo /usr/local/sbin/runc start alpine # コンテナ内でshが実行される
/ # ls
bin/ etc/ lib/ media/ proc/ run/ sys/ usr/
dev/ home/ linuxrc@ mnt/ root/ sbin/ tmp/ var/
dockerデーモンのような中央集権なデーモンいらずで実行できます。
production$ pa auxfww
...
root 3770 0.0 0.5 193432 2772 pts/0 S+ 06:27 0:00 | \_ sudo /usr/local/sbin/runc start alpine
root 3771 0.3 0.9 198028 4676 pts/0 Sl+ 06:27 0:00 | \_ /usr/local/sbin/runc start alpine
root 3778 0.0 0.0 1512 256 pts/1 Ss+ 06:27 0:00 | \_ sh
...
コンテナの挙動を定義した設定ファイル。
docker 1.11.0
からバックエンドがrunCになったため、コンテナ起動時に/var/run/docker/libcontainerd/<CONTAINER_ID>/config.json
生成される。
(以前の/var/lib/docker/containers/<CONTAINER_ID>/config.json
とは違うので注意)
config.jsonの仕様はこちら。
runc spec
でconfig.json
生成し、これを編集してコンテナの挙動を定義する。
runc start
でコンテナ内で実行されるコマンドはprocess.args
で定義する(例ではsh
コマンドを実行する)。
また、ターミナルにアタッチする場合はprocess.terminal
、環境変数はprocess.env
、カレントディレクトリはproces.cwd
で定義する。
"process": {
...
"args": [
"sh"
],
...
デフォルトではroot.readonly
はtrueなのでコンテナ内のプロセスはファイルシステムに書き込むことができない。書き込みを行う場合はfalseする。
"root": {
"path": "rootfs",
"readonly": false
},
プロセスの動作にケーパビリティ(capabilities(7))が不足している場合はprocess.capabilities
を修正する。
"process": {
...
"capabilities": [
"CAP_AUDIT_WRITE"
"CAP_KILL",
"CAP_NET_BIND_SERVICE",
"CAP_NET_RAW" # pingを使いたい場合は必要
]
ちなみにDocker 1.11におけるコンテナのデフォルトケーパビリティは下記のものとなります。
- CAP_CHOWN
- CAP_DAC_OVERRIDE
- CAP_FSETID
- CAP_FOWNER
- CAP_MKNOD
- CAP_NET_RAW
- CAP_SETGID
- CAP_SETUID
- CAP_SETFCAP
- CAP_SETPCAP
- CAP_NET_BIND_SERVICE
- CAP_SYS_CHROOT
- CAP_KILL
- CAP_AUDIT_WRITE
またデフォルトではnetwork namespaceが作成されるためコンテナ内で独立したネットワークになってしまい、外->内、内->外への通信を行うことはできない。
コンテナ内でホストのネットワークを直接利用する(docker run --net=host
と同等)場合はnetwork namespaceを無効にし、ホストの/etc/hosts
や/etc/resolv.conf
などをコンテナ内で参照できるよう定義する。
具体的にはconfig.jsonを下記のように修正してコンテナを起動する。
-
linux.namespaces
からtype
がnetwork
の要素を削除する -
/etc/hosts
,/etc/resolv.conf
,/etc/hostname
をマウントするようmounts
に追記する(dockerではこれらのファイルをテンポラリ領域にコピーして、それらをマウントしている)"mounts": [ ... { "destination": "/etc/resolv.conf", "type": "bind", "source": "/etc/resolv.conf", "options": [ "rbind", "rprivate", "ro" ] }, { "destination": "/etc/hostname", "type": "bind", "source": "/etc/hostname", "options": [ "rbind", "rprivate", "ro" ] }, { "destination": "/etc/hosts", "type": "bind", "source": "/etc/hosts", "options": [ "rbind", "rprivate", "ro" ] } ],
runc spec
で生成されるconfig.json
はこちら。
{
"ociVersion": "0.5.0",
"platform": {
"os": "linux",
"arch": "amd64"
},
"process": {
"terminal": true,
"user": {},
"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_stats",
"/proc/sched_debug"
],
"readonlyPaths": [
"/proc/asound",
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
}
}
いくつかのサブコマンドを解説します。
config.json
を生成します。
$ runc spec
config.json
を元にコンテナを起動します。
デフォルトではカレントディレクトリのconfig.json
を元にコンテナを起動します。
$ cd /path/to/container/myapp
$ ls -F
config.json rootfs/
$ sudo runc run --detach myapp # コンテナ内からデタッチ
実行中のコンテナ一覧やステータスを表示します。
list
では--fromat json
でJSONで取得できます。
稼働中のコンテナの情報は/run/runc/<CONTAINER_ID>/state.json
に格納されています。
$ sudo runc list
ID PID STATUS BUNDLE CREATED
alpine 10958 running /var/container/alpine 2016-04-15T06:15:54.433918254Z
alpine_2 11043 running /var/container/alpine 2016-04-15T06:17:48.369802219Z
# cat /run/runc/alpine/state.json | jq -C . | less -R
{
"id": "alpine",
"init_process_pid": 3963,
"init_process_start": "272024",
"created": "2016-09-26T06:48:57.070792327Z",
...
...
...
}
$ sudo runc state alpine
{
"ociVersion": "0.5.0",
"id": "alpine",
"pid": 10958,
"bundlePath": "/var/container/alpine",
"rootfsPath": "/var/container/alpine/rootfs",
"status": "running",
"created": "2016-04-15T06:15:54.433918254Z"
}
起動中のコンテナ内で新しくコマンドを実行します。
docker exec
と同等。
$ sudo runc exec alpine sh # 稼働中のaplineコンテナで`sh`を実行
/ # ps
PID USER TIME COMMAND
1 root 0:00 sh # コンテナのinitプロセス(`sh`)
15 root 0:00 sh # execで実行した`sh`プロセス
19 root 0:00 ps # 現在実行した`ps`プロセス
コンテナのinitプロセスにシグナル(デフォルトではSIGTERM)を送ります。
$ sudo runc kill alpine KILL
ちなみにinit(PID1)プロセスに送ることができるシグナルは、initが明示的にシグナルハンドラを設定したシグナルだけとなる
(alpineコンテナのinitプロセスが/sbin/sh
の場合はTERMシグナルはハンドルされない)ので注意が必要。
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/kill.2.html
ステータスがdestroyed
なコンテナのリソースを削除します。
主にでタッチされたコンテナに利用します。
$ sudo runc delete myapp