Docker の低レベルランタイムを Nabla Container に入れ替え、専用イメージもビルドして動かしてみる

## 要約
- コンテナの作成方法は Docker 標準 (containerd + runc) だけではなく、複数存在する。
- その多くがホストとのカーネル共有から脱却して安全にコンテナを使う方法を検討しており、異なる手法で実現を目指している。
- Unikernel を使って実現しようとしている IBM の Nabla Container の導入～専用イメージのビルドを実際にやってみました。

コンテナランタイムも runnc も知ってる！という方は以下から。
[Nabla Containers のインストール](https://qiita.com/thirdpenguin/private/1cdeacda439dbe14eafe#nabla-containers-%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB)
コンテナランタイムは分かるけど runnc って何？という方は以下から。
[Nabla Containers (runnc) とは](https://qiita.com/thirdpenguin/private/1cdeacda439dbe14eafe#nabla-containers-runnc-%E3%81%A8%E3%81%AF)
コンテナランタイムって何？という方は以下から。
[Docker 標準 (containerd + runc) 以外のコンテナ作成方法](
https://qiita.com/thirdpenguin/private/1cdeacda439dbe14eafe#docker-%E6%A8%99%E6%BA%96-containerd--runc-%E4%BB%A5%E5%A4%96%E3%81%AE%E3%82%B3%E3%83%B3%E3%83%86%E3%83%8A%E4%BD%9C%E6%88%90%E6%96%B9%E6%B3%95)

## Docker 標準 (containerd + runc) 以外のコンテナ作成方法
コンテナの作成・管理を直接的に担っているのはコンテナランタイムと呼ばれる部分です。
多くのコンテナランタイムは次の二層から成ります。

- 高レベルランタイム: ユーザや Kubernetes のようなコンテナオーケストレータからの命令を受け取り、低レベルランタイムへ指示を出す。
- 低レベルランタイム: 高レベルランタイムからの指示を受け取り、コンテナ環境を実際に作成する。

Dockerのデフォルトで採用されているコンテナランタイムは以下の通りです。

- 高レベルランタイム: containerd
- 低レベルランタイム: runc

これ以外にも様々なコンテナランタイムが存在します。
高レベルランタイムと低レベルランタイムの間のやり取りは OCI という規格で定義されているので、入れ替えて使うこともできます。
主要なランタイムと特色については以下を参照してください。
http://c.itdo.jp/?p=2653

## なぜランタイムを入れ替えるのか
多くの場合はセキュリティのためだと思います。
runc はコンテナとホストがカーネルを共有しているため、コンテナが乗っ取られるとホストに被害範囲が拡大する恐れがあります。
Docker デフォルト以外のランタイムはこの問題の抜本的な解決のため、ホストとカーネルを共有しない方向でランタイムの改善を図っています。

## Nabla Containers (runnc) とは
- Unikernel を使ってホストとカーネルを共有せずにコンテナをより安全に動作させるランタイム。runnc とも呼ばれる。
- ざっくり言うと Unikernel とは、アプリを動作させるための最低限の機能のみを持ち、 1:1 で紐づくカーネルのこと。
- コンテナライフサイクルのパフォーマンスに優れ、アプリケーションの動作パフォーマンスも比較的良好な一方で、専用イメージでしかコンテナを実行できないなど機能面に大きな制約がある。(具体的な制約は [Nabla Containers の Github リポジトリ](https://github.com/nabla-containers/runnc#limitations) から確認できます。)

## Nabla Containers のインストール
### 環境
- OS: Ubuntu 18.04
- Docker: 19.03.8
- git: 2.17.1
- gcc: 6.5.0

### 実行手順
####1. Go のインストール

参考: https://github.com/golang/go#download-and-install
　
1. Go のバイナリをダウンロード。

```bash
# ver.1.14.2 は 2020/05/12 時点の最新安定版です。
$ wget https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz
```
　
2. /usr/local 下へ解凍。

```bash
$ tar -C /usr/local -xzf go1.14.2.linux-amd64.tar.gz
```
　
3. 環境変数の設定。

```bash
$ export PATH=$PATH:/usr/local/go/bin
$ export GOPATH=$HOME/go
```

####2. Nabla Containers のインストール

参考: https://github.com/nabla-containers/runnc#getting-started-with-the-go-repo
　
1. リポジトリを取得。

```bash
$ go get github.com/nabla-containers/runnc
```

どうやら runc の github リポジトリから依存パッケージを引っ張ってきているらしいですが、依存先から一部のパッケージが消失しておりこんなメッセージが出ますがこの後の作業には差し支えませんでした。

```bash
package github.com/opencontainers/runc/libcontainer/label: cannot find package "github.com/opencontainers/runc/libcontainer/label" in any of:
        /usr/local/go/src/github.com/opencontainers/runc/libcontainer/label (from $GOROOT)
        /home/yasuda/go/src/github.com/opencontainers/runc/libcontainer/label (from $GOPATH)
```
　
2. ホストに genisoimage をインストール。

```
$ sudo apt install genisoimage。
```
　
3. ホストに jq をインストール。

```
$ sudo apt install jq
```
　
4. コンテナを使ってインストール。

Docker があれば、コンテナ内でビルドした成果物をホストへコピーする形でインストールができます。

```bash
$ cd $GOPATH/src/github.com/nabla-containers/runnc
$ make container-build
$ make container-install
```
　
5. libseccomp-dev をインストール。

```bash
$ sudo apt install libseccomp-dev
```
　
6. /etc/docker/daemon.json を編集して docker デーモン起動時のオプションを追加。

```bash
$ vi /etc/docker/daemon.json
```

```bash:/etc/docker/daemon.json
{
    "runtimes": {
        "runnc": {
                "path": "/usr/local/bin/runnc"
        }
    }
}
```

※他のランタイムと併用したい場合、以下のように , で区切る必要があります。

```bash:/etc/docker/daemon.json(複数のランタイムを併用する場合)
{
    "runtimes": {
        "runnc": {
                "path": "/usr/local/bin/runnc"
        },
        "runsc":{
                "path": "/usr/local/bin/runsc"
    }
}
```
　
7. docker デーモンを再起動。

```bash
$ systemctl restart docker
```
　
8. テストイメージを実行。

```bash
$ sudo docker run --rm --runtime=runnc nablact/nabla-node-base:v0.3
nabla-run arg [/opt/runnc/bin/nabla-run --x-exec-heap --mem=512 --net=tap1123274cef1a --disk=/var/run/docker/runtime-runnc/moby/1123274cef1a51a3405f359b4b27f02df95ddcee60dfeafda8c6d10cd187b923/rootfs.iso /var/lib/docker/overlay2/94626cd9623c92c1c5820b39aaf0c369d732ec856ee37a491b349e3679e45951/merged/node.nabla {"env":"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin","env":"HOSTNAME=1123274cef1a","cmdline":"/var/lib/docker/overlay2/94626cd9623c92c1c5820b39aaf0c369d732ec856ee37a491b349e3679e45951/merged/node.nabla","net":{"if":"ukvmif0","cloner":"True","type":"inet","method":"static","addr":"172.18.0.2","mask":"16","gw":"172.18.0.1"},"blk":{"source":"etfs","path":"/dev/ld0a","fstype":"blk","mountpoint":"/"},"cwd":"/"}]
nabla-run: WARNING: The use of --x-exec-heap is dangerous and not recommended as it makes the heap and stack executable.
nabla-run: /var/lib/docker/overlay2/94626cd9623c92c1c5820b39aaf0c369d732ec856ee37a491b349e3679e45951/merged/node.nabla: Warning: phdr[0] requests WRITE and EXEC permissions
            |      ___|
  __|  _ \  |  _ \ __ \
\__ \ (   | | (   |  ) |
____/\___/ _|\___/____/
Solo5: Memory map: 512 MB addressable:
Solo5:   reserved @ (0x0 - 0xfffff)
Solo5:       text @ (0x100000 - 0xb0f377)
Solo5:     rodata @ (0xb0f378 - 0xdd3bef)
Solo5:       data @ (0xdd3bf0 - 0x1018aa7)
Solo5:       heap >= 0x1019000 < stack < 0x20000000
rump kernel bare metal bootstrap

[   1.0000000] Copyright (c) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005,
[   1.0000000]     2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017,
[   1.0000000]     2018 The NetBSD Foundation, Inc.  All rights reserved.
[   1.0000000] Copyright (c) 1982, 1986, 1989, 1991, 1993
[   1.0000000]     The Regents of the University of California.  All rights reserved.

[   1.0000000] NetBSD 8.99.25 (RUMP-ROAST)
[   1.0000000] total memory = 247 MB
[   1.0000000] timecounter: Timecounters tick every 10.000 msec
[   1.0000080] timecounter: Timecounter "clockinterrupt" frequency 100 Hz quality 0
[   1.0000090] cpu0 at thinair0: rump virtual cpu
[   1.0000090] root file system type: rumpfs
[   1.0000090] kern.module.path=/stand/amd64/8.99.25/modules
[   1.0200090] mainbus0 (root)
[   1.0200090] timecounter: Timecounter "bmktc" frequency 1000000000 Hz quality 100
[   1.0200090] ukvmif0: Ethernet address 06:bb:07:e1:d1:56
[   1.0662231] /dev//dev/ld0a: hostpath XENBLK_/dev/ld0a (45002 KB)
mounted tmpfs on /tmp

=== calling "/var/lib/docker/overlay2/94626cd9623c92c1c5820b39aaf0c369d732ec856ee37a491b349e3679e45951/merged/node.nabla" main() ===

rumprun: call to ``_sys___sigprocmask14'' ignored
rumprun: call to ``sigaction'' ignored
Hello, Rump!!

=== main() of "/var/lib/docker/overlay2/94626cd9623c92c1c5820b39aaf0c369d732ec856ee37a491b349e3679e45951/merged/node.nabla" returned 0 ===

=== _exit(0) called ===
[   1.2419573] rump kernel halting...
[   1.2419573] syncing disks... done
[   1.2419573] unmounting file systems...
[   1.2421135] unmounted tmpfs on /tmp type tmpfs
[   1.2421135] unmounted /dev//dev/ld0a on / type cd9660
[   1.2421135] unmounted rumpfs on / type rumpfs
[   1.2421135] unmounting done
halted
```

お疲れさまでした。インストールは終了です。

## 専用イメージをビルドする

参考: https://github.com/nabla-containers/nabla-base-build

1. ローカルリポジトリを用意。

```bash
$ mkdir localrepo
$ cd localrepo
$ git init
```
　
2. Nabla Containers が公開する、nabla-base-build リポジトリをクローン。

```
$ git clone https://github.com/nabla-containers/nabla-base-build.git
```
　
3. サブモジュールをフェッチ。

```bash
$ ls -la
$ cd nabla-base-build
$ git submodule update --init --recursive
```
　
4. zlib1g-devとlibseccomp-devをインストール。

```bash
$ apt install zlib1g-dev libseccomp-dev
```
　
5. gcc のバージョンが 5 か 6 であることを確認。

```
gcc --version
```
　
6. gcc のバージョンが 5 か 6 でない場合、 6.5 をインストール。

内部では [rumprun](https://github.com/rumpkernel/rumprun) が Unikernel を提供しているのですが、7 以上のバージョンだと rumprun のビルドが失敗します。

```bash
$ sudo apt-get update && \
sudo apt-get install build-essential software-properties-common -y && \
sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y && \
sudo apt-get update && \
sudo apt-get install gcc-6 g++-6 -y && \
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-6 60 --slave /usr/bin/g++ g++ /usr/bin/g++-6 && \
$ gcc -v
```

7.gcc-6 を明示的に指定。

```bash
$ export CC=gcc-6
```
　
8. pcre のダウンロード元を変更。

pcre のダウンロード元として指定されている ftp.csx.cam.ac.uk ですが、現在は存在していません。
代わりになるダウンロード先を指定します。

```bash
$ vi rumprun-packages/pcre/Makefile
```

```bash:rumprun-packages/pcre/Makefile
include ../Makefile.inc
#UPSTREAM=ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/pcre-8.40.tar.gz #この行をコメントアウト
UPSTREAM=ftp://ftp.pcre.org/pub/pcre/pcre-8.40.tar.gz # この行を追加
```
　
9. イメージをビルド。

インストール可能なイメージは以下の通り。

- nginx-base
- node-base
- python3-base
- redis-base
- go-base

```bash
# 全てのイメージをビルドしたい場合
$ make world
# または、特定のイメージのみビルドしたい場合
$ make -C nginx-base
```
　
10. イメージを確認。

```
$ sudo docker images
```
　
11. nginx コンテナを実行して実際にブラウザから接続してみる。

```
$ sudo docker run --rm -d -p 80:80 --name nabginx nabla-nginx-base
```

docker ホストのポート80番へ接続すれば nginx コンテナの同ポートへ転送されます。

![nabginx.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/320878/e4b183f2-8ca2-2d20-efcd-31ccd1e8666e.png)


## おわりに
この後、対話的シェルを呼び出してバージョンを確認しようと思ったのですが、 [Nabla Containers の制約](https://github.com/nabla-containers/runnc#limitations) を読む限り docker exec や docker run -it などコンテナ作成後に任意のコマンドを実行する手段に対応していないようだったので断念しました。今の段階で運用するには機能面の制約が大きく感じますが、Unikernels は数あるカーネル分離へのアプローチの中でも光るものがある技術であると感じます。今後の動向にも着目していきたいと思います。

ちなみに記事中でも触れたパフォーマンス面ですが、弊社 web サイトにて Redis-benchmark と Apache benchmark を使った runc との動作パフォーマンス比較があるので、よろしければご参照ください。
http://c.itdo.jp/?p=2653#nablavsrunc