Man page of CHIKU_WAIT(2)

システムソフトウェアとポエムの差が激しいので,高山病に注意

HaconiwaとmrubyスクリプトでLinuxコンテナをつくる

tech.pepabo.com インターン中に理解を深めるために作ったLinuxコンテナについて書きます。chikuwaitです。

最近よくコンテナ型仮想化技術の名前よく聞きますよね。とりあえずDocker使うみたいな流れがあってとてもベンリ!みたいな。 漠然と普段使ってますけど、中身ちょっと知りたくありません?ということでとりあえず作ってみましょう。そんなに難しくない。

Linuxコンテナなあに

そもそもコンテナ型仮想化技術というのは単一の技術・仕組みではなくて、Linuxの複数の機能(Linux Namespaces、cgroup、chrootなどなど)を組み合わせて作られています。これらの機能を組み合わせOSの中に隔離した環境を用意する、という感じですね。コンテナ仲良くなれそう!

インターン中となりで Uchio Kondo 🦉⬛️ (@udzura) | Twitter さんが作っていた資料が初めての人にも良さそうだったので、これを見ると幸せになれるかもしれません。

Haconiwaについて

github.com

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の使用率を確認します。 f:id:chikuwa_it:20171004194316p:plain

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のファイルの属性や管理情報を格納している管理領域のことです。

f:id:chikuwa_it:20171004201719p:plain

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-IDset-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などがよく上げられますが、自作コンテナも教養として良いんじゃないかなって思います。