前回の記事では、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とコンテナイメージを公開しています。
こういう事が、簡単に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に対してどこまで特権を与えるか、といったあたりを試していいこうかと思います。