haconiwa をお試ししてみた
Yet another container, haconiwa.
そもそもコンテナってなんじゃ
「コンテナ」というと真っ先に「Docker」が思い浮かびますね。
軽いつかみとして用途を一つ上げるとすると、アプリの開発環境を用意するのに いちいちVM上げて、サービスが上がってくるの待つのがだるいから コンテナで環境を用意してその中でアプリを起動するみたいな感じに使います。
その実は、 Linux Kernel の機能を駆使して、実行環境を隔離する技術です。
主に下記のような技術が利用されています。
1つ1つを掘り下げると大変なことになるので「フワッ」っとご紹介します。
chroot
Change Root の名前の通り、指定したプロセスとその配下の子プロセスに対して、
仮想的にルートディレクトリを変更する機能です。
例えば bash
を起動するときに chroot で /var/www/virtual_home/
が
ルートディレクトリに見えるように指定すると、
起動した bash
上で cd /
を実行したときに 実際にアクセスするのは
/var/www/virtual_home/
というようにすることができます。
言い換えると隔離された環境から /var/www/virtual_home/
より上の
ディレクトリは見えなくなります。
ルートディレクトリが変わるということは、chroot 後に
アプリを起動するためのライブラリなどの資産は全て chroot 後の
ディレクトリ配下に揃っている必要がある点に注意しましょう。
namespace
C++, C# などでも出てくる namespace
と同じく、Linux の namespace
はプロセスの名前空間を定義します。
namespace
を利用すると任意のプロセスを、大本の環境から仮想的に隔離した状態で実行することができます。
例えば bash
を initプロセス(PID:1) として起動し子プロセスをぽこぽこ起動するということができます。
namespace
が隔離できるのは PIDだけでなく、ユーザーID、グループID、マウントポイントなど他にもありオプションで指定可能です。
コンテナのメリットって?
複数VMを起動するというようなシーンでは、Linux Kernel の機能を使って仮想的に実行環境を隔離するため、
個々のVMで動くOS分のリソースを削減することができます。(よく見るあの図がそういう理解になります。)
その為、コンテナの起動と言うのはVMに比較して基本的に早くなります。
また namespace
でプロセスが分離されているため別途 cgroup (リソース制限を行う仕組み)
などでコンテナに割り当てるCPUやメモリの制限を行うことができますし、
適切な運用がなされていれば chroot で専用のディレクトリ配下で動くので
大本のシステムに影響が出ることは少ないです。
コンテナのデメリットって?
「Linux Kernel の機能を使っての仮想的な実行環境の隔離」といっている手前、
OSを上げる必要の無い分大本のKernel部分は各コンテナと共有されます。
その為特定のコンテナで Liunx Kernel に対して
特殊なパッチを当てる必要があるようなケースについては、
コンテナを実行する環境を別に用意する必要が出たりします。
「haconiwa」 とはなんぞ
id:udzura さんが主導開発されている mruby によるコンテナ実装です。
基本的には Docker と同じく chroot や namespace、cgroup を利用してコンテナを構築することができます。
Docker との違いは Dockerfile のような独自のシンプルなDSLではなく、
mRuby をつかってコンテナを記述することができる点です。
実装は mruby-cli
を利用して行われており、chroot、namespace、cgroupを設定できる権限があれば
今のところは、Linux環境下で公式で配布されているバイナリを配置するだけで利用することができます。
(一部機能には LXC のインストールが必要ですが必須ではありません。今回はこの機能を使います)
https://github.com/haconiwa/haconiwa/releases から取得できるバイナリは3種類です。
基本的には haconiwa
を入り口として触っていくことになります。
「haconiwa」 でコンテナを作ってみよう
コマンドラインについて
ひとまずコマンドラインのオプションを見てみましょう。こんな感じです。
# haconiwa -h haconiwa - The MRuby on Container commands: new - generate haconiwa's config DSL file template create - create the container rootfs provision - provision already booted container rootfs archive - create, provision, then archive rootfs to image start - run the container attach - attach to existing container reload - reload running container parameters, following its current config kill - kill the running container version - show version revisions - show mgem/mruby revisions which haconiwa bin uses Invoke `haconiwa COMMAND -h' for details.
new やら create やら色々オプションがありますね。
コンテナの設定を作るには new
を指定するようです。
# haconiwa new -h -n, --name=CONTAINER_NAME Specify the container name if you want -r, --root=ROOTFS_LOC Specify the rootfs location to generate on host -G, --global Create global config /etc/haconiwa.conf.rb -h, --help Show help HACO_FILE Put the config file at the end of command
コンテナの設定ファイルを作る
物は試しに叩いてみましょう
# haconiwa new test001.haco assign new haconiwa name = haconiwa-44bb30b9 assign rootfs location = /var/lib/haconiwa/44bb30b9 create test001.haco
haconiwa-44bb30b9
という名前の、/var/lib/haconiwa/44bb30b9
rootfs にするような
コンテナの設定ファイルが出来上がったみたいです。
せっかくなので中身を見てみましょう。(コメントは省略)
$ cat test001.haco Haconiwa.define do |config| # コンテナ名 (uname -n とかで出てくる名前) config.name = "haconiwa-44bb30b9" # Initプロセスの指定 config.init_command = "/bin/bash" # rootfs の指定 (chroot する先) root = Pathname.new("/var/lib/haconiwa/44bb30b9") config.chroot_to root # コンテナ構築の起点の指定 config.bootstrap do |b| b.strategy = "lxc" # LXC-Create を使って b.os_type = "alpine" # Alpine Linux のコンテナを作成する end # プロビジョニングの設定 (apk を使って bash をインストール) config.provision do |p| p.run_shell <<-SHELL apk add --update bash SHELL end # その他マウントの設定やnamespaceの設定など (今回は省略) 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.namespace.unshare "mount" config.namespace.unshare "ipc" config.namespace.unshare "uts" config.namespace.unshare "pid" end
お、なんだか Vagrantfile に雰囲気が似てますね。
読めばなんとなくわかりそうな感じがします。
補足するとデフォルトではコンテナの構築時に lxc-create という
Linux Containers ( Dockerとは別のコンテナ技術 ) の機能を使い Alpine Linux の rootfs を作ることができます。
b.strategy = "lxc"
という指定がそれですね。OSの指定は b.os_type = "alpine"
という指定です。
もうちょっとだけ掘り下げると Linux Containers には
もともと他のOSのコンテナを作るためのテンプレートがあり
lxc-create
コマンドに使いたいテンプレートを渡すとそれに準じて
コンテナ用のrootfsを構築してくれるというわけです。
# ls /usr/share/lxc/templates
lxc-alpine lxc-busybox lxc-debian lxc-gentoo lxc-oracle lxc-sparclinux lxc-ubuntu-cloud
lxc-altlinux lxc-centos lxc-download lxc-openmandriva lxc-plamo lxc-sshd
lxc-archlinux lxc-cirros lxc-fedora lxc-opensuse lxc-slackware lxc-ubuntu
デフォルトだとこれだけあります。
haconiwaに対し b.strategy = "lxc"
、b.os_type = "alpine"
を指定すると
lxc-create に lxc-alpine
を使うように指定して rootfs を作る というように読み替えることができます。
他にも strategy に指定できるものがあり debootstrap
を指定すると
debian公式の機能を使ってコンテナを作れるほか、
git
や tarball
を指定すれば gitリポジトリ や tar を展開して
rootfs としたり、shell
や mruby
を指定すれば任意の処理に移譲して
rootfs を作ることができるようです。
ちなみに haconiwa そのものには Docker のような
レイヤー構造を持ったイメージという概念は今のところ存在しません。
そういうところについては別途ファイルシステムなどに移譲しているようで、
あくまでシンプルなコンテナの実装なんだという認識でいると良さそうです。
そのため Data Volume Container を用意してマウントしてあげないとデータを永続化できない
というようなことは基本的にはなく、Vagrant と Docker のいいとこ取りができてそうに感じました。
コンテナを構築して実行する
というわけでまずコンテナを構築してみましょう。
haconiwa new
で作成したファイルを指定して haconiwa create
を実行してあげます。
# haconiwa create test001.haco Creating rootfs of haconiwa-44bb30b9... Start bootstrapping rootfs with lxc-create... [bootstrap.lxc]: Obtaining an exclusive lock... done [bootstrap.lxc]: [bootstrap.lxc]: ==> Fetching and/or verifying APK keys # (中略) lxc-create を使って /var/lib/haconiwa/44bb30b9 に必要なファイルを作っていく Command success: /bin/sh -xe exited 0 Success!
無事できたようですね。
早速起動してみましょう。 haconiwa start
で起動することができます。
今回は init_command に bash が指定されているので起動すると
コンテナ上で動作する bash にターミナルが移ります。
# haconiwa start test001.haco Container fork success and going to wait: pid=2742 # コンテナ名が出てくる bash-4.3# uname -n haconiwa-44bb30b9 # bash が PID 1 で起動し initプロセスになっていることがわかる bash-4.3# ps PID USER TIME COMMAND 1 root 0:00 /bin/bash 10 root 0:00 ps
良さそうですね。実際にRailsアプリを動かしたりするシーンでは
nginx を initプロセスに指定して起動するか、あるいは supervisord を起動して、
さらに配下に必要なプロセスを起動させることになるとおもいます。
ちなみに 起動したコンテナは Ctrl+D で終了することで抜けることができます。
「haconiwa」のフック機能
haconiwa独特の機能として、mrubyによるフックの実装と言うものがあります。
より具体的に言及すると、
- 「コンテナを起動する前にネットワークの設定をいじりたい」
- 「コンテナを終了した後にメールを飛ばしたい」
- 「コンテナの実行中に1分ごとに処理を実行したい」
- 「特定のシグナルを受けたときに処理を実行したい」
というような機能を実装することができます。
これも試しに使ってみましょう。
作成されている test001.haco
の最後に下記のようなブロックを定義します。
起動後3秒待って 1秒おきに画面に出力するようにしてみます
cnt = 0 config.add_async_hook(sec: 3, interval_msec: 1 * 1000) do |base| cnt = cnt + 1 p("async hook called #{cnt} times [#{base.pid}]") end
では早速起動してみましょう
# haconiwa start test001.haco Container fork success and going to wait: pid=2791 bash-4.3# Async hook starting... "async hook called 1 times [2791]" Async hook starting... "async hook called 2 times [2791]" Async hook starting... "async hook called 3 times [2791]" Async hook starting... "async hook called 4 times [2791]" Async hook starting... "async hook called 5 times [2791]" Async hook starting... "async hook called 6 times [2791]" exit Container(2791) finish detected: #<Process::Status: pid=2791,exited(0)> Container successfully exited: #<Process::Status: pid=2791,exited(0)> One of supervisors finished: 2790, #<Process::Status: pid=2790,exited(0)>
うまくいきましたね。
このように hacoファイルで好きなフックを書いてコンテナの動きをどんどん拡張していくことができます。
他にどんなフックが有るかは 公式のREADME 参考にすると良さそうです。
まとめ
今回はコンテナがどういう風に実現されているかをおさらいしたほか、
注目の haconiwa についてお試ししてみました。
使い勝手としては良いなーという感想を持ったとこで、
デフォルトだとデータの永続化を考えなくても使えるあたり
普段使いだとチョッパヤなVagrantの代替え的な扱いがし易いのではないかと思いました。
(Linux環境以外だと Docker for Windows/Mac 的な何かが別途必要ですが。)
※ またこの記事、触り始めたばかりなので間違ってたらごめんなさい (気がついたら修正します)