HaconiwaとmrubyスクリプトでLinuxコンテナをつくる
tech.pepabo.com インターン中に理解を深めるために作ったLinuxコンテナについて書きます。chikuwaitです。
最近よくコンテナ型仮想化技術の名前よく聞きますよね。とりあえずDocker使うみたいな流れがあってとてもベンリ!みたいな。 漠然と普段使ってますけど、中身ちょっと知りたくありません?ということでとりあえず作ってみましょう。そんなに難しくない。
Linuxコンテナなあに
そもそもコンテナ型仮想化技術というのは単一の技術・仕組みではなくて、Linuxの複数の機能(Linux Namespaces、cgroup、chrootなどなど)を組み合わせて作られています。これらの機能を組み合わせOSの中に隔離した環境を用意する、という感じですね。コンテナ仲良くなれそう!
インターン中となりで Uchio Kondo 🦉⬛️ (@udzura) | Twitter さんが作っていた資料が初めての人にも良さそうだったので、これを見ると幸せになれるかもしれません。
Haconiwaについて
Uchio Kondo 🦉⬛️ (@udzura) | Twitterさんとかペパボの人が中心になって作っているmruby製コンテナ。インターン後半はHaconiwa触ってたりしました(主にHaconiwa内部で使ってるmrubyをCurrent Masterに合わせてた)。Rubyの文法でコンテナの設定とかを書けたりする。 今回はHaconiwaに含まれているhacorbというLinuxコンテナに必要な機能が組み込まれたmrubyバイナリを使用します。
作り方
Haconiwaを使えるようにする
※ここから先はUbuntu 16.04 LTS (Xenial Xerus)での方法で書いています。
Haconiwaをビルドするのはちょっと手間がかかるのでpackagecloudで公開されているパッケージを使います。
$ curl -s https://packagecloud.io/install/repositories/udzura/haconiwa/script.deb.sh | sudo bash $ sudo apt-get install haconiwa=0.8.6-1 $haconiwa version => haconiwa: v0.8.6
rootfsを用意する
コンテナのためのrootのファイルシステムを用意します。 今回はDebian基本システムをインストールする公式のdebootstrapを使います。
$ sudo apt install debootstrap $ sudo debootstrap --arch amd64 jessie ~/debian-container/ http://ftp.jp.debian.org/debian $ ls debian-container => bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
実際に作っていく
下準備は出来たので、実際にスクリプトを書いていきます。
1.プロセスをforkする
コンテナ型仮想化技術といっても、実際はプロセスなので、まずはプロセスをforkするところからはじまります。
pid = Process.fork do Kernel.exec '/bin/bash' end Process.waitpid2(pid)
プロセスをforkして、子プロセスの中で/bin/bash
を実行します。
親プロセスは子プロセスの終了を待っていないといけないのでProcess.waitpid2(pid)
で待機します。
2.cgroupでリソースを制限する
cgroup(control group)はプロセスをグループ化してcpuやメモリなどのリソースの制御をグループ単位で管理できる機能です。 今回はcgroupで使用するCPUのコア数に制限をかけたりします。
pid = Process.fork do rate = Cgroup::CPU.new "chikuwait" core = Cgroup::CPUSET.new "chikuwait" rate.cfs_quota_us = 200000 rate.cfs_period_us = 100000 core.cpus = "0-1" core.mems = "0" rate.create core.create rate.attach core.attach Kernel.exec '/bin/bash' end Process.waitpid2(pid)
rate.cfs_quota_us
で一定の期間に実行される合計時間をµs単位で指定することができます。
rate.cfs_period_us
でCPU リソースへのアクセスを再割り当てする一定間隔をµs単位で指定することができます。
core.cpus
で使用するコア数を決めることができます。今回は2コアにしてあります。
core.mems
で使用できるメモリーノードを指定できます。
一度このスクリプトを動かし、リソースの制御が出来ているか確認します。
$ sudo hacorb container.rb $ yes >>/dev/null & $ yes >>/dev/null & $ yes >>/dev/null & $ yes >>/dev/null &
yes >>/dev/null &
でCPUの使用率を上げてから、topコマンドでCPUの使用率を確認します。
CPUの使用率見てみるとリソースの制御ができていますね。ちょっとコンテナらしくなってきた。
名前空間を分離する
参考:https://linuxjm.osdn.jp/html/LDP_man-pages/man7/namespaces.7.html
namespace(名前空間)でシステムのリソースを分離します。 今回はPID名前空間、UTS名前空間、IPC名前空間、マウント名前空間、ネットワーク名前空間を分離します。
Namespace.unshare(Namespace::CLONE_NEWPID) pid = Process.fork do rate = Cgroup::CPU.new "chikuwait" core = Cgroup::CPUSET.new "chikuwait" rate.cfs_quota_us = 200000 rate.cfs_period_us = 100000 core.cpus = "0−1" core.mems = "0" rate.create core.create rate.attach core.attach Namespace.unshare(Namespace::CLONE_NEWUTS) Namespace.unshare(Namespace::CLONE_NEWIPC) Namespace.unshare(Namespace::CLONE_NEWNS) Namespace.unshare(Namespace::CLONE_NEWNET) Kernel.exec '/bin/bash' end Process.waitpid2(pid)
コンテナの中と外でls -l /proc/$$/ns
をして比較することで、分離した名前空間のinode番号が変わっていることを確認します。
inodeとはLinuxのファイルの属性や管理情報を格納している管理領域のことです。
capabilityの設定をする
Linuxのプロセスにおける権限は一般ユーザ権限と特権(root)の二種類しかありません。しかしながら、プロセスに全ての特権を与えてしまうと脆弱性など色々の問題があってよくありません。そこでcapabilityという権限を細分化したものを使います。
Namespace.unshare(Namespace::CLONE_NEWPID) pid = Process.fork do rate = Cgroup::CPU.new "chikuwait" core = Cgroup::CPUSET.new "chikuwait" rate.cfs_quota_us = 200000 rate.cfs_period_us = 100000 core.cpus = "0−1" core.mems = "0" rate.create core.create rate.attach core.attach Namespace.unshare(Namespace::CLONE_NEWUTS) Namespace.unshare(Namespace::CLONE_NEWIPC) Namespace.unshare(Namespace::CLONE_NEWNS) Namespace.unshare(Namespace::CLONE_NEWNET) c = Capability.new cap = [Capability::CAP_CHOWN, Capability::CAP_DAC_OVERRIDE, Capability::CAP_FSETID, Capability::CAP_FOWNER, Capability::CAP_MKNOD, Capability::CAP_NET_RAW, Capability::CAP_SETGID, Capability::CAP_SETUID, Capability::CAP_SETPCAP, Capability::CAP_NET_BIND_SERVICE, Capability::CAP_SYS_CHROOT, Capability::CAP_KILL, Capability::CAP_AUDIT_WRITE] c.set Capability::CAP_PERMITTED, cap c.set_flag Capability::CAP_EFFECTIVE, cap, Capability::CAP_SET Kernel.exec '/bin/bash' end Process.waitpid2(pid)
以下のものを有効にしています。
CAP_CHOWN : ファイルのUIDとGIDを任意に変更
CAP_DAC_OVERRIDE : ファイルの読み出し、書き込み、実行の権限チェックをバイパス
CAP_FSETID : ファイルに変更があった時にset-user-ID
とset-group-ID
の許可ビットをクリアしない
CAP_FOWNER : 任意のファイルに対してアクセス制御を設定
CAP_MKNOD : システムコールmkmod(2)
を使って特殊ファイルを作成
CAP_NET_RAW : RAW・PACKETソケットの使用
CAP_SETGID : システムコールsetgid(2)
を使用してグループIDのセット
CAP_SETUID : システムコールsetuid(2)
を使用してユーザ識別子のセット
CAP_SETPCAP : ファイルケーパビリティがサポートされている場合、呼び出し元スレッドのバインディングセットの任意ケーパビリティを自身の継承ケーパビリティリストに追加。ファイルケーパビリティがサポートされて居ない場合は、呼び出し元が許可されている任意ケーパビリティセットを他のプロセスへ付与、削除
CAP_NET_BIND_SERVICE : 1024番以下の特権ポートをバインド
CAP_SYS_CHROOT : システムコールchroot(2)
の使用
CAP_KILL : システムコールkill(2)
の使用
CAP_AUDIT_WRITE : カーネル監査ログへの書き込み
chrootと仕上げ
ここまできたら、もう一息chrootと最後の仕上げをします。 chrootとは、現在のプロセスとその子プロセスのルートディレクトリを変更する仕組みです。chrootでルートディレクトリを変更されたプロセスは、その範囲外のディレクトリにはアクセスすることができなくなります。
今回はchrootではdebootstrapで用意したDebian基本システムが入っているdebian-containerディレクトリをルートディレクトリとします。
Namespace.unshare(Namespace::CLONE_NEWPID) pid = Process.fork do rate = Cgroup::CPU.new "chikuwait" core = Cgroup::CPUSET.new "chikuwait" rate.cfs_quota_us = 200000 rate.cfs_period_us = 100000 core.cpus = "0−1" core.mems = "0" rate.create core.create rate.attach core.attach Namespace.unshare(Namespace::CLONE_NEWUTS) Namespace.unshare(Namespace::CLONE_NEWIPC) Namespace.unshare(Namespace::CLONE_NEWNS) Namespace.unshare(Namespace::CLONE_NEWNET) c = Capability.new cap = [Capability::CAP_CHOWN, Capability::CAP_DAC_OVERRIDE, Capability::CAP_FSETID, Capability::CAP_FOWNER, Capability::CAP_MKNOD, Capability::CAP_NET_RAW, Capability::CAP_SETGID, Capability::CAP_SETUID, Capability::CAP_SETPCAP, Capability::CAP_NET_BIND_SERVICE, Capability::CAP_SYS_CHROOT, Capability::CAP_KILL, Capability::CAP_AUDIT_WRITE] c.set Capability::CAP_PERMITTED, cap c.set_flag Capability::CAP_EFFECTIVE, cap, Capability::CAP_SET Dir.chdir("/home/user/debian-container") Dir.chroot("/home/user/debian-container") system 'hostname debian' system 'ip link set lo up' system "mount -t proc proc /proc" Kernel.exec '/bin/bash' end Process.waitpid2(pid)
最後の仕上げとしてhostnameを「debian」に変更、procをマウントなどをして完成! これで最低限動くコンテナが完成しました。
おわりに
今回作ったコンテナでは、ネットワーク周りの設定はちゃんとしていないため外と通信をするために別途操作が必要ですが、最低限コンテナとして動くものとなっています。最近流行りのコンテナ型仮想化技術、自分で作ってみるとなかなか楽しいですね。Linuxの仕組み・ファイルシステムなどに触れることも出来てとても良い勉強になりました。これを気に、コンテナ型仮想化技術にもうちょっと詳しくなりたいなと思った次第。エンジニアの教養として、自作言語・自作OSなどがよく上げられますが、自作コンテナも教養として良いんじゃないかなって思います。