TenForward

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

Plamo 7.0 上で非特権な systemd コンテナを起動する

Plamo 7.0 では cgroupfs_mount パッケージと lxc パッケージを提供していますが、非特権コンテナを起動するための調整は特に行っていませんでした。

cgroupfs_mount パッケージは、Plamo 7.0 リリース直後は Ubuntu 14.04 あたりで cgroupfs をマウントするのに使われていた cgroup-lite パッケージを元にした cgroupfs-mount をそのまま入れていました。これはシェルスクリプトで、システム上でサポートされているコントローラに対応したディレクトリを /sys/fs/cgroup に作成し、それぞれコントローラをオプションに指定してマウントするだけのシンプルなスクリプトです。

これで非特権コンテナは起動しますが、コンテナ内の systemd がちゃんと起動しません。今回はコンテナは Ubuntu bionic のイメージを使っています。

誰得?な記事ですが、非 systemd 環境でコンテナを起動する場合は参考になるかもしれません(←やっぱり誰得😂)

非特権コンテナを起動する準備

非特権の lxc コンテナを起動するためには、連載の第17回 で書いた方法と特に変わっていません。しかし一部の設定項目名が変わったりしています。

まずはコンテナを起動したいユーザが使えるサブIDを登録します。ここでは plamo ユーザーとします。

$ sudo usermod -v 200000-299999 -w 200000-299999 plamo

記事では、システム上で使う 65536 個のサブIDを使えるようにしていましたが、今の実装ではちょうどだと「足りない」とエラーが出るようです(詳しい原因は調べてません)。これは LXD のときも同じです。ですので実際に使う以上に余裕を持ってサブIDを確保しておきましょう。

非特権コンテナで veth インターフェースを使うために /etc/lxc/lxc-usernet ファイルにエントリを作成します。これはエディタで直接編集。

$ cat /etc/lxc/lxc-usernet 
plamo   veth    lxcbr0  10

個人用の lxc 設定をコピーします。そして非特権コンテナ用のIDマッピングの設定を追加します。

$ mkdir -p ~/.config/lxc
$ cp /etc/lxc/default.conf ~/.config/lxc/
$ (編集)
$ cat ~/.config/lxc/default.conf
lxc.net.0.type = veth
lxc.net.0.link = lxcbr0
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:xx:xx:xx

# これ以降が追加した行
lxc.idmap = u 0 200000 65536
lxc.idmap = g 0 200000 65536

記事では lxc.id_map でしたが、現在のバージョンである 3.x 系では lxc.idmap になっていますので注意が必要です。

pam_cgfs

記事の時点では、ユーザーが書き込み可能な cgroup をシェルスクリプトで作成し、各 cgroup に PID を登録していました。

今では lxc に pam_cgfs という PAM モジュールが付属していますので、これを使うとログイン時にユーザー権限の cgroup を作成してくれます。

/etc/pam.d/system-session ファイルの最後に次のように追加します。-c オプションの後にコントローラー名をカンマ区切りで指定するか、all を指定します。all だとシステム上で使えるすべてのコントローラーにユーザー用の cgroup を使います。

session  optional    pam_cgfs.so -c all

一部のコントローラーだけ必要であれば、次のように書きます。Ubuntu なんかでは次のように書かれています。

session  optional    pam_cgfs.so -c freezer,memory,name=systemd

これで freezermemoryコントローラー用の cgroup 配下と、systemd が作る systemd という cgroup 配下にユーザー用の cgroup を作ります。

Plamo の場合、ssh でアクセスしたときは system-session ファイルは使いませんので、/etc/pam.d/sshd ファイルの末尾に上記の行を追加してください。

これでログインすると、こんな感じにユーザー用の cgroup が作られ、ログインしたシェルが登録されます。

$ cat /proc/self/cgroup 
14:rdma:/user/plamo/0
13:pids:/user/plamo/0
12:hugetlb:/user/plamo/0
11:net_prio:/user/plamo/0
10:perf_event:/user/plamo/0
9:net_cls:/user/plamo/0
8:freezer:/user/plamo/0
7:devices:/user/plamo/0
6:memory:/user/plamo/0
5:blkio:/user/plamo/0
4:cpuacct:/user/plamo/0
3:cpu:/user/plamo/0
2:cpuset:/user/plamo/0
1:name=systemd:/user/plamo/0

各コントローラー用の cgroup 配下に /user/ ディレクトリを挟んでユーザー名のディレクトリ(= cgroup)が作られ、さらにはセッションID名のcgroupができています。自身のシェルプロセスの /proc/self/cgroup ファイルを見ると、上記のようにそのディレクトリにプロセスが登録されているのがわかります。

これで準備は OK のはずなので、Ubuntu コンテナを作成し、起動してみます。

$ lxc-create -t download c2 -- -d ubuntu -r bionic -a amd64
$ lxc-start c2
$ lxc-attach c2
$ lxc-ls -f
NAME STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED 
c2   RUNNING 0         -      -    -    true         

起動しましたが、待っていてもアドレスは割り振られません。中で確認してみます。

$ lxc-attach c2
root@c2:/# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.2  76408  5408 ?        Ss   11:15   0:00 /sbin/init
root         7  0.0  0.1  21444  3328 pts/2    Ss   11:16   0:00 /bin/bash
root        10  0.0  0.1  37336  2856 pts/2    R+   11:16   0:00 ps aux

というように init とシェルしかプロセスがありません。

どうやら systemd でホスト側に systemd が作成した cgroup が必要のようです(ちゃんと調べたわけではないですが)。

ホスト側で必要な systemd 用 cgroup を作成

そこで systemd が作る cgroup を自分で作ってみます。

$ sudo mkdir -p /sys/fs/cgroup/systemd/user.slice/user-$(id -u plamo).slice
$ sudo chown $(id -u plamo):$(id -g plamo) /sys/fs/cgroup/systemd/user.slice/user-$(id -u plamo).slice
$ sudo chown $(id -u plamo):$(id -g plamo) /sys/fs/cgroup/systemd/user.slice/user-$(id -u plamo).slice/{cgroup.procs,tasks}

各コントローラー用には次のようなスクリプトをでっち上げます。

UNPRIV_USERS="plamo"
mkdir -p /sys/fs/cgroup/systemd
mount -t cgroup -o none,name=systemd cgroup /sys/fs/cgroup/systemd
mkdir -p /sys/fs/cgroup/systemd/user.slice
for u in $UNPRIV_USERS
do
    USER_CG="/sys/fs/cgroup/systemd/user.slice/user-$(id -u $u).slice"
    mkdir -p $USER_CG
    chown $(id -u $u):$(id -g $u) $USER_CG
    chown $(id -u $u):$(id -g $u) $USER_CG/{cgroup.procs,tasks}
done

for sys in $(awk '!/^#/ { if ($4 == 1) print $1 }' /proc/cgroups); do
    mkdir -p $sys
    if ! mountpoint -q $sys; then
        if ! mount -n -t cgroup -o $sys cgroup $sys; then
            rmdir $sys || true
        else
            echo 1 > /sys/fs/cgroup/$sys/cgroup.clone_children
            for u in $UNPRIV_USERS
            do
                USER_CG="$sys/user.slice/user-$(id -u $u).slice"
                mkdir -p $USER_CG
                chown $(id -u $u):$(id -g $u) $USER_CG
                chown $(id -u $u):$(id -g $u) $USER_CG/{cgroup.procs,tasks}
                echo 1 > $USER_CG/cgroup.clone_children
                if [ $sys = "cpuset" ]; then
                    chown $(id -u $u):$(id -g $u) $USER_CG/cpuset.{cpus,mems}
                    echo 0 > $USER_CG/cpuset.cpus
                    echo 0 > $USER_CG/cpuset.mems
                fi
            done
        fi
    fi
done

つまり各 cgroup 配下に user.slice/user-$(id).slice という cgroup を作って、ユーザー権限を与えるわけですね。

次のようにディレクトリができます。

$ find /sys/fs/cgroup/memory/ | grep user.slice
/sys/fs/cgroup/memory/user.slice
/sys/fs/cgroup/memory/user.slice/memory.memsw.usage_in_bytes
/sys/fs/cgroup/memory/user.slice/memory.use_hierarchy
  :(snip)
/sys/fs/cgroup/memory/user.slice/user-1000.slice
/sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.memsw.usage_in_bytes
/sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.use_hierarchy
/sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.kmem.tcp.max_usage_in_bytes
/sys/fs/cgroup/memory/user.slice/user-1000.slice/memory.kmem.slabinfo

これでコンテナを起動させてみます。

$ lxc-start c2
$ lxc-ls -f
NAME STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
c2   RUNNING 0         -      10.0.3.223 -    true
$ lxc-attach c2
root@c2:/# ps aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.8  0.4  77040  8312 ?        Ss   11:58   0:00 /sbin/init
root        17  0.0  0.4  78312  9700 ?        Ss   11:58   0:00 /lib/systemd/systemd-journald
root        26  0.0  0.1  42104  3408 ?        Ss   11:58   0:00 /lib/systemd/systemd-udevd
systemd+    31  0.0  0.2  80028  5148 ?        Ss   11:58   0:00 /lib/systemd/systemd-networkd
systemd+    50  0.0  0.2  70616  5128 ?        Ss   11:58   0:00 /lib/systemd/systemd-resolved
root        54  0.0  0.1  31292  3036 ?        Ss   11:58   0:00 /usr/sbin/cron -f
root        55  0.6  0.8 170360 16912 ?        Ssl  11:58   0:00 /usr/bin/python3 /usr/bin/networkd-dispatcher --run-startup-triggers
root        56  0.0  0.2  62008  5508 ?        Ss   11:58   0:00 /lib/systemd/systemd-logind
message+    57  0.0  0.2  49928  4196 ?        Ss   11:58   0:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation --syslog-only
syslog      58  0.0  0.1 193400  4040 ?        Ssl  11:58   0:00 /usr/sbin/rsyslogd -n
root        59  0.0  0.1  61816  2956 ?        Ss   11:58   0:00 /lib/systemd/systemd-hostnamed
root        63  0.0  0.1  15956  2392 pts/3    Ss+  11:58   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/3 115200,38400,9600 vt220
root        64  0.0  0.1  15956  2324 pts/1    Ss+  11:58   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/1 115200,38400,9600 vt220
root        65  0.0  0.1  15956  2308 pts/1    Ss+  11:58   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud console 115200,38400,9600 linux
root        66  0.0  0.1  15956  2264 pts/2    Ss+  11:58   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/2 115200,38400,9600 vt220
root        67  0.0  0.1  15956  2296 pts/0    Ss+  11:58   0:00 /sbin/agetty -o -p -- \u --noclear --keep-baud pts/0 115200,38400,9600 vt220
root        70  0.0  0.1  21444  3264 pts/2    Ss   11:58   0:00 /bin/bash
root        73  0.0  0.1  37336  2996 pts/2    R+   11:58   0:00 ps aux

ちゃんと起動していますね。pidsdevicessystemd cgroup 配下に色々と systemd が cgroup を作っているので、この辺りがホストにも必要だったということでしょう。

Plamo 7.x の現時点の最新の cgroupfs_mount パッケージ 1.8 では、上のような処理を反映させたスクリプトにしてありますので、これをインストールして、/etc/sysconfig/cgroupfs-mount ファイル内で非特権コンテナを起動したいユーザーを指定すれば OK です。

$ cat /etc/sysconfig/cgroupfs-mount 
# List of non-root users who want to create owned cgroups
UNPRIV_USERS="plamo"

まとめ

以上のようにダミーで systemd が作る cgroup ツリーを作成すれば、systemd 採用のコンテナは起動しました。

今回はコンテナとして Ubuntu bionic を使っていますが、もう少し古い systemd だと /sys/fs/cgroup/systemd という cgroup ツリーさえあれば起動していた気がします。今後も systemd の変化とともに動きが変わるかもしれません。

Linux 4.20 で導入の PSI 機能(2)

前回 はとりあえず /proc/pressure の中身を見てみましたが、PSI 機能は cgroup v2 とも連携しています。というわけで、今回は cgroup v2 をマウントしてみましょう(前回以上に中身なし)。

まずは cgroup v2 をマウントして、コントローラーを子 cgroup で使えるように登録します。このあたりは連載 をどうぞ。

$ sudo mount -t cgroup2 -o nsdelegate cgroup2 /sys/fs/cgroup/
$ echo "+io +memory +cpu +pids" | tee /sys/fs/cgroup/cgroup.subtree_control
+io +memory +cpu +pids
$ cat /sys/fs/cgroup/cgroup.subtree_control
cpu io memory pids

子 cgroup を作って中身を覗いてみましょう。

$ sudo mkdir test01
$ ls
cgroup.controllers      cgroup.stat             cpu.pressure     io.max          memory.high       memory.pressure      pids.current
cgroup.events           cgroup.subtree_control  cpu.stat         io.pressure     memory.low        memory.stat          pids.events
cgroup.max.depth        cgroup.threads          cpu.weight       io.stat         memory.max        memory.swap.current  pids.max
cgroup.max.descendants  cgroup.type             cpu.weight.nice  memory.current  memory.min        memory.swap.events
cgroup.procs            cpu.max                 io.latency       memory.events   memory.oom.group  memory.swap.max

"pressure" という名前が付くファイルが登場していますね。

$ for f in $(ls *.pressure); do echo "$f: "; cat $f; done
cpu.pressure: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
io.pressure: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0
memory.pressure: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=0
full avg10=0.00 avg60=0.00 avg300=0.00 total=0

はい。前回の /proc/pressure 以下に登場したファイルと同じような中身ですね。

Linux 4.20 で導入の PSI 機能(1)

以前から Facebook のスライドなんかでは見かけた PSI という機能が 4.20 カーネルで入ったようですね。

CONFIG_PSI:

Collect metrics that indicate how overcommitted the CPU, memory,
and IO capacity are in the system.

If you say Y here, the kernel will create /proc/pressure/ with the
pressure statistics files cpu, memory, and io. These will indicate
the share of walltime in which some or all tasks in the system are
delayed due to contention of the respective resource.

In kernels with cgroup support, cgroups (cgroup2 only) will
have cpu.pressure, memory.pressure, and io.pressure files,
which aggregate pressure stalls for the grouped tasks only.

For more details see Documentation/accounting/psi.txt.

これを有効にすれば良さそう。さっそく Y で 4.20 を make。/proc/pressure なんてディレクトリが現れて、そこにファイルが 3 つほど置かれていました。

$ uname -a
Linux discovery 4.20.0-plamo64 #2 SMP PREEMPT Mon Jan 7 16:31:47 JST 2019 x86_64 GNU/Linux
$ for f in $(ls /proc/pressure/); do echo "$f: "; cat /proc/pressure/$f; done
cpu: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=60048550
io: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=30250330
full avg10=0.00 avg60=0.00 avg300=0.00 total=28401529
memory: 
some avg10=0.00 avg60=0.00 avg300=0.00 total=146509
full avg10=0.00 avg60=0.00 avg300=0.00 total=100482

cgroup2 をマウントすれば、cgroup にも同じようなファイルが現れるらしいがとりあえず今日はここまで(内容なさすぎ)。

af-graft を LXC コンテナで試してみた

Twitter でコンテナ用の面白い仕組みを見つけたので試してみました。中身ほぼゼロです。

論文も公開されているようですね。

README を見る限りは普通に LXC で使えそうなのでロクにドキュメント読まず(コラ)やってみました。

とりあえず Ubuntu 16.04 ホストを準備します(AWS 上に準備しました)。そして LXC をインストールします。こういうときは LXD でなく設定ファイルちょいちょいといじって起動させられる LXC 便利。

host:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 16.04.5 LTS
Release:    16.04
Codename:   xenial
host:~$ sudo apt install lxc

コンテナを作ります。

host:~$ lxc-create -t download -n c1 -- -d ubuntu -r xenial -a amd64
host:~$ sudo egrep '(lxc.net*|mount)' /var/lib/lxc/c1/config
lxc.network.type = empty
lxc.mount.entry = /home/ubuntu/af-graft home/ubuntu/af-graft none bind,relative,create=dir 0 0
  • lxc.network.type(LXC 3.0では lxc.net.0.type)は empty でループバックのみ
  • lxc.mount.entry はホスト側でコンパイルしたバイナリをコンテナ内でもそのまま使いかかったので書いてるだけで特に重要ではありません
  • CAP_NET_ADMIN が必要ですが、LXC コンテナではデフォルト許可なので capability はいじらず

ってところです。

ここでコンテナを起動します。コンテナには Apache がインストールされていて、コンテンツとして簡単な index.html を準備しました。

host:~$ sudo lxc-start -n c1
host:~$ sudo lxc-attach -n c1
root@c1:~# 
root@c1:~# cat /var/www/html/index.html
<h1>c1</h1>

ここからが本番。エンドポイントを作成します。

root@c1:~# af-graft/iproute2-4.10.0/ip/ip graft add nx4 type ipv4 addr 0.0.0.0 port 80
root@c1:~# af-graft/iproute2-4.10.0/ip/ip graft add nx6 type ipv6 addr :: port 80
root@c1:~# af-graft/iproute2-4.10.0/ip/ip graft show
nx4 type ipv4 addr 0.0.0.0 port 80 
nx6 type ipv6 addr :: port 80 

はい、できましたね。apache 起動してみましょう。

root@c1:~# LD_PRELOAD=af-graft/tools/libgraft-hijack.so GRAFT_CONV_PAIRS="0.0.0.0:80=nx4 :::80=nx6" apachectl start
libgraft-hijack.so:466:socket(): overwrite family 10 with AF_GRAFT (4)
libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=15
libgraft-hijack.so:466:socket(): overwrite family 2 with AF_GRAFT (4)
libgraft-hijack.so:466:socket(): overwrite family 10 with AF_GRAFT (4)
libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=2
libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=9
libgraft-hijack.so:597:setsockopt(): wrap setsockopt() level=1, optname=15
libgraft-hijack.so:540:bind(): convert bind :::80 to nx6

起動しました。試しに netstat してみると

root@c1:~# netstat -napt
Active Internet connections (servers and established)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name

何もありません。

では、アクセスしてみましょう。

  • ホストから
host:~$ curl http://localhost/
<h1>c1</h1>
  • Internet 経由で
$ curl http://afgrafttest.ten-forward.ws/
<h1>c1</h1>

ま、動いて当たり前なのでしょうが、試してみましたってことでご報告。論文なんかにパフォーマンス比較してありますが、手元でも時間があればやってみたいですね。macvlan とか ipvlan との比較も含めて(論文で何と比較してるのかは見てない)。

cgroup v2 の nsdelegate オプション(2)〜 cgroup namespace 作成後の cgroup root を権限委譲の境界として扱う

cgroup v2 の nsdelegate オプション第 2 弾です。

前回紹介したのは、cgroup namespace を作ると cgroup root を超えたプロセスの移動が禁止されるという機能でした。

これはユーザに関わらず関係するお話でしたが、今回は一般ユーザで namespace(つまりコンテナ)を作ったときのお話です。

私の連載の第 40 回 で紹介したとおり、cgroup v1 に比べて cgroup v2 は一般ユーザでプロセスを移動できる条件が厳しくなりました。ただ、これは実際のユースケースに合っている、というのは連載で書いたとおりです。

一般ユーザに cgroup ツリーを権限委譲する際には、権限委譲する root cgroup に当たる cgroup には、

という権限を与えました。

cgroup 内でコントローラを使えるようにしていても、権限委譲の root cgroup 内のコントローラ関連のファイルには権限は与えません。コンテナに与えるリソースを定義するのはあくまでホストの管理者であって、コンテナの管理者が自身に与えられたリソースを変更できるとマズいから、というのは納得の行くところだと思います。

これを実現するために、前述のようにホスト側の管理者がパーミッションの調整を行っていたわけですが、nsdelegate オプションをつけると、これが自動的に行われます。

nsdelegate の有無で処理がどう変わるか見てみましょう。処理は

  1. cgroup v2 をマウントする(オプション有無)
  2. cgroup.subtree_control にコントローラを登録
  3. cgroup 作成
  4. cgroup ディレクトリとそれ以下のファイルに所有権を与える
  5. cgroup にシェルの PID 登録
  6. cgroup namespace 作成
  7. 作成した cgroup のコントローラを制御するファイル(ここでは pids.max)に制限値を書き込む

という処理をやってみました。

nsdelegate を付けない場合

では、まず普通に cgroup v2 をマウントして処理をすすめましょう。

# mount -t cgroup2 cgroup2 /sys/fs/cgroup/ (1)
# echo "+io +pids +memory" > /sys/fs/cgroup/cgroup.subtree_control (2)
# mkdir /sys/fs/cgroup/test01 (3)
# chown karma /sys/fs/cgroup/test01   (4)
# chown karma /sys/fs/cgroup/test01/* (4)
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs (5)
4013
$ unshare --user --cgroup --map-root-user (6)
root@plamo70b2:~# ls -l /sys/fs/cgroup/test01 (権限を確認)
  : (snip)
-rw-r--r-- 1 root nogroup 0  9701:03 pids.max

このように pids.max には書き込み権がありますね。

一応 cgroup root にいるか確認します。

# cat /proc/$$/cgroup 
0::/

ここで pids.max に制限値として 50 を書き込みます。

# echo 50 > /sys/fs/cgroup/test01/pids.max (7)
# cat /sys/fs/cgroup/test01/pids.max 
50

登録できました。普通の cgroup v2 のコントローラ操作ですね。

nsdelegate を付けた場合

nsdelegate をオプションで与える以外は同じ操作をしてみます。

# mount -t cgroup2 -o nsdelegate cgroup2 /sys/fs/cgroup/ (1)
# echo "+io +pids +memory" > /sys/fs/cgroup/cgroup.subtree_control  (2)
# mkdir /sys/fs/cgroup/test01 (3)
# chown karma /sys/fs/cgroup/test01   (4)
# chown karma /sys/fs/cgroup/test01/* (4)
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs (5)
4013
$ unshare --user --cgroup --map-root-user (6)
# ls -l /sys/fs/cgroup/test01/
  : (snip)
-rw-r--r-- 1 root nogroup 0  9701:25 pids.max

先ほどと同様に書き込み権はあります。

cgroup root にいることも確認しておきます。

# cat /proc/$$/cgroup 
0::/

先ほどと同様に制限値を書き込みます。

# echo 50 > /sys/fs/cgroup/test01/pids.max (7)
-bash: echo: 書き込みエラー: 許可されていない操作です

今回は先ほどと違ってエラーになりました。

このように権限委譲した場合、その root cgroup には限定的な権限のみを与えるのが nsdelegate のもうひとつの機能です。

つまり cgroup namespace (つまりコンテナ)を権限委譲の境界とし、その root cgroup (コンテナの root cgroup)には限定的な権限を与え、それ以下の cgroup のみ権限委譲対象のユーザ管理とするわけです。