LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第40回Linuxカーネルのコンテナ機能 ― cgroupの改良版cgroup v2[4]

前回までに引き続き、今回もcgroup v2が持つ基本機能を紹介していきます。

Linuxでは、特権を持たないユーザでもユーザ名前空間を使ってコンテナを起動できます。その際、特権を持たないユーザがcgroupを操作できるように権限が委譲できるようになっています。今回は、権限を委譲する際の操作や動きについて、cgroup v1とv2それぞれの場合どのようになるのかを紹介していきましょう。

非特権ユーザに対する権限委譲

図1 非特権ユーザに対する権限委譲
図1 非特権ユーザに対する権限委譲

デフォルトではcgroupを操作する権限は特権ユーザにしかありません。一般ユーザ権限で起動する非特権コンテナを起動する場合などでは、一般ユーザにcgroupを操作できる権限を与える必要があります。

たとえば、図1のように一般ユーザ権限で"B"というコンテナを起動し、そのコンテナに対してリソース管理を行うためにcgroup Bを作成したとします。この場合、cgroup B以下に存在するツリーはコンテナ"B"を起動したユーザに管理させることになります。cgroup B以下のツリーを一般ユーザに管理させたい場合、権限を与えたいユーザに対して

  1. cgroup B以下にcgroupを作れる権限
  2. cgroup B以下でプロセスを自由に登録できる権限

を与えれば良いでしょう図2⁠。

図2 cgroupに対する権限委譲の一般的なパターン
図2 cgroupに対する権限委譲の一般的なパターン

cgroup B上のコントローラを制御するファイルについては、一般ユーザに権限は与えないことが一般的でしょう。なぜなら、この制御ファイル群に書き込み権を与えると、コンテナ内の管理者が、コンテナにかけられた制限を自由に変更できることになります。

もちろん、コンテナの管理者にコンテナが使えるリソースを自由に変更させたいという場合もあるでしょうから、その場合はcgroup B内のファイルに書き込み権を与えます。

cgroup v1の場合

cgroup v1の場合、先に挙げた権限は表1のようになります。

表1 cgroup v1で委譲するのに必要な権限
権限 実際の書き込み権
1 cgroup B以下にcgroupを作れる権限 ディレクトリBへの書き込み権
2 cgroup B以下でプロセスを自由に登録できる権限 登録先cgroup内のcgroup.procs, tasksファイルへの書き込み権

実際に試してみましょう。図1に対応させるためにcgroup_Bというcgroupを作成し、このcgroupの所有権をubuntuユーザにします。

$ whoami
ubuntu
$ sudo mkdir /sys/fs/cgroup/cpu/cgroup_B (cgroup_B を作成)
$ sudo chown ubuntu /sys/fs/cgroup/cpu/cgroup_B/ (cgroup_Bの所有権をubuntuに)
$ ls -ld /sys/fs/cgroup/cpu/cgroup_B/ (ubuntuユーザ所有になっている)
drwxr-xr-x 2 ubuntu root 0 Feb 27 20:04 /sys/fs/cgroup/cpu/cgroup_B/

これで子cgroupを作る準備はできましたので、実際にcgroup_Ccgroup_Dを作ってみましょう。

$ mkdir /sys/fs/cgroup/cpu/cgroup_B/cgroup_C
$ mkdir /sys/fs/cgroup/cpu/cgroup_B/cgroup_D
$ $ ls -ld /sys/fs/cgroup/cpu/cgroup_B/cgroup_*
drwxrwxr-x 2 ubuntu ubuntu 0 Feb 27 20:07 /sys/fs/cgroup/cpu/cgroup_B/cgroup_C
drwxrwxr-x 2 ubuntu ubuntu 0 Feb 27 20:07 /sys/fs/cgroup/cpu/cgroup_B/cgroup_D

通常のディレクトリを作成する際と同じで、cgroup_Bディレクトリはubuntuユーザ所有ですので、その配下にディレクトリを作ってもエラーは出ません。

$ ls -l /sys/fs/cgroup/cpu/cgroup_B/cgroup_C
total 0
-rw-r--r-- 1 ubuntu ubuntu 0 Feb 27 20:07 cgroup.clone_children
-rw-r--r-- 1 ubuntu ubuntu 0 Feb 27 20:07 cgroup.procs
-r--r--r-- 1 ubuntu ubuntu 0 Feb 27 20:07 cpuacct.stat
-rw-r--r-- 1 ubuntu ubuntu 0 Feb 27 20:07 cpuacct.usage
-r--r--r-- 1 ubuntu ubuntu 0 Feb 27 20:07 cpuacct.usage_percpu
-rw-r--r-- 1 ubuntu ubuntu 0 Feb 27 20:07 cpu.cfs_period_us
-rw-r--r-- 1 ubuntu ubuntu 0 Feb 27 20:07 cpu.cfs_quota_us
-rw-r--r-- 1 ubuntu ubuntu 0 Feb 27 20:07 cpu.shares
-r--r--r-- 1 ubuntu ubuntu 0 Feb 27 20:07 cpu.stat
-rw-r--r-- 1 ubuntu ubuntu 0 Feb 27 20:07 notify_on_release
-rw-r--r-- 1 ubuntu ubuntu 0 Feb 27 20:07 tasks

上の実行例のようにcgroup_Ccgroup_D以下にファイルは、完全にubuntuユーザ所有ですので自由にcgroup操作ができます。

次にプロセスの登録について見てみましょう。cgroup_Btaskscgroup.procsファイルの所有権をubuntuに変更し、プロセスを登録してみましょう[1]⁠。

$ sudo chown ubuntu /sys/fs/cgroup/cpu/cgroup_B/tasks
(cgroup_Bのtasksファイルの所有権をubuntuに)
$ sudo chown ubuntu /sys/fs/cgroup/cpu/cgroup_B/cgroup.procs
(cgroup_Bのcgroup.procsファイルの所有権をubuntuに)
$ echo $$
1690
$ echo $$ > /sys/fs/cgroup/cpu/cgroup_B/tasks (プロセスをcgroup_Bに登録)
$ cat /sys/fs/cgroup/cpu/cgroup_B/tasks (登録できている)
1690
2285

cgroup_Bにプロセスが登録できました。cgroup_Ccgroup_D以下のファイルの所有者はubuntuですから、プロセスの移動は自由です。

$ echo $$ > /sys/fs/cgroup/cpu/cgroup_B/cgroup_C/tasks
(上でcgroup_Bに登録したプロセスをcgroup_Cに移動)
$ cat /sys/fs/cgroup/cpu/cgroup_B/tasks (cgroup_Bにはプロセスはない)
$ cat /sys/fs/cgroup/cpu/cgroup_B/cgroup_C/tasks (cgroup_Cに移動した)
1690
2316
$ echo $$ > /sys/fs/cgroup/cpu/cgroup_B/cgroup_D/tasks 
$ cat /sys/fs/cgroup/cpu/cgroup_B/cgroup_D/tasks (cgroup_Dに移動した)
1690
2328
$ cat /sys/fs/cgroup/cpu/cgroup_B/tasks (再度cgroup_Bに移動した)
1690
2343

cgroup v1でプロセスをcgroup間で移動させる場合は、上の例のようにユーザが管理するcgroupへ自身が所有するプロセスを移動できます。厳密には以下の条件を満たす必要があるようです。

  • 移動先のtaskscgroup.procファイルへの書き込み権がある
  • 移動(書き込み)しようとするプロセスの実効UIDが、ターゲットとなるプロセスの実UIDもしくは保存set-user-idと一致する

上のような条件ですので、たとえば図3のように独立したcgroupツリーの間でも、権限さえあればプロセスの移動は自由です。cgroup A配下とcgroup B配下のそれぞれはコンテナとみなせますので、所有者が同じであればコンテナ間のプロセスの移動も自由であるということです(実際のコンテナでは名前空間による隔離があるので、通常はコンテナ内のユーザが他のコンテナにプロセスの移動はできないのが普通でしょう⁠⁠。

図3 独立したコンテナ間のプロセスの移動(cgroup v1)
図3 独立したコンテナ間のプロセスの移動(cgroup v1)

試してみましょう。

$ sudo mkdir /sys/fs/cgroup/cpu/cgroup_{A,B} (cgroup_A, B作成)
$ sudo chown ubuntu /sys/fs/cgroup/cpu/cgroup_{A,B} (A, Bの所有権の変更)
$ mkdir /sys/fs/cgroup/cpu/cgroup_A/cgroup_C (cgroup_C作成)
$ mkdir /sys/fs/cgroup/cpu/cgroup_B/cgroup_D (cgroup_D作成)
$ echo $$ > /sys/fs/cgroup/cpu/cgroup_A/cgroup_C/tasks (Cにプロセスを登録)
$ cat /sys/fs/cgroup/cpu/cgroup_A/cgroup_C/tasks | grep $$
1622 (登録されている)
$ echo $$ > /sys/fs/cgroup/cpu/cgroup_B/cgroup_D/tasks (Dにプロセスを移動)
$ cat /sys/fs/cgroup/cpu/cgroup_B/cgroup_D/tasks | grep $$
1622 (移動している)

図3の通りのcgroupを作成して、cgroup_Cに登録したプロセスをcgroup_Dに移動できました。

cgroup v2の場合

cgroup v2の場合、プロセスを移動するための条件が少し厳しくなっています。cgroup Bへプロセスを登録するには次のような権限が必要です。

表2 cgroup v2で委譲するために必要な権限
権限 実際の書き込み権
1 cgroup B以下にcgroupを作れる権限 ディレクトリBへの書き込み権
2 cgroup B以下でプロセスを自由に登録できる権限 登録先cgroup内のcgroup.procsファイルへの書き込み権
プロセスがもともと所属しているcgroupと登録先cgroupの共通の祖先となるcgroup内のcgroup.procsファイルへの書き込み権

実効UIDの制限がなくなった理由は、表2の2つめの条件が加えられたためです。完全に自身がコントロールしているツリーでなければプロセスを移動できませんので、任意のプロセスは移動できません[2]⁠。

文字で書くと条件が少しわかりづらいですね。実際に、cgroup v2で非特権の場合にプロセスを移動させてみましょう。

図4 cgroup v2でのプロセスの移動(1)
図4 cgroup v2でのプロセスの移動(1)

図4のような構成のツリーがあるとします。まずはcgroup_Bを作成し、ディレクトリとcgroup.procsファイルの所有権を変更します。

$ sudo chown ubuntu /sys/fs/cgroup/cgroup_B (cgroup Bをubuntuユーザ所有に)
$ sudo chown ubuntu /sys/fs/cgroup/cgroup_B/cgroup.procs
(cgroup Bのcgroup.procsファイルをubuntuユーザ権限に)
$ ls -al /sys/fs/cgroup/cgroup_B
total 0
drwxr-xr-x 4 ubuntu root  0 Mar  7 20:27 ./
dr-xr-xr-x 4 root   root  0 Mar  7 20:14 ../
-r--r--r-- 1 root   root  0 Mar  7 20:38 cgroup.controllers
-r--r--r-- 1 root   root  0 Mar  7 20:38 cgroup.events
-rw-r--r-- 1 ubuntu root  0 Mar  7 20:25 cgroup.procs
-rw-r--r-- 1 root   root  0 Mar  7 20:38 cgroup.subtree_control
(cgroup Bディレクトリとcgroup.procsファイルのみubuntuの所有となっている)

root cgroupからcgroup Bへのプロセスの移動

図4の(1)の操作を試してみましょう。ubuntuユーザでcgroup_Bcgroup.procsに登録を試みます。cgroup v1の場合はcgroup B内のcgroup.procsに書き込み権があれば、プロセスが登録できました。

$ whoami 
ubuntu
$ echo $$ > /sys/fs/cgroup/cgroup_B/cgroup.procs
-bash: echo: write error: Permission denied
(ユーザ自身の権限で実行されているプロセスを登録しようとしたが失敗した)

しかしcgroup v2では、cgroup.procsファイルはubuntuユーザの所有であり、ユーザ権限で実行されているプロセスを登録しようしたにも関わらずエラーになります。

これは表2で示した条件の2番目に引っかかったためです。この場合、⁠プロセスがもともと所属しているcgroupとcgroup Bの共通の祖先」はroot cgroupになります。ubuntuユーザはroot cgroupのcgroup.procsファイルに対する権限を持っていませんので、プロセスを移動させようとするとエラーになるわけです。

これは、図2で示した権限委譲のパターンのように、コンテナに所属させるプロセスを決めるのはroot cgroupの管理者であるべきという考え方でしょう。通常、コンテナを起動するのは、コンテナ内に権限を持つユーザではなくホスト上で権限を持つユーザですから、実際のユースケースに合っています。

この考え方に従い、ここはroot権限でプロセスをcgroup_Bに登録しましょう。

$ echo $$ | sudo tee /sys/fs/cgroup/cgroup_B/cgroup.procs
3126 (root権限でubuntuユーザ権限で動いているプロセスをcgroup_Bに登録)
$ grep $$ /sys/fs/cgroup/cgroup_B/cgroup.procs
3126 (登録できた)

この操作はroot権限で行っていますので登録できます。

権限があるcgroup間でのプロセスの移動(共通のcgroup配下のcgroup間)

それではcgroup_B配下にcgroup_Dcgroup_Eを作成し、先に登録したプロセスを移動させてみましょう。

$ whoami 
ubuntu
$ mkdir /sys/fs/cgroup/cgroup_B/cgroup_{D,E}
(ubuntuユーザ権限でcgroup D,Eを作成)
$ ls -d /sys/fs/cgroup/cgroup_B/cgroup_*
/sys/fs/cgroup/cgroup_B/cgroup_D/  /sys/fs/cgroup/cgroup_B/cgroup_E/
(問題なく作成できた)

cgroup_Bubuntuユーザ所有ですので、cgroup_Dcgroup_Eは問題なく作成できました。

それでは、さきほどroot権限でroot cgroupからcgroup_Bへ移動させたプロセスをcgroup_Dに移動させましょう。図4の(2)のパターンです。

$ whoami 
ubuntu
$ echo $$ > /sys/fs/cgroup/cgroup_B/cgroup_D/cgroup.procs
(自身の所有であるcgroup_Bのcgroup.procsに登録されたプロセスを自身の所有であるcgroup Dに移動)
$ grep $$ /sys/fs/cgroup/cgroup_B/cgroup_D/cgroup.procs
3126

今度はエラーなく作成できました。これはcgroup_Bcgroup_Bcgroup_Dの共通の祖先」という扱いになるためです。つまり、

  • cgroup_Dcgroup.procsubuntuユーザが所有
  • cgroup_Dcgroup_Bの共通の祖先であるcgroup_Bcgroup.procsファイルはubuntuユーザが所有

となり、2つの条件を満たします。

次に、cgroup_Dからcgroup_Eへのプロセスの移動を見てみましょう。図4の(3)のパターンです。

  • cgroup_Ecgroup.procsubuntuユーザが所有
  • cgroup_Dcgroup_Eの共通の祖先であるcgroup_Bcgroup.procsファイルはubuntuユーザが所有

ですので、両方の条件を満たします。試してみましょう。

$ whoami
ubuntu
$ ls -l /sys/fs/cgroup/cgroup_B/cgroup.procs
-rw-r--r-- 1 ubuntu root 0 Mar  7 20:25 /sys/fs/cgroup/cgroup_B/cgroup.procs
$ ls -l /sys/fs/cgroup/cgroup_B/cgroup_E/cgroup.procs
-rw-r--r-- 1 ubuntu ubuntu 0 Mar  7 20:57 /sys/fs/cgroup/cgroup_B/cgroup_E/cgroup.procs
(共通の祖先と移動先のcgroup.procsファイルに権限があることの確認)
$ grep $$ /sys/fs/cgroup/cgroup_B/cgroup_D/cgroup.procs 
3126 (cgroup Dにプロセスが登録されていることの確認)
$ echo $$ > /sys/fs/cgroup/cgroup_B/cgroup_E/cgroup.procs
(プロセスをcgroup Eに移動)
$ grep $$ /sys/fs/cgroup/cgroup_B/cgroup_E/cgroup.procs 
3126 (移動できた)

移動できました。

権限があるcgroup間でのプロセスの移動(共通のcgroup配下でないcgroup間)

別の例を紹介しましょう。

図5 cgroup v2でのプロセスの移動(2)
図5 cgroup v2でのプロセスの移動(2)

図5のようにroot cgroup直下にcgroup_Acgroup_Bを作成し、それぞれに子cgroup cgroup_Ccgroup_Dを作成します。

$ ls -ld /sys/fs/cgroup/cgroup_? (cgroup_A,Bはubuntuユーザ所有)
drwxr-xr-x 3 ubuntu root 0  3月 14日  19:38 /sys/fs/cgroup/cgroup_A/
drwxr-xr-x 3 ubuntu root 0  3月 14日  19:38 /sys/fs/cgroup/cgroup_B/
$ ls -l /sys/fs/cgroup/cgroup_?/cgroup.procs
(ubuntuユーザはcgroup_A,Bのcgroup.procsに書き込み権あり)
-rw-r--r-- 1 ubuntu root 0  3月 14日  19:33 /sys/fs/cgroup/cgroup_A/cgroup.procs
-rw-r--r-- 1 ubuntu root 0  3月 14日  19:33 /sys/fs/cgroup/cgroup_B/cgroup.procs
$ ls -ld /sys/fs/cgroup/cgroup_?/cgroup_? (cgroup_C,Dはubuntuユーザ所有)
drwxr-xr-x 2 ubuntu users 0  3月 14日  19:38 /sys/fs/cgroup/cgroup_A/cgroup_C/
drwxr-xr-x 2 ubuntu users 0  3月 14日  19:38 /sys/fs/cgroup/cgroup_B/cgroup_D/
$ ls -l /sys/fs/cgroup/cgroup_?/cgroup_?/cgroup.procs
(ubuntuユーザはcgroup_C,Dのcgroup.procsに書き込み権あり)
-rw-r--r-- 1 ubuntu users 0  3月 14日  19:38 /sys/fs/cgroup/cgroup_A/cgroup_C/cgroup.procs
-rw-r--r-- 1 ubuntu users 0  3月 14日  19:38 /sys/fs/cgroup/cgroup_B/cgroup_D/cgroup.procs

以上のようにcgroup_A〜Dまですべてubuntuユーザ所有のグループで、cgroup内のcgroup.procsファイルの所有権もubuntuユーザになっており、図5のような状態のcgroupツリーを準備しました。

ここでcgroup_Cからcgroup_Dへプロセスが移動できるでしょうか? 試してみましょう。

まずはcgroup_Cにプロセスを登録します。root cgroupからの移動になりますのでroot権限で移動させます。

$ echo $$ | sudo tee /sys/fs/cgroup/cgroup_A/cgroup_C/cgroup.procs 
3058 (root権限でcgroup_Cにプロセスを登録)
$ grep $$ /sys/fs/cgroup/cgroup_A/cgroup_C/cgroup.procs 
3058 (登録されている)

これでcgroup_Cにプロセスが登録された状態になりました。つぎに、このプロセスをcgroup_Cからcgroup_Dに移動させてみましょう。

$ echo $$ > /sys/fs/cgroup/cgroup_B/cgroup_D/cgroup.procs
-bash: echo: write error: Permission denied

cgroup_Dcgroup.procsubuntuユーザ所有であり、1番目の条件を満たしています。しかし、cgroup_Ccgroup_Dの共通の祖先をたどるとroot cgroupになります。root cgroupのcgroup.procsファイルに対してubuntuユーザは書き込み権限を持っていませんので、2番目の条件を満たしませんので、プロセスは移動できません。

このような移動ができない理由を考えてみましょう。

図5の構造を見ると、cgroup_A以下とcgroup_B以下はそれぞれ独立したコンテナとみなせます。いくら同じユーザ権限で動いているコンテナとはいえ、通常はコンテナをまたいでプロセスを移動することは考えにくいので、実際のユースケースに合った条件になったと言えるでしょう。

このようにcgroup v2では、条件を複雑にすることなく、プロセスの移動をコンテナ内に制限できるようになりました。

まとめ

今回を含めて4回でcgroup v2コアの機能を紹介してきました。

今回は特権を持たないユーザに対して、cgroupツリーの一部を権限委譲する際の機能について紹介しました。cgroup v1に比べてcgroup v2は、よりコンテナで使う場合の実際的な条件や制限が設定されていることが、お分かりいただけたのではないでしょうか。

cgroup v2には、これまで紹介したプロセスをグループ分けする以外にも、v1と同様にコントローラによってリソース制限をする機能があります。また、コアの機能としても新たに追加された機能がありますので、この連載では引き続きcgroup v2の機能を紹介していきたいと思いますが、今回でいったん説明の区切りが付きましたので、次回以降は少し違う話題を扱う予定です。

次回以降はしばらく私ではなく、ご自身でmrubyによるコンテナ実装Haconiwaを開発していらっしゃるudzuraさんにお書きいただく予定です。テーマは、この連載の32回でも軽く扱ったCRIU(チェックポイント・リストア)の話題の予定です。

udzura さんは、Haconiwaの開発を通じてCRIUのいろいろな機能を調査されているようですので、私もCRIUの機能を詳細に説明していただけるのではないかと、今からワクワクしています。

お楽しみに。

おすすめ記事

記事・ニュース一覧