Skip to content

🐈 Nekonote.

haconiwa をお試ししてみた

May 03, 2017

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本体 Cli実装
  • hacoirb
    • haconiwaが組み込まれたirbコマンド相当の実装
  • hacorb
    • haconiwaが組み込まれたrubyコマンド相当の実装

基本的には 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公式の機能を使ってコンテナを作れるほか、 gittarball を指定すれば gitリポジトリ や tar を展開して rootfs としたり、shellmruby を指定すれば任意の処理に移譲して 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 的な何かが別途必要ですが。)

※ またこの記事、触り始めたばかりなので間違ってたらごめんなさい (気がついたら修正します)


関連リンク


忘備録や調べたことなどを気が向いたときに書いたりします。