Haconiwa を改めて紹介する

OSS紹介 Advent Calendar 2017 にかこつけて、自作オーエスエスを紹介していくスタンス。ということで20日の記事です。

Haconiwaは、いわゆるLinuxコンテナランタイムです。コンテナといえばクジラっぽいロゴのやつが有名ですが、Haconiwaは[mruby](https://github.com/mruby/mruby)を組み込んでおり、コンテナに関するLinuxカーネルの機能を自分で組み合わせて使える点、フック機構など様々なAPIにより自分自身を拡張できる点、に特徴があります。

リリースして [RubyKaigi 2016](http://rubykaigi.org/2016/presentations/udzura.html) でおしゃべりしてから大体1年（雑...）経っているのですが、改めて紹介してみます。みるぞ！

## インストール

手元にUbuntu Zestyの環境があるので雑にインストールします（Xenialならリポジトリ追加もできますがLTS以外は用意していなかった...）

```console
$ wget --content-disposition https://packagecloud.io/udzura/haconiwa/packages/ubuntu/xenial/haconiwa_0.8.9-1_amd64.deb/download.deb
$ sudo apt install ./haconiwa_0.8.9-1_amd64.deb
```

```console
$ haconiwa version
haconiwa: v0.8.9
```

今日(!!)リリースした0.8.9が最新です。

## 試しに動かす

Haconiwaの設定ファイル兼Ruby DSLを `Hacofile` と呼んでいます。[^1]: 一部でしか呼んでいない雰囲気があるので呼んでいきたい。

```console
$ haconiwa new qiita.haco                                                                                                    
assign  new haconiwa name = haconiwa-9bdfc517                                                                                                     
assign  rootfs location = /var/lib/haconiwa/9bdfc517                                                                                              
create  qiita.haco
```

作られた `qiita.haco` は文法としては普通のRubyのスクリプトです。bootstrap/provisionのブロックを以下のように変更してみます。

```ruby:qiita.haco
  config.bootstrap do |b|
    b.strategy = "debootstrap"
    b.variant = "minbase"
    b.debian_release = "stretch"
  end

  config.provision do |p|
    p.run_shell <<-SHELL
apt -y update
apt -y install procps iproute2 ruby
    SHELL
  end
```

この状態で `sudo haconiwa create qiita.haco` と打つと、DSLに沿ってコンテナのrootfsの作成とプロビジョニングをしてくれます。

なおこの設定の場合は、debootstrapコマンドを使うので事前インストールしておいてください。

<img width="1028" alt="screen-qiita.png" src="https://qiita-image-store.s3.amazonaws.com/0/569/734015b1-dccf-0666-2d8a-311519eb44b8.png">

rootfsの作成が完了したらコンテナに入ってみます。

```console
$ sudo haconiwa run qiita.haco -T -- /bin/bash
Create lock: #<Lockfile path=/var/lock/.haconiwa-9bdfc517.hacolock>
Container fork success and going to wait: pid=11736
root@haconiwa-9bdfc517:/# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.3  18164  3180 ?        S    09:32   0:00 /bin/bash
root         2  0.0  0.2  36632  2836 ?        R+   09:32   0:00 ps auxf
root@haconiwa-9bdfc517:/#
```

PIDも `1` から開始しているし、コンテナですね。やった！

## コンテナ機能を試す

コンテナの機能を「個別に」有効にして検証してみるということをします。

### cgroup

以下のような設定を追記します。

```ruby
  config.cgroup["cpu.cfs_period_us"] = 100000
  config.cgroup["cpu.cfs_quota_us"]  =  30000
  config.cgroup["pids.max"] = 128
```

この設定を入れてコンテナに入って、Rubyなどで無限にフィボナッチ数を計算させてみます。

```console
$ sudo haconiwa run qiita.haco -T -- /bin/bash
Create lock: #<Lockfile path=/var/lock/.haconiwa-9bdfc517.hacolock>
Container fork success and going to wait: pid=11910
root@haconiwa-9bdfc517:/# ruby -e \
  'def fib(n);n<2?1:fib(n-2)+fib(n-1);end;loop{fib rand(32)}'
```

しかし、cgroupの制限が効いているので、 30000us/100000us の30%までしかCPU利用率が上がらないことがわかります。

<img width="690" alt="スクリーンショット 2017-12-20 18.36.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/569/d7b7d2c4-1d6e-a4aa-dc2c-947c88942dbe.png">

また、プロセス数の上限も制限をしているので、このコンテナでfork bomb攻撃（危ないのでコードの掲載はしません:( ）を実行してみても、無事に攻撃が収まることがわかります。 `pids.max` の制限をしていない場合 **ホストごと落ちる** ので気をつけましょう。

```console
## fork bomb コマンドを発行
...
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable
bash: fork: Resource temporarily unavailable

root@haconiwa-9bdfc517:/# ps auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.3  18300  3316 ?        S    09:38   0:00 /bin/bash
root       828  0.0  0.2  36632  2904 ?        R+   09:40   0:00 ps auxf
```

### seccomp

seccompも試せます。以下のようなDSLを書いて、またコンテナを立ち上げてみます。

```ruby
  config.seccomp.filter(default: :allow) do |rule|
    rule.kill :mkdir
    rule.kill :fchownat
  end
```

すると、見た目は普通のbashセッションですが、このコンテナの中では、rootであるにもかかわらず `mkdir` の操作や `chown` の操作などが一切できなくなっていることがわかります。

```console
root@haconiwa-9bdfc517:/# touch hoge.txt                                 
root@haconiwa-9bdfc517:/# chown www-data hoge.txt 
Bad system call
root@haconiwa-9bdfc517:/# mkdir /tmp/test
Bad system call
```

このように、Haconiwaでは特定のシステムコール呼び出しを制限するサンドボックス環境を作ることができ、そのホワイトリスト/ブラックリストをRubyで記述可能です。

### ネットワーク機能

今日のリリースで入った機能です。ブリッジとveth/network namespaceを用いいてコンテナに仮想的なネットワークを割り振ることができます。

最初にブリッジが必要なので、以下のコマンドで生成しましょう。

```console
$ sudo haconiwa init --bridge --bridge-ip=10.254.254.1/24
Command success: ip link add haconiwa0 type bridge exited 0
Command success: ip addr add 10.254.254.1/24 dev haconiwa0 exited 0
Command success: ip link set dev haconiwa0 up exited 0
```

デフォルトでは `haconiwa0` というブリッジができます。

```console
$ ip a s haconiwa0
5: haconiwa0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UNKNOWN group default qlen 1000
    link/ether 56:98:fb:4e:2f:3b brd ff:ff:ff:ff:ff:ff
    inet 10.254.254.1/24 scope global haconiwa0
       valid_lft forever preferred_lft forever
    inet6 fe80::5498:fbff:fe4e:2f3b/64 scope link 
       valid_lft forever preferred_lft forever
```

このブリッジの `10.254.254.1/24` の範囲で一つIPを決め、Hacofileで設定してみましょう。

```ruby
  config.network.container_ip = "10.254.254.2"
  config.network.namespace = config.name
```

このHacofileでコンテナを立ち上げると、ちゃんと `10.254.254.2` が割り当てられたコンテナになり、またホストのネットワークとちゃんと分離されることがわかります。

```console
root@haconiwa-9bdfc517:/# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
10: e6d4d690_g@if11: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000
    link/ether 02:6b:df:0e:d9:38 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 10.254.254.2/24 scope global e6d4d690_g
       valid_lft forever preferred_lft forever
    inet6 fe80::6b:dfff:fe0e:d938/64 scope link 
       valid_lft forever preferred_lft forever
```

----

Haconiwaには他にも様々な機能（特に、便利なフック機能など...）がありますが、無限に紹介記事が長くなってしまいそうなので、この辺りにしておきます。

ぜひいろいろ試してみてください！