TenForward

技術ブログ。はてなダイアリーから移転しました

Linux 4.3 の Process Number Controller (1)

連載書いたり、勉強会で発表したりしているとなかなかブログが更新できませんね。久々の更新です。

これを書いている時点では Linux カーネルの 4.3-rc1 がリリースされていますが、久々に cgroup に新しいコントローラが追加されそうですね。

説明が不要なほどシンプルで分かりやすいです。このブログエントリ読まなくても使えるはず。:-)

軽く使ってみました。大体は上記ドキュメントに沿って試してるだけです。

カーネル

カーネルの config では Cgroup 関連の設定以下に "PIDs cgroup subsystem" という項目が新設されているので、ここを有効にしてカーネルを作成するだけです。

General setup  --->
  Control Group support  --->
    [*] PIDs cgroup subsystem

起動すると /proc/cgroups に pids が現れます。

# cat /proc/cgroups 
#subsys_name	hierarchy	num_cgroups	enabled
cpuset	1	1	1
cpu	2	1	1
cpuacct	3	1	1
blkio	4	1	1
memory	5	1	1
devices	6	1	1
freezer	7	1	1
net_cls	8	1	1
perf_event	9	1	1
net_prio	10	1	1
hugetlb	11	1	1
pids	12	2	1
debug	13	1	1

マウント

まずはマウントしてみましょう。単一階層構造でなく、現在の仕様の cgroup でも使えるようです。

マウントは他のコントローラと同じです。例えば

# mkdir -p /sys/fs/cgroup/pids
# mount -t cgroup -o pids none /sys/fs/cgroup/pids

なかにはどんなファイルがあるかな? ルートは以下のような感じでした。

# ls /sys/fs/cgroup/pids/
cgroup.clone_children  cgroup.sane_behavior  pids.current   tasks
cgroup.procs           notify_on_release     release_agent

"pids.current" が PIDs サブシステム独自のファイルのようですね。

cgroup の作成

グループを作成してみます。ディレクトリを作ることに変わりはありません。

# mkdir /sys/fs/cgroup/pids/test01
# ls /sys/fs/cgroup/pids/test01
cgroup.clone_children  notify_on_release  pids.max
cgroup.procs           pids.current       tasks

"pids.current" と "pids.max" という 2 つのファイルがこのコントローラの独自のファイルのようですね。

制限の設定

ファイル名を見ただけで大体わかると思いますが、それぞれのファイルの役割は以下です。

pids.current
現在のプロセス(タスク)数
pids.max
許可するプロセス(タスク)数の最大値

先に作った "test01" の作成直後、つまり初期値は以下のようになっています。

# cat /sys/fs/cgroup/pids/test01/pids.current 
0
# cat /sys/fs/cgroup/pids/test01/pids.max     
max

"tasks" ファイルに何も追加していないので、当然現在値は 0 です。制限値なしにするには "pids.max" に "max" と書くようで、これが初期値です。

制限値を 2 にしてみましょう。

# echo 2 > /sys/fs/cgroup/pids/test01/pids.max
# cat /sys/fs/cgroup/pids/test01/pids.max 
2

きちんと設定されました。

現在のシェルをグループに追加します。

# echo $$ | tee /sys/fs/cgroup/pids/test01/tasks
5600
# cat /sys/fs/cgroup/pids/test01/pids.current 
2

作成後に現在値を見ると、追加したシェルと実行した cat コマンドで 2 となっていますね。では、ここで 2 つ同時にコマンドを実行してみましょう。

# ( /bin/echo "Here's some processes for you." | cat )
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: Resource temporarily unavailable
Terminated

シェルで 1 消費している状態で 2 つ実行しようとしたので、プロセスを生成できませんでした。

当たり前ですが、ちゃんと制限が効いていますね。

階層構造

上記の "test01" の子グループとして "test02" を作り、"test01" には制限をかけ、"test02" には制限をかけない状態にしてみます。

# mkdir /sys/fs/cgroup/pids/test01/test02
# echo 2 > /sys/fs/cgroup/pids/test01/pids.max 
# cat /sys/fs/cgroup/pids/test01/pids.max        (test01の制限値は2)
2
# cat /sys/fs/cgroup/pids/test01/test02/pids.max (test02は無制限)
max

以下のような設定です。

/sys/fs/cgroup/pids/
└── test01      --> (pids.max = 2)
    └── test02  --> (pids.max = max)

この状態で "test02" で "test01" の制限を超えるプロセスを起動してみます。

# echo $$ > /sys/fs/cgroup/pids/test01/test02/tasks (1 つ追加)
#  ( /bin/echo "Here's some processes for you." | cat )
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: retry: No child processes
bash: fork: Resource temporarily unavailable
Terminated

起動しませんね。つまり複数の階層がある場合は、上位の階層の制限にひとつでも引っかかればダメということです。

制限を設定した状態でグループのタスクを増やす

"pids.max" を設定した状態で、グループの "tasks" にたくさんプロセスを追加していったらどうなるでしょう?

# echo 2 > /sys/fs/cgroup/pids/test01/pids.max 
# cat /sys/fs/cgroup/pids/test01/pids.max 
2

"test01" に 2 の制限を設定しました。別途起動してあるプロセスを追加していってみましょう。

# echo 5954 > /sys/fs/cgroup/pids/test01/tasks 
# echo 5955 > /sys/fs/cgroup/pids/test01/tasks 
# echo 5956 > /sys/fs/cgroup/pids/test01/tasks
# 

おや、特にエラーにならずに、追加できてしまっています。

# cat /sys/fs/cgroup/pids/test01/tasks        (追加したタスクは全部存在する)
5954
5955
5956
# cat /sys/fs/cgroup/pids/test01/pids.max     (制限値は2に設定されている)
2
# cat /sys/fs/cgroup/pids/test01/pids.current (現在値は3で、制限値より多い)
3

つまり fork() や clone() で新しいプロセスを起動しようとする時にだけ制限が効くということですね。

存在するプロセス数以下の制限値を設定する

では、プロセスが既にグループに存在する状態で、その数以下に "pids.max" を設定してみます。

# echo 2 > /sys/fs/cgroup/pids/test01/pids.max (制限値は2)
# cat /sys/fs/cgroup/pids/test01/tasks 
5968
5970
# cat /sys/fs/cgroup/pids/test01/pids.current  (現在2つのタスクが存在)
2

制限値以下であるふたつのタスクが存在していますが、ここで制限値を 1 に減らしてみましょう。

# echo 1 > /sys/fs/cgroup/pids/test01/pids.max (制限値を1に下げる)
# cat /sys/fs/cgroup/pids/test01/tasks 
5968
5970
# cat /sys/fs/cgroup/pids/test01/pids.current  (タスクは2つのまま)
2

タスクは 2 つのままですね。

まとめ

Linux 4.3-rc1 環境で PIDs コントローラを試してみました。

  • 制限値を設定しておくと、グループ内で fork() や clone() で新たに起動するプロセスを制限できる
  • グループの "tasks" にプロセスを追加したり、制限値を変更したりしても、制限にひっかからない。つまり "pids.max" < "pids.current" となることもある
  • グループに属することのできるタスク数は、そのグループの上位(祖先)のグループの制限を受ける

つづくかも...

シェルスクリプトで書かれた軽量コンテナ MINCS がすばらしい (2)

これはだいぶ前に書いたエントリです。MINCS作者による最新の解説があるのでそちらもご覧ください。 (2016-11-21追記)

先に書いた シェルスクリプトで書かれた軽量コンテナ MINCS がすばらしい (1) - TenForwardの日記 は私もびっくりの、このブログを書き始めて以来のはてぶ数に到達しました。MINCS がすばらしいからですね :-)

こないだ書いてからも MINCS はかなり活発に開発が進んでおり、かなり変化していますね。とりあえずもう少しデフォルトの動きを見てみようと思っておっかけたメモです (このエントリはあまり参考にならない気がします ^^;)。

minc コマンドの主要な処理を行っているのは libexec にインストールされる minc-exec です。

minc-exec は最後の行で以下のように unshare コマンドを実行しています。

$IP_NETNS unshare -iumpf $0 $@

つまり unshare の引数に自身 (minc-exec) を指定しており、自身の中に以下のように新しい Namespace で起動したときの処理を記述しています。

if [ $$ -eq 1 ]; then
  :
fi

この中の処理を少し追いましょう。

マウントの伝播を private に設定

systemd は mount propagation を shared にマークしてしまうので、せっかく新しい Mount Namespace を作成しても、Mount Namespace 内のマウントが他の Namespace に伝播してしまいます。これではコンテナにならないので、まずはこれを private にしています。

mount --make-rprivate /

overlayfs マウント

次に libexec/minc-coat を呼び、overlayfs でマウントを行います。minc-coat の該当部分を見ると、

mount -t overlay -o upperdir=$UD,lowerdir=$BASEDIR,workdir=$WD overlayfs $RD

こんな感じにマウントしています。コンテナ用のディレクトリはあらかじめ mktemp -d /tmp/minc$$-XXXXXX のように作られており、この下に upperdir, workdir, コンテナ用root を作成します。

特にオプションを指定せずに実行すると以下のようなマウントを実行します (minc1149-aEYmn0 は mktemp -d /tmp/minc$$-XXXXXX で作成したディレクトリ)。

mount -t overlay -o upperdir=/tmp/minc1149-aEYmnO/storage,lowerdir=/,workdir=/tmp/minc1149-aEYmnO/work overlayfs /tmp/minc1149-aEYmnO/root

つまり

  • overlayfs の upperdir は "/tmp/minc$$-XXXXXX/storage"
  • overlayfs の lowerdir は /
  • overlayfs の workdir は "/tmp/minc$$-XXXXXX/work"
  • overlayfs をマウントするディレクトリは "/tmp/minc$$-XXXXXX/root"

という感じになります。

ホスト名の設定

コンテナ名が指定されていたり、コンテナ内にコンテナ名を指定するファイルがあれば設定します。もちろん UTS Namespace が作成されているので、ホストには影響ありません。

  if [ "$MINC_UTSNAME" ]; then
    hostname $MINC_UTSNAME
    echo $MINC_UTSNAME > $MINC_TMPDIR/utsname
  elif [ -f $MINC_TMPDIR/utsname ]; then
    hostname `cat $MINC_TMPDIR/utsname`
  fi

/dev

"--usedev" オプションを指定しなければコンテナ内の /dev (/tmp/minc$$-XXXXXX/root/dev) は tmpfs でマウントされます。

そしてコンテナの /dev/pts はコンテナ専用に独立した /dev/pts としてマウントされます。ホストの /dev/pts を bind mount したりするとホストとコンテナの devpts が共通化されてしまい、コンテナから出力したらホストに出たりしてまずいので。この辺りはカーネル付属の filesytems/devpts.txt をどうぞ。

mkdir $RD/dev/pts
mount devpts -t devpts -onoexec,nosuid,gid=5,mode=0620,newinstance,ptmxmode=0666 $RD/dev/pts
ln -s /dev/pts/ptmx $RD/dev/ptmx

あとは適当に必要なデバイスファイルを bind mount します。

/proc

この時点ではホストの /proc は見えていますので、これを読み込み専用でマウントします。そしてコンテナ用の /proc をマウントします。以下のような処理を行っています。

mount -t proc -o ro,nosuid,nodev,noexec proc /proc
mount -t proc -o rw,nosuid,nodev,noexec,relatime proc $RD/proc

以上を実行する前の状態だと /proc は以下のような状態になっており、ホストのプロセスの状態が見えています (プロセスがたくさんあることからわかります)。

# ls -1F /proc
1/
10/
1019/
11/
12/
1200/
1201/
125/
13/
132/
134/
135/
137/
139/
  :()

処理後は /proc も以下のようになります。この例は、処理途中の様子を見るためにbashを実行しているので PID 1 の minc-exec の他に bash と ls のプロセスが存在しています。

# ls -1F  /proc
1/
36/
50/
  :()

この後に /proc 以下のセキュリティ的にヤバそうなファイルや /sys を bind mount しています。さきほどの 2 行の proc のマウントで /proc は読み込み専用でマウントしていましたので、bind mount するとこれらは読み込み専用になります。うまく考えられてますね。:-)

/ の変更

この後いよいよ pivot_root で / を変更します。

cd $RD
mkdir -p .orig
pivot_root . .orig
grepumount -e "^/\.orig/"

"pivot_root . .orig" で /tmp/minc$$-XXXXXX/root を新しい / にして、元を .orig にマウントします。この pivot_root 直後の状態は .orig 以下のものがたくさんマウントされた状態です。

# cat /proc/mounts 
/dev/mapper/vivid--vg-root /.orig ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
udev /.orig/dev devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
devpts /.orig/dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 0 0
tmpfs /.orig/dev/shm tmpfs rw,nosuid,nodev 0 0
hugetlbfs /.orig/dev/hugepages hugetlbfs rw,relatime 0 0
mqueue /.orig/dev/mqueue mqueue rw,relatime 0 0
tmpfs /.orig/run tmpfs rw,nosuid,noexec,relatime,size=101696k,mode=755 0 0
  :()
sysfs /.orig/sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
securityfs /.orig/sys/kernel/security securityfs rw,nosuid,nodev,noexec,relatime 0 0
tmpfs /.orig/sys/fs/cgroup tmpfs ro,nosuid,nodev,noexec,mode=755 0 0
cgroup /.orig/sys/fs/cgroup/systemd cgroup rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd 0 0
  :()
pstore /.orig/sys/fs/pstore pstore rw,nosuid,nodev,noexec,relatime 0 0
fusectl /.orig/sys/fs/fuse/connections fusectl rw,relatime 0 0
debugfs /.orig/sys/kernel/debug debugfs rw,relatime 0 0
proc /.orig/proc proc rw,nosuid,nodev,noexec,relatime 0 0
systemd-1 /.orig/proc/sys/fs/binfmt_misc autofs rw,relatime,fd=35,pgrp=0,timeout=300,minproto=5,maxproto=5,direct 0 0
/dev/vda1 /.orig/boot ext2 rw,relatime 0 0
overlayfs / overlay rw,relatime,lowerdir=/,upperdir=/tmp/minc1718-shaYgx/storage,workdir=/tmp/minc1718-shaYgx/work 0 0
tmpfs /dev tmpfs rw,relatime 0 0
devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0
udev /dev/console devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/null devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/zero devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
mqueue /dev/mqueue mqueue rw,relatime 0 0
proc /.orig/proc proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/sysrq-trigger proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/irq proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/bus proc ro,nosuid,nodev,noexec,relatime 0 0
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0

この状態で .orig をアンマウントしたいところですが、実はそれは以下のように一部失敗します。overlayfs の lowerdir を元の / にしているからですね。

# cut -f 2 -d " " < /proc/mounts | grep -e "^/\.orig/" | sort -r | xargs umount
umount: /.orig/proc/sys/fs/binfmt_misc: not mounted
umount: /.orig/proc: target is busy
        (In some cases useful info about processes that
         use the device is found by lsof(8) or fuser(1).)
# cat /proc/mounts
/dev/mapper/vivid--vg-root /.orig ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
proc /.orig/proc proc rw,nosuid,nodev,noexec,relatime 0 0
systemd-1 /.orig/proc/sys/fs/binfmt_misc autofs rw,relatime,fd=35,pgrp=0,timeout=300,minproto=5,maxproto=5,direct 0 0
overlayfs / overlay rw,relatime,lowerdir=/,upperdir=/tmp/minc1718-shaYgx/storage,workdir=/tmp/minc1718-shaYgx/work 0 0
tmpfs /dev tmpfs rw,relatime 0 0
devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0
udev /dev/console devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/null devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/zero devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
mqueue /dev/mqueue mqueue rw,relatime 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/sysrq-trigger proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/irq proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/bus proc ro,nosuid,nodev,noexec,relatime 0 0
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0

/.orig とかマウントされたままです。umount はできませんでしたので、.orig 内に移動して再度 pivot_root します。

cd /.orig/
pivot_root . dev/

この2度目の pivot_root 直後はこんなです。

# cat /proc/mounts 
/dev/mapper/vivid--vg-root / ext4 rw,relatime,errors=remount-ro,data=ordered 0 0
  :()
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0
  :()
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
  :()
/dev/vda1 /boot ext2 rw,relatime 0 0
overlayfs /dev overlay rw,relatime,lowerdir=/,upperdir=/tmp/minc1814-ESDwdP/storage,workdir=/tmp/minc1814-ESDwdP/work 0 0
tmpfs /dev/dev tmpfs rw,relatime 0 0
devpts /dev/dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0
udev /dev/dev/console devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/dev/null devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/dev/zero devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
mqueue /dev/dev/mqueue mqueue rw,relatime 0 0
proc /proc proc ro,nosuid,nodev,noexec,relatime 0 0
proc /dev/proc proc rw,nosuid,nodev,noexec,relatime 0 0
proc /dev/proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0
proc /dev/proc/sysrq-trigger proc ro,nosuid,nodev,noexec,relatime 0 0
proc /dev/proc/irq proc ro,nosuid,nodev,noexec,relatime 0 0
proc /dev/proc/bus proc ro,nosuid,nodev,noexec,relatime 0 0
sysfs /dev/sys sysfs rw,nosuid,nodev,noexec,relatime 0 0

この状態で /dev は元の / がマウントされていて、他に余計なものが見えない状態ですので chroot /dev して、ここを / にします。

RD=dev/
  :
exec $MINC_DEBUG_PREFIX chroot $OPT $RD $@

これで minc コマンドで指定されたコマンドが Namespace 内でコンテナ用の新しい / 以下で実行されますね。

と、ほとんど私が複数回 pivot_root 行われているけど、それぞれどういう意味だ? とおっかけた個人メモ的なわかりづらいエントリになってしまいました。

minc --debug /bin/bash

とやって実際どういうコマンドが実行されているのか観察したり、実際にやってることをおっかけたりすると良いと思いますよ。

aufs を使った一般ユーザ権限で起動するコンテナ

LXC ではコンテナのクローンを行う際に色々なストレージバックエンドの特徴を生かしたスナップショットクローンを行えます。この辺りは 連載の第 19 回〜 22 回 辺りで詳しく解説しています。

今まで、非特権LXCコンテナでストレージバックエンドの特徴を生かしたスナップショットクローンは、btrfs か overlayfs でしか行えませんでした。

先日、私の送ったパッチで aufs を使ったスナップショットクローンが一般ユーザ権限でもできるようになりました。

前者は liblxc 側の変更で lxc-clone や lxc-start 側で関係するパッチ、後者は lxc-start-ephemeral コマンドに関係するパッチです。

これで一般ユーザでも aufs を使った非特権クローンとコンテナが起動できるようなりました。簡単に紹介しておきます。

Plamo 5.3.1 に aufs パッチを当てた 4.1.1 カーネルを使っています。

$ lsb_release -d
Description:	Plamo Linux release 5.3.1
$ uname -r
4.1.1-plamo64-aufs

最近の aufs では allow_userns というモジュールオプションを Y にすると User Namespace 内の特権ユーザが aufs をマウントできるようになります。マニュアルにも記載があります。

私も手元では以下のように設定しています。

$ cat /etc/modprobe.d/aufs.conf 
options aufs allow_userns=1

こんな一般ユーザで操作しています。

$ id
uid=1000(karma) gid=100(users) groups=100(users),26(audio),28(dialout),29(video),32(cdrom),36(kvm),38(pulse),39(pulse-access),44(mlocate),47(libvirt),60(docker),1000(sudo)

まず、普通に dir バックエンドを使ったコンテナを作成します。以下がその config の rootfs の設定。

$ grep lxc.rootfs ~/.local/share/lxc/ct01/config 
lxc.rootfs = /home/karma/.local/share/lxc/ct01/rootfs

このコンテナのクローンを作成します。

$ lxc-clone -o ct01 -n aufs01 -s -B aufs
Created container aufs01 as snapshot of ct01
$ lxc-ls -f
NAME    STATE    IPV4  IPV6  GROUPS  AUTOSTART  
----------------------------------------------
aufs01  STOPPED  -     -     -       NO         
ct01    STOPPED  -     -     -       NO         

クローンは成功しています。

$ grep lxc.rootfs ~/.local/share/lxc/aufs01/config
lxc.rootfs = aufs:/home/karma/.local/share/lxc/ct01/rootfs:/home/karma/.local/share/lxc/aufs01/delta0

こんな感じに aufs を使うコンテナのルートファイルシステムの定義がされています。

起動してみます。Plamo では cgmanager とか systemd-logind とかないので、起動前には自分で一般ユーザ権限の cgroup を作成して、現在のシェルの PID を登録してから起動しています。

$ lxc-start -n aufs01 -d
$ lxc-ls -f
NAME    STATE    IPV4          IPV6  GROUPS  AUTOSTART  
------------------------------------------------------
aufs01  RUNNING  10.0.100.179  -     -       NO         
ct01    STOPPED  -             -     -       NO         

無事起動しましたね。

なぜ今 aufs ?

以上が動きと機能の紹介でした。でも、overlayfs がカーネルにマージされた今、なぜ aufs なのか? ってところですが...

色々なファイルシステムを扱う場合は特権が必要となります。例え User Namespace を使った Namespace 内の特権であっても、ファイルシステムをマウントできなかったりします。

User Namespace 内の特権ユーザがファイルシステムをマウントするには、カーネル内でファイルシステムのフラグに "FS_USERNS_MOUNT" というフラグが指定されている必要があります。(参考: overlayfs と LXC 非特権コンテナの snapshot によるクローン - TenForwardの日記)

このフラグ、4.1.1 カーネルで指定されているファイルシステムを調べてみると

$ find fs/ -type f | xargs grep FS_USERNS_MOUNT
fs/devpts/inode.c:	.fs_flags	= FS_USERNS_MOUNT | FS_USERNS_DEV_MOUNT,
fs/proc/root.c:	.fs_flags	= FS_USERNS_MOUNT,
fs/ramfs/inode.c:	.fs_flags	= FS_USERNS_MOUNT,
fs/sysfs/mount.c:	.fs_flags	= FS_USERNS_MOUNT,
fs/namespace.c:		if (!(type->fs_flags & FS_USERNS_MOUNT)) {

最後の (fs/namespace.c) は関係ないので devpts, proc, ramfs, sysfs だけです。この辺りはマウントできないとそもそも非特権のシステムコンテナが起動できなかったりするので指定されていて当然という気がしますね。つまり普通のファイルシステムはこれがそもそも指定されていないわけです。

overlayfs もこのフラグは指定されていません。なのに LXC で非特権 overlayfs がサポートされているのはなぜか? という話ですが、これは Ubuntuカーネルにパッチが当たっているからです。パッチがあたっていないバニラカーネルだと当然これは失敗します。

$ lxc-clone -o ct01 -n overlay01 -s -B overlayfs
clone failed

私の使っている 4.1.1 の overlayfs には特に何の変更も加えていないので、このように失敗します。

一方、aufs はカーネルにはマージされていないものの、最新の aufs が使える環境では素の aufs で User Namespace 内の特権ユーザが aufs をマウントできるわけです。この辺りからでしょうか。

まあ、パッチの量の大小の差はあるにせよ、overlayfs も aufs もバニラカーネルにパッチを当てないと非特権コンテナでは使えないわけで、なら aufs の非特権コンテナサポートを追加することで少しでも非特権コンテナのサポートする範囲が広がれば良いかなと思ってパッチを作りました。

あ、ちなみに Ubuntuカーネルに当たっている aufs は古いバージョンなので、allow_userns は使えない模様です。Ubuntu は aufs 止めちゃうみたいですものね。

ま、LXC の overlayfs 周りとかは以前もパッチ送ったことがあってコードをよく知ってたので、非特権 aufs 対応はさほど難しくないって分かってたから作ったんですけどね。(^_^;)

シェルスクリプトで書かれた軽量コンテナ MINCS がすばらしい (1)

これはだいぶ前に書いたエントリです。MINCS作者による最新の解説があるのでそちらもご覧ください。 (2016-11-21追記)

コンテナは使いたいけど、たくさんコンテナを起動すると結局それぞれのコンテナに対するセキュリティアップデートなどのメンテナンスは必要だし、コンテナ内独自のプログラムやライブラリ以外はホストと共有したいよね、って話が出てきたりします。みんな考えることは同じで、bind mount を使えば良いよね、って話はでてきてました。

この LinuxCon のセッション中に、ホストと色々共有しつつ (*) 簡単にコンテナ環境を作成できる、シェルスクリプトで書かれたコンテナ実装を知りました。これが @mhiramat さんの MINCS です。セッション中に早速軽く見て、これは素晴らしいってことで帰ってから少し調べたりしていました。

(*) ホストと共有しないこともできそうです。

どうやってコンテナを作っているか

MINCS がコンテナを作成する方法のキモは以下の 3 つかと思います。

  • unshare コマンドと ip netns コマンドで Namespace を作成
  • overlayfs の下層側 (lowerdir) にホストの / (ルート) を使い、コンテナディレクトリをマウント
  • pivot_root でコンテナの / に移動

なのでキモの部分だけ抜き出すと

mount -t overlayfs ...
pivot_root ...
ip netns unshare -iumpf ...

の 3 行になってしまうシンプルさ(*)!! これだけでも素晴らしいのですが、これだけではコンテナ環境はできないので、それまでに色々細かい処理をやっています。その処理がシェルスクリプトなので簡単に追っていけるので、それも素晴らしいです。

(*) 実際は pivot_root を複数回行って chroot します。

軽くおためし

MINCS を使える環境は比較的新しい環境になります。それは、unshare コマンド (util-linux パッケージに入っています) でキチンと Namespace を作るには比較的新しい環境が必要なのと、カーネルが overlayfs をサポートしている必要があるためです (Ubuntu であれば 12.04 辺りからパッチ適用で overlayfs が使えます。ただし util-linux は古いです)。unshare は README.md にもあるように 2.24 以上が必要です。

(話はそれますが unshare コマンドについて少し:MINCS では使ってませんが、User Namespace をきちんと使おうとすると 2.26 が必要そうです。2.24 -> 2.25 間に便利なオプションが追加されてますが、2.26 にならないとちゃんと動きません --map-root-user。2.24 で追加されている --mount-proc も便利そう。)

私は Ubuntu 15.04 上で試しました。

インストールは git clone して、付属の install.sh を実行するだけです。/usr/local 以下に入ります。

一番簡単に使うには

$ sudo minc /bin/bash
root@mincs01:/#

こんな感じになります。README.md に書かれていないオプションもあるので確認しましょう。

$ minc --help
/usr/local/bin/minc - Run given command in a temporary namespace
Usage: /usr/local/bin/minc [options] <command> [argument...]
 options:
    -h or --help        Show this help
    -k or --keep        Keep the temporary directory
    -t or --tempdir <DIR>  Set DIR for temporary directory (imply -k)
                    <UUID> Reuse UUID named container
    -r or --rootdir <DIR>  Set DIR for original root directory
                    <UUID> Use UUID named container image
    -X or --X11         Export local X11 unix socket
    -n or --net         Use network namespace
    -c or --cpu <mask>  Set CPU mask
    -p or --pty         Assign new pty for the container
    --name <NAME>       Set <NAME> as container's name (hostname)
    --user <USER>[:GROUP]  specify user and group (ID or name) to use
    --simple            Simple chroot model (do not pivot_root)
    --usedev		Use devtmpfs for /dev (for loopback etc.)
    --debug             Debug mode

ホスト名が付いていたほうが分かりやすいので --name を付けてみましょう。

$ sudo minc --name container /bin/bash 
root@container:/# hostname
container

コンテナの中を少し見てみると、

root@container:/# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.5  22512  5240 ?        S    19:58   0:00 /bin/bash
root        48  0.0  0.2  18480  2636 ?        R+   19:59   0:00 ps aux

PID Namespace も分離されているようですし、

root@container:/# cat /proc/mounts 
overlayfs / overlay rw,relatime,lowerdir=/,upperdir=/tmp/minc1300-WwY7qz/storage,workdir=/tmp/minc1300-WwY7qz/work 0 0
tmpfs /dev tmpfs rw,relatime 0 0
devpts /dev/pts devpts rw,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=666 0 0
udev /dev/console devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/null devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
udev /dev/zero devtmpfs rw,relatime,size=496816k,nr_inodes=124204,mode=755 0 0
mqueue /dev/mqueue mqueue rw,relatime 0 0
proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0
proc /proc/sys proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/sysrq-trigger proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/irq proc ro,nosuid,nodev,noexec,relatime 0 0
proc /proc/bus proc ro,nosuid,nodev,noexec,relatime 0 0
sysfs /sys sysfs rw,nosuid,nodev,noexec,relatime 0 0

/ は確かに overlayfs でマウントされているようですし、/proc 以下のヤバいファイルは Read-only でマウントされているようですね。

Network Namespace も分離するには -n か --net オプションを付けます。

$ sudo minc --net --name container /bin/bash 
root@container:/# ip a
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default 
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
4: vminc1369: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 6a:d9:17:92:bb:9a brd ff:ff:ff:ff:ff:ff

一応、veth ペアも作られて、コンテナに片方が割り当てられているようですね。アドレスは割りあたってませんし、ホスト側も

$ ip a
  :(snip)
3: veth1369: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 56:00:fe:13:93:28 brd ff:ff:ff:ff:ff:ff

という感じで作っただけという感じなので、その辺りはなんとかする必要はありそうです。

シェルスクリプトで書かれた軽量コンテナ MINCS がすばらしい (2) - TenForwardの日記』 へ続く

bind mount を使ったお気軽 LXC コンテナのススメ

お手軽に軽量な隔離環境を作るための kazuho さんの jailing とか、それにリソース管理の仕組みを加えた matsumotory さんの virtualing いいですね。

コンテナほどの隔離性は不要だし、一々イメージを落としてきて構築とかやるにはちょっと重たいなというシーンもあるわけで、そういう時に chroot、bind mount はお気軽で良いですし、それに cgroup でのリソース管理を加えてもそんなに重くはなりません。(docker だと他にも色々考えないとダメだし)

こういうのを思いついてサクッと作れてしまう才能がうらやましいわけですが、LXC を使えば才能がない私でも、何かをサクッと作る能力も努力もなく、もう少し隔離度を上げつつある程度の軽さを残した環境を LXC を使って作れますので、ここで紹介しましょう。

LXC 使っちゃったら、前述の軽さがなくなるから意味ないやんって話ですが、LXC ならコンテナ用のファイルシステムを作らずに bind mount を使ってコンテナを起動するのも簡単ですので、環境を作る手間のお手軽さと、bind mount を使ってホストのシステムを共用するというお手軽さは残して環境構築が可能です。

そもそもコンテナって、単に clone システムコールを使ってプロセス起動するだけなので、そんなに重くはならないでしょう、というお話です。もちろん LXC はそれ以外に色々やるので chroot に比べたら重いんですけどね。

bind mount を使った軽量(?)コンテナ

LXC にはコンテナ起動時にコンテナ用に何かをマウントするための設定があります。ここでホストのディレクトリやファイルを bind mount するだけで、ホストのシステムの一部をコンテナにエクスポートできます。

例えばホストの /usr をコンテナでそのまま使う場合、以下のように書けばそれでコンテナ起動時にマウントしてくれます。

lxc.mount.entry = /usr usr none ro,bind 0 0

つまり

  1. コンテナ用 rootfs 以下にディレクトリを作る (bind mount するにはマウントポイントないとダメですね)
  2. LXCの設定を書く
  3. lxc-startコマンドでコンテナ起動

これで jailing と同じような隔離環境が得られます。

LXC から cgroup を使うのも設定ファイルに制限値を設定すれば良いので、virtualing 相当の操作も可能ですね。

テンプレートを使って bind mount を使ったコンテナを作成する

言葉で書くと簡単ですが、ディレクトリを作ったり、設定ファイルを作ったりを手でやってると面倒で、jailing のお手軽さにかなうわけもありません。

そこで作ってみました。

LXC でコンテナを作る時は lxc-create コマンドにテンプレートを指定して作成します。その lxc-create に指定できるテンプレートファイルです。

$ sudo lxc-create -t bind -n (コンテナ名)

みたいにすれば、必要なディレクトリと設定ファイルを作成してくれます。

私の作った lxc-bind は決め打ちのところが多いです。そこはシェルスクリプトで書かれてるお気軽さですので、適当に変えてもらうということで。

もともと LXC には lxc-sshd というテンプレートが付属していて、これはまさしくコンテナのファイルシステムのほとんどをホストのディレクトリを bind mount して、sshd だけが起動するコンテナを作るテンプレートです。lxc-bind はこれを少し変えただけです。

jailing の例にあった /usr/local/apache/httpd を起動するコンテナを作るのも、

$ sudo lxc-create -t bind -n apache01 \
    -- --bind=/usr/local/apache \
    /usr/local/apache/bin/httpd \
    -c /usr/local/apache/conf/httpd.conf
$ sudo lxc-start -n apache01

ってな感じで簡単です (試してませんが :-p)。もちろん、作成したあとは起動させないとダメですので、jailing よりは実行するコマンドは増えます :-p

テンプレートに渡す引数なしで

lxc-create -t bind -n test01
みたいに実行すると bash を実行するコンテナを勝手に作ります。他に dhclient を実行して勝手に eth0 にアドレスも割り当てます (lxc.network.ipv4 みたいな設定で静的に割り当てることも可能)。

実行例
$ sudo lxc-create -t bind -n test01
$ sudo cat /var/lib/lxc/test01/config
lxc.network.type=veth
lxc.network.link=lxcbr0
lxc.network.flags=up
lxc.rootfs = /var/lib/lxc/test01/rootfs
lxc.utsname = test01
lxc.pts = 1024
lxc.cap.drop = sys_module mac_admin mac_override sys_time
lxc.mount.auto = cgroup:mixed proc:mixed sys:mixed
lxc.mount.entry = /etc/rc.d etc/rc.d none ro,bind 0 0
lxc.mount.entry = /etc/ssl/certs etc/ssl/certs none ro,bind 0 0
lxc.mount.entry = /dev dev none ro,bind 0 0
lxc.mount.entry = /run run none ro,bind 0 0
lxc.mount.entry = /bin bin none ro,bind 0 0
lxc.mount.entry = /sbin sbin none ro,bind 0 0
lxc.mount.entry = /usr usr none ro,bind 0 0
lxc.mount.entry = /lib lib none ro,bind 0 0
lxc.mount.entry = /lib64 lib64 none ro,bind 0 0
lxc.mount.entry = /var/lib/lxc/test01/init sbin/init none ro,bind 0 0

作成するとこんな感じに。

$ sudo lxc-start -n test01 
$ sudo lxc-ls -f test01
NAME    STATE    IPV4         IPV6  GROUPS  AUTOSTART  
-----------------------------------------------------
test01  RUNNING  10.0.100.41  -     -       NO
$ pstree
  :(略)
     |-lxc-start---init.lxc-+-bash
     |                      `-dhclient
  :(略)
$ sudo lxc-attach -n test01 -- ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  17272   596 ?        S    11:28   0:00 /usr/sbin/init.lxc -- /bin/bash
root        18  0.0  0.0  18984  6852 ?        Ss   11:28   0:00 /usr/sbin/dhclient eth0 -cf /dhclient.conf -v
root        19  100  0.0  20264  3144 ?        R    11:28  12:00 /bin/bash
root        21  0.0  0.0  16612  2624 ?        R+   11:40   0:00 ps aux

起動してますね。コンテナ内は bash と dhclient が動いています。

コンテナ内のマウントの様子を見てみると

$ sudo lxc-attach -n test01 -- cat /proc/mounts
  :(略)
/dev/root /etc/rc.d ext4 ro,relatime,data=ordered 0 0
/dev/root /etc/ssl/certs ext4 ro,relatime,data=ordered 0 0
/dev /dev tmpfs ro,relatime,mode=755 0 0
/dev/root /run ext4 ro,relatime,data=ordered 0 0
/dev/root /bin ext4 ro,relatime,data=ordered 0 0
/dev/root /sbin ext4 ro,relatime,data=ordered 0 0
/dev/root /usr ext4 ro,relatime,data=ordered 0 0
/dev/root /lib ext4 ro,relatime,data=ordered 0 0
/dev/root /lib64 ext4 ro,relatime,data=ordered 0 0
/dev/mapper/LXCVG01-LXCLV01 /sbin/init btrfs ro,relatime,space_cache 0 0
  :(略)

lxc.mount.entry で設定した辺りは上記でしょうかね。

ruby-lxc を使って同じようなことをやる

LXC には ruby-lxc というものがあって、これでスクリプトを書けば同じようなことが比較的簡単にできます。これをやるのが

です。シャレで作っただけなのでこれを使おうと思わないでください。^^;

こっちは --bind みたいなオプションも未実装なので使えないと思います。コンセプト作と思ってください。(ruby-lxc を使ってみたかっただけ)

$ sudo ruby lxcing /bin/bash

まとめ

延々とわけのわからないことを書きましたが、何が言いたいかというと、LXC お手軽だからもっとみんな使いましょう、ってことです。

おまけ

LXC 使っちゃうとまあ色々プロセス起動したり、ファイルシステム色々扱ったりするので、声を大にして「軽いぞ!!」とは言えない気もするのですが、libcontainer とか libct とか使ったら bind mount して気軽に隔離環境作るプログラムってすぐ作れないかな? 誰か作るといいですね。