人間とウェブの未来

「ウェブの歴史は人類の歴史の繰り返し」という観点から色々勉強しています。

ApacheとPHPが動くコンテナにUID/GIDの空間を動的に割り当ててhaconiwaで起動させる

hb.matsumoto-r.jp

前回の記事では、PHPが動くApacheのコンテナをhaconiwaを使って沢山起動しました。しかし、前回のコンテナは、

  • 同じコンテナイメージがあるディレクトリを複数コンテナで共有している
  • ホストから見てもuid/gidがapacheで全て起動している
  • pidもホストとコンテナで共有している

というように、コンテナとホストの空間が混在した状況になっていました。そこで、今回はさらに踏み込んで、よりコンテナを独立した環境にするべく、

  • 動的にコンテナイメージのtar.gzからそれぞれの独立したコンテナ用のディレクトリを用意する
  • 動的に各コンテナにホストのuid/gidレンジを切り出し、コンテナ内ではuid=0からレンジの数だけマッピングする
    • 例えば100個のuidレンジ(80000 ~ 80099)をコンテナ内では(0 ~ 99)とみなせるようにする
  • 動的にマッピングしたuidの最初のuid(コンテナではrootに該当)をコンテナのrootfsのuidとなるようにファイルのオーナを変更する
    • そうしないとファイルとuidがずれてパーミッションが適切に一致しない
  • pidがコンテナ内で独立したidとなるように名前空間を分ける
  • Apacheをpreforkで起動させて、コンテナ内でapacheユーザとして起動しても、ホストから見ると各レンジに分けたuidにマッピングされるようにする

を満たしたコンテナを、前回同様以下のようなApacheのportだけを環境変数で変化させるワンラインのシェルで、複数コンテナ起動できるようにしてみましょう。

for port in `seq 10001 10100`; do \
  APACHE_PORT=$port haconiwa start phpinfo-test.haco; sleep 0.01; \
done

実現するためのhaconiwa DSL

まずは結論から、この要件を実現するためのDSLを以下のように書いてみました。細かいDSLの説明はコードのコメントを読んで下さい。また、前回の記事と見比べて、どこが追加になったかを比較して見てみるのも良いでしょう。

Haconiwa.define do |config|

  root = Pathname.new("/var/lib/haconiwa/haconiwa-test-#{ENV['APACHE_PORT']}")

  # 元になるhaconiwaイメージ
  # https://github.com/haconiwa/haconiwa-image-php-tester にあるchroot環境を固めただけ
  haconiwa_image = Pathname.new("/var/lib/haconiwa/images/haconiwa-image-php-tester.tar.gz")

  # uidとgidの切り出しレンジ、この場合はコンテナに100個分uid/gidを切り出す
  uid_gid_range = 100

  # port番号からレンジの最初のuidを決定、このuidがコンテナ内ではroot(0)になる
  # 100倍することによって、ポートをキーに100ずつuid/gidをずらせる
  rootfs_root_uid = ENV['APACHE_PORT'].to_i * uid_gid_range
  config.name = "haconiwa-test-#{ENV['APACHE_PORT']}"

  if ENV['DEBUG'] == "apache"
    config.init_command = ["/usr/bin/env", 
                           "APACHE_PORT=#{ENV['APACHE_PORT']}", 
                           "APACHE_PID=/var/run/httpd/httpd.#{ENV['APACHE_PORT']}.pid", 
                           "/usr/sbin/httpd", "-X"]
  elsif ENV['DEBUG'] == "bash"
    config.init_command = "/bin/bash"
  else
    # haconiwaはforegroundで起動する前提の仕様なのでApacheをforegroundで起動する
    config.init_command = ["/usr/bin/env",
                           "APACHE_PORT=#{ENV['APACHE_PORT']}",
                           "APACHE_PID=/var/run/httpd/httpd.#{ENV['APACHE_PORT']}.pid",
                           "/usr/sbin/httpd", "-DFOREGROUND"]
    config.daemonize!
  end

  # コンテナイメージをsetupする際に最初だけ呼ばれるブロック
  config.bootstrap do |b|
    # tarファイルからコンテナを複製する
    unless Dir.exists?(root)
      `mkdir -p #{root}`
      `tar xf #{haconiwa_image} -C #{root}`
    end
     # apache起動に必要なdevファイルを作っておく
    `mknod -m 444 #{root}/dev/random c 1 9` unless File.exist?("#{root}/dev/random")
    `mknod -m 444 #{root}/dev/urandom c 1 9` unless File.exist?("#{root}/dev/random")

    # 各コンテナのrootfsを切り出した最初のuid/gidに変更
    # これによって、コンテナのrootとホストからみたコンテナのrootのuid/gidとファイルのオーナを一致させる
    `chown -R #{rootfs_root_uid}:#{rootfs_root_uid} #{root}`
  end

  config.chroot_to root

  config.namespace.unshare "ipc"
  config.namespace.unshare "uts"
  config.namespace.unshare "mount"

  # pid名前空間を分離
  config.namespace.unshare "pid"

  # 実際にnamespaceによってコンテナ内のrootとホスト上のuidをマッピングさせる
  config.rootfs_owner uid: rootfs_root_uid, gid: rootfs_root_uid

  # マッピングのrangeを設定。今回は100個のuidを切り出している。
  # そして、offsetにはコンテナ内でrootとするuidを設定することで、コンテナ内のrootの0から99まで
  # のuidのレンジと、ホスト上のuidのレンジ’(port番号*100 ~ port番号*100+99)をマッピングさせる
  config.namespace.set_uid_mapping min: 0, max: uid_gid_range - 1, offset: rootfs_root_uid
  config.namespace.set_gid_mapping min: 0, max: uid_gid_range - 1, offset: rootfs_root_uid

  config.add_mount_point "tmpfs", to: root.join("tmp"), fs: "tmpfs"

  config.mount_network_etc(root, host_root: "/etc")

  config.mount_independent "procfs"
  config.mount_independent "sysfs"
  config.mount_independent "devtmpfs"
  config.mount_independent "devpts"
  config.mount_independent "shm"

  config.cgroup["cpu.cfs_period_us"] = 100000
  config.cgroup["cpu.cfs_quota_us"]  =  30000

end
  • ちなみに前回記事のDSL
Haconiwa.define do |config|
  # The container name and container's hostname:
  config.name = "haconiwa-2d057697-#{ENV['APACHE_PORT']}"
  # The first process when invoking haconiwa run:
  config.init_command = ["/usr/bin/env", "APACHE_PORT=#{ENV['APACHE_PORT']}", "APACHE_PID=/var/run/httpd/httpd.#{ENV['APACHE_PORT']}.pid", "/usr/sbin/httpd", "-X"]
  # If your first process is a daemon, please explicitly daemonize by:
  config.daemonize!

  # The rootfs location on your host OS
  # Pathname class is useful:
  root = Pathname.new("/var/lib/haconiwa/2d057697")
  config.chroot_to root

  # mount point configuration:
  config.add_mount_point "tmpfs", to: root.join("tmp"), fs: "tmpfs"

  # Share network etc files from host to contianer
  # You can reuse /etc/netns/${netnsname}/* files:
  config.mount_network_etc(root, host_root: "/etc")

  # Re-mount specific filesystems under new container namespace
  # These are recommended when namespaces such as pid and net are unshared:
  config.mount_independent "procfs"
  config.mount_independent "sysfs"
  config.mount_independent "devtmpfs"
  config.mount_independent "devpts"
  config.mount_independent "shm"

  # namespeaceの設定を2行追記
  config.namespace.unshare "ipc"
  config.namespace.unshare "uts"

  # cgroupによって、コンテナのCPU使用上限を30%に制限
  config.cgroup["cpu.cfs_period_us"] = 100000
  config.cgroup["cpu.cfs_quota_us"]  =  30000

end

上記のようなDSLによって、ワンラインで唯一の変数であるポート番号を元に、ホスト上でのuid/gidの空間を動的に100個切り出した上で、それをコンテナに割り当てて、その切り出した最初のuidを、コンテナ内のroot(uid=0)にmappingすることができます。

ポートから動的に切り出して割り出す所のコードをピックアップ

  • uid/gidレンジの動的割当
  # uidとgidの切り出しレンジ、この場合はコンテナに100個分uid/gidを切り出す
  uid_gid_range = 100

  # port番号からレンジの最初のuidを決定、このuidがコンテナ内ではroot(0)になる
  # 100倍することによって、ポートをキーに100ずつuid/gidをずらせる
  rootfs_root_uid = ENV['APACHE_PORT'].to_i * uid_gid_range
  • 切り出したuid/gidをコンテナ内でマッピング
  # namespaceによってコンテナ内のrootとホスト上のuidをマッピングさせる
  config.rootfs_owner uid: rootfs_root_uid, gid: rootfs_root_uid

  # マッピングのrangeを設定。今回は100個のuid/gidを切り出している。
  # offsetにはコンテナ内でrootとするホストのuidを設定することで、コンテナ内のrootの0から99まで
  # のuidのレンジと、ホスト上のuidのレンジ’(port番号*100 ~ port番号*100+99)をマッピングさせる
  config.namespace.set_uid_mapping min: 0, max: uid_gid_range - 1, offset: rootfs_root_uid
  config.namespace.set_gid_mapping min: 0, max: uid_gid_range - 1, offset: rootfs_root_uid
  • port番号から動的に決定したコンテナのrootfs
  root = Pathname.new("/var/lib/haconiwa/haconiwa-test-#{ENV['APACHE_PORT']}")

  # 元になるhaconiwaイメージ
  # https://github.com/haconiwa/haconiwa-image-php-tester にあるchroot環境を固めただけ
  haconiwa_image = Pathname.new("/var/lib/haconiwa/images/haconiwa-image-php-tester.tar.gz")
  • ベースのコンテナイメージであるtarをbootstrapブロックで展開
  • mknodなどしつつ最終行でrootfsを切り出したuidの最初のuid(コンテナ内でのroot)に変更
  # コンテナイメージをsetupする際に最初だけ呼ばれるブロック
  config.bootstrap do |b|
    # tarファイルからコンテナを複製する
    unless Dir.exists?(root)
      `mkdir -p #{root}`
      `tar xf #{haconiwa_image} -C #{root}`
    end
     # apache起動に必要なdevファイルを作っておく
    `mknod -m 444 #{root}/dev/random c 1 9` unless File.exist?("#{root}/dev/random")
    `mknod -m 444 #{root}/dev/urandom c 1 9` unless File.exist?("#{root}/dev/random")

    # 各コンテナのrootfsを切り出した最初のuid/gidに変更
    # これによって、コンテナのrootとホストからみたコンテナのrootのuid/gidとファイルのオーナを一致させる
    `chown -R #{rootfs_root_uid}:#{rootfs_root_uid} #{root}`
  end
  • PID名前空間はこれだけ
  # pid名前空間を分離
  config.namespace.unshare "pid"

こんな風にかけます。DSLさえわかれば細かい動きまで制御できて簡単ですね。

ワンラインで実行

そして、以下のようにhaconiwaコマンドをワンラインで実行します。

for port in `seq 10001 10100`; do \
  APACHE_PORT=$port haconiwa start phpinfo-test.haco; sleep 0.01; \
done

すると、以下のようにapacheが起動し始めました。ホストのpsコマンドで見た時は以下のように見えます。

root       20990  0.0  0.0  16092  3360 ?        Ss   15:32   0:00 haconiwa start phpinfo-test.haco
1000700    20991  0.0  0.0 309844 18480 ?        S    15:32   0:00  \_ /usr/sbin/httpd -DFOREGROUND
1000748    20997  0.0  0.0 309844  7024 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000748    20998  0.0  0.0 309844  7004 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000748    20999  0.0  0.0 309844  7112 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000748    21000  0.0  0.0 309844  7112 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000748    21001  0.0  0.0 309844  7112 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
root       21004  0.0  0.0  16092  3352 ?        Ss   15:32   0:00 haconiwa start phpinfo-test.haco
1000800    21005  0.0  0.0 309844 18560 ?        S    15:32   0:00  \_ /usr/sbin/httpd -DFOREGROUND
1000848    21010  0.0  0.0 309844  7036 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000848    21011  0.0  0.0 309844  7036 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000848    21012  0.0  0.0 309844  7036 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000848    21013  0.0  0.0 309844  7036 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000848    21014  0.0  0.0 309844  7036 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
root       21017  0.0  0.0  16092  3360 ?        Ss   15:32   0:00 haconiwa start phpinfo-test.haco
1000900    21018  0.0  0.0 309844 18284 ?        S    15:32   0:00  \_ /usr/sbin/httpd -DFOREGROUND
1000948    21023  0.0  0.0 309844  6872 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000948    21024  0.0  0.0 309844  6872 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000948    21025  0.0  0.0 309844  6872 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000948    21026  0.0  0.0 309844  6872 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1000948    21027  0.0  0.0 309844  6872 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
root       21030  0.0  0.0  16092  3352 ?        Ss   15:32   0:00 haconiwa start phpinfo-test.haco
1001000    21031  0.0  0.0 309844 18560 ?        S    15:32   0:00  \_ /usr/sbin/httpd -DFOREGROUND
1001048    21036  0.0  0.0 309844  6844 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001048    21037  0.0  0.0 309844  6844 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001048    21038  0.0  0.0 309844  6844 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001048    21039  0.0  0.0 309844  6844 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001048    21040  0.0  0.0 309844  6844 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
root       21043  0.0  0.0  16092  3296 ?        Ss   15:32   0:00 haconiwa start phpinfo-test.haco
1001100    21044  0.0  0.0 309844 18588 ?        S    15:32   0:00  \_ /usr/sbin/httpd -DFOREGROUND
1001148    21049  0.0  0.0 309844  6880 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001148    21050  0.0  0.0 309844  6880 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001148    21051  0.0  0.0 309844  6880 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001148    21052  0.0  0.0 309844  6880 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001148    21053  0.0  0.0 309844  6880 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
root       21056  0.0  0.0  16092  3292 ?        Ss   15:32   0:00 haconiwa start phpinfo-test.haco
1001200    21057  0.0  0.0 309844 18360 ?        S    15:32   0:00  \_ /usr/sbin/httpd -DFOREGROUND
1001248    21062  0.0  0.0 309844  6876 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001248    21063  0.0  0.0 309844  6800 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001248    21064  0.0  0.0 309844  6816 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001248    21065  0.0  0.0 309844  6876 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001248    21066  0.0  0.0 309844  6876 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
root       21069  0.0  0.0  16092  3364 ?        Ss   15:32   0:00 haconiwa start phpinfo-test.haco
1001300    21070  0.0  0.0 309844 18520 ?        S    15:32   0:00  \_ /usr/sbin/httpd -DFOREGROUND
1001348    21075  0.0  0.0 309844  7028 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001348    21076  0.0  0.0 309844  7028 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001348    21077  0.0  0.0 309844  7028 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001348    21078  0.0  0.0 309844  7028 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001348    21079  0.0  0.0 309844  7028 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
root       21082  0.0  0.0  16092  3360 ?        Ss   15:32   0:00 haconiwa start phpinfo-test.haco
1001400    21083  0.0  0.0 309844 18484 ?        S    15:32   0:00  \_ /usr/sbin/httpd -DFOREGROUND
1001448    21088  0.0  0.0 309844  6784 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND
1001448    21089  0.0  0.0 309844  6784 ?        S    15:32   0:00      \_ /usr/sbin/httpd -DFOREGROUND

今回は、ポートを10001から10100まで使って、コンテナを動かしたので、ちょうどポートに合わせてhaconiwa内で動作しているapacheプロセスのuidが割り当てているのがわかるでしょうか。例えばポートが10012の場合、

10012 * 100 + 48 = 1001248

というように、ホストからは1001248、コンテナ内ではapacheの定番のuidである48として動いています。10014ポートだと、そのポートで起動しているApacheコンテナのrootのuidはホストからみると1001400、apacheのuidは1001448となります。実際にこの設定で、apacheではなくbashでコンテナを起動して確認すると以下のようになっていました。

$ sudo DEBUG=bash APACHE_PORT=10012 haconiwa start phpinfo-test.haco 
Container fork success and going to wait: pid=33166
bash-4.2# id apache  
uid=48(apache) gid=48(apache) groups=48(apache)
bash-4.2# 

ちゃんとコンテナ内では48と認識していますね。

また、コンテナ内でmatsumotoryユーザも作る事ができました。

bash-4.2# useradd matsumotory
bash-4.2# grep matsumotory /etc/passwd
matsumotory:x:90:90::/home/matsumotory:/bin/bash
bash-4.2# su - matsumotory
[matsumotory@haconiwa-test-7000 ~]$ pwd
/home/matsumotory
[matsumotory@haconiwa-test-7000 ~]$ getpcaps $$
Capabilities for `59': =
[matsumotory@haconiwa-test-7000 ~]$ 

このように、コンテナイメージのlogin.defsを例えば以下の様に設定しておくと、useraddでも指定されたuid/gidの切り出しスペース内でuserを作ってくれます。

  • /etc/login.defs
#
# Min/max values for automatic uid selection in useradd
#
UID_MIN                 90
UID_MAX                 99
# System accounts
SYS_UID_MIN             20
SYS_UID_MAX             89

#
# Min/max values for automatic gid selection in groupadd
#
GID_MIN                 90
GID_MAX                 99
# System accounts
SYS_GID_MIN             20
SYS_GID_MAX             89

また、psコマンドを叩いても、シェルのpidが1でpsコマンドのpidが4など、pidも分離されていて、/proc以下にも1と4のpidに対応したディレクトリしか見えませんでした。

まとめ

ということで、今回は、

  • コンテナイメージのtar.gzからそれぞれの独立したコンテナ用のディレクトリを用意する
  • 各コンテナにホストのuid/gidレンジを切り出し、コンテナ内ではuid=0からレンジの数だけマッピングする
    • 例えば100個のuidレンジ(80000 ~ 80099)をコンテナ内では(0 ~ 99)とみなせるようにする
  • マッピングしたuidの最初のuid(コンテナではrootに該当)をコンテナのrootfsのuidとなるようにファイルのオーナを変更する
    • そうしないとファイルとuidがずれてパーミッションが適切に一致しない
  • pidがコンテナ内で独立したidとなるように名前空間を分ける
  • Apacheをpreforkで起動させて、コンテナ内でapacheユーザとして起動しても、ホストから見ると各レンジに分けたuidにマッピングされるようにする
    • そのためhttpdの設定はuid48と一律書いておけば、ホストからはきれいに区別してmappingされるが、コンテナ内は同一の設定ファイルを利用できる

の要件をワンラインでhaconiwaで起動できるようにしました。以下のリポジトリに今回試したDSLとコンテナイメージを公開しています。

github.com

こういう事が、簡単にDSLで表現できて、機能毎に少しずつ必要な機能を追加して独自のコンテナ環境を作ることができるのはhaconiwaの魅力ですね。今回はコンテナ環境の各種設定をbootstrapブロックで実現しましたが、コンテナ環境作成後、コンテナの中でいくつかのコマンドをプロビジョニング的に実行したい場合は、provisionブロックを使うことでコンテナ内でのコマンドをhaconiwa起動時にコンテナ内で実行することができるのも応用が効きそうです。

また、今回bootstrapの中でシェルで書いていた動きは、 @udzura さんが今DSL化してくださっているので、より簡潔かつ綺麗にDSLを書く事ができるようになります。例えば、

  root = Pathname.new("/var/lib/haconiwa/48b7030f-3")
  config.chroot_to root

  config.bootstrap do |b|
    b.strategy = "shell"
    b.code = <<-EOS
rsync -alv /var/lib/haconiwa/48b7030f-1/ #{root}/
test -f #{root}/dev/random || mknod -m 444 #{root}/dev/random c 1 9
test -f #{root}/dev/urandom || mknod -m 444 #{root}/dev/urandom c 1 9
    EOS
  end

こんなのや、

  config.bootstrap do |b|
    b.strategy = "mruby"
    b.code do
      cmd = RunCmd.new("bootstrap.mymruby")
      cmd.run "rsync -alv /var/lib/haconiwa/48b7030f-1/ #{root}/"
      cmd.run "test -f #{root}/dev/random || mknod -m 444 #{root}/dev/random c 1 9"
      cmd.run "test -f #{root}/dev/urandom || mknod -m 444 #{root}/dev/urandom c 1 9"
      Haconiwa::Logger.puts "Setup is OK!"
    end
  end

さらには、

  config.bootstrap do |b|
    b.strategy = "tarball"
    b.archive_path = "/usr/local/src/testball.tar.xz"
    b.tar_options = ["-v"]
  end

などのように書けるようになるそうです。詳細は udzuraさんのリリース情報に注目しておきましょう。ぱっと見るだけでもいい感じですね。

ということで、今後は、Linuxのcapabilityについては、まだ設定していないため、コンテナで起動するrootに対してどこまで特権を与えるか、といったあたりを試していいこうかと思います。