Firecrackerを利用したコンテナランタイムfirecracker-containerdを試してみる

CX事業本部@大阪の岩田です。

Firecrackerを使ったコンテナランタイムfirecracker-containerdを試したので、簡単にご紹介します。

環境

今回利用した環境です

  • OS Ubuntu Server 18.04 LTS (HVM), SSD Volume Type - 64 ビット (x86) ami-04b9e92b5572fa0d1
  • インスタンスタイプ i3.metal
  • Firecracker v0.19.1
  • firecracker-containerd コミットID:fe2ba1b6091a61173eb7bb5d8ec2c9ad7b15f98f

本当は前回構築したラズパイ4の環境で動かしたかったのですが、MicroVM内のエージェントとホスト側が通信できずうまくいきませんでした。また時間を見つけて再チャレンジしようと思います。

firecracker-containerdとは?

Docker等で利用されているコンテナランタイムcontainedを拡張し、FirecrackerのMicroVMにコンテナ環境を構築するソフトウェアです。通常のコンテナ型仮想化技術は、ざっくり言ってしまうとプロセスの分離で、ホストOSとカーネルを共有しています(Docker for MacやKata container等、話を広げていくとキリがないので通常のコンテナ型仮想化技術というぼやかした表現をしています)。そのためオーバーヘッドが小さいというメリットがありますが、

  • ホストOSと同じカーネルしか使えない
  • 仮想環境間の分離レベルが小さい

といったデメリットもあります。

firecracker-containerdを利用すると、FirecrackerのMicroVM内にコンテナを作成することが可能になります。FirecrackerのMicroVMは非常に高速に起動するため、オーバーヘッドが小さいというコンテナ型仮想化のメリットを維持しつつも、コンテナ間の高度な環境分離を実現することができます。こういったセキュリティ面のメリットから、Fargateの基盤でもセキュリティレベルを向上させるためにfirecracker-containerdが採用されていることが知られています。

[レポート] AWS Fargate under the hood #reinvent #CON423]

Lambdaの基盤でfirecracker-containerdが採用されているという明示的なドキュメントは見つけられませんでしたが、LambdaのアーキテクチャとしてFirecrackerのMicroVM上にサンドボックス環境(コンテナ)を構築することは知られており、Lambdaの基盤でも恐らくfirecracker-containerdが利用されているのかなー?と想像しています。

こちらは昨年末のre:invent2019でのfirecracker-containerdに関するセッションです

こちらはdocker con19でのfirecracker-containerdに関するセッションです

環境構築

ここからは実際に環境構築の手順をご紹介します。EC2インスタンスを起動後に諸々のセットアップを行います。

Dockerのインストール

諸々のビルドにビルド用のコンテナを利用するので、Dockerをインストールしておきます。

$ sudo apt update
$ sudo apt install -y docker.io
$ sudo systemctl start docker
$ sudo usermod -aG docker $USER

ubuntuユーザーをdockerグループに追加したので、一旦ログアウトと再ログインを行いましょう。

Goのインストール

続いてGoをインストールします。firecracker-containerdにはGoの11.0以後が必要ですが、普通にaptでインストールするとGoのバージョンが古いので、リポジトリを追加してからインストールします。

$ sudo add-apt-repository ppa:longsleep/golang-backports
$ sudo apt update
$ sudo apt install -y golang-go

インストールできたらバージョンを確認しましょう

$ go version
go version go1.13.4 linux/amd64

firecracker-containerdと関連ツールのインストール

ここからが本番です。firecracker-containerdのソースコードを取得して諸々ビルド&インストールしていきましょう。

$ git clone --recurse-submodules https://github.com/firecracker-microvm/firecracker-containerd
$ cd firecracker-containerd/
$ make all
$ sudo make install

これでfirecracker-containerdが/usr/local/binにインストールされます。

続いてFirecracker本体もインストールします。先程git cloneした際にサブモジュールとしてFirecrackerのソースもクローンされています。

$ sudo make install-firecracker

ビルド用のコンテナが起動し、FIrecrackerとJailerのバイナリがビルド&インストールされます。

続いてfirecracker-containerd用のディレクトリや設定ファイルを準備します。

$ sudo mkdir -p /etc/firecracker-containerd
$ sudo mkdir -p /var/lib/firecracker-containerd/containerd
$ sudo mkdir -p /var/lib/firecracker-containerd/snapshotter/devmapper

$ sudo tee /etc/firecracker-containerd/config.toml <<EOF
disabled_plugins = ["cri"]
root = "/var/lib/firecracker-containerd/containerd"
state = "/run/firecracker-containerd"
[grpc]
  address = "/run/firecracker-containerd/containerd.sock"
[plugins]
  [plugins.devmapper]
    pool_name = "fc-dev-thinpool"
    base_image_size = "10GB"
    root_path = "/var/lib/firecracker-containerd/snapshotter/devmapper"

[debug]
  level = "debug"
EOF

snapshotter用のプールを作成するために以下のシェルスクリプトを実行します

#!/bin/bash

# Sets up a devicemapper thin pool with loop devices in
# /var/lib/firecracker-containerd/snapshotter/devmapper

set -ex

DIR=/var/lib/firecracker-containerd/snapshotter/devmapper
POOL=fc-dev-thinpool

if [[ ! -f "${DIR}/data" ]]; then
touch "${DIR}/data"
truncate -s 100G "${DIR}/data"
fi

if [[ ! -f "${DIR}/metadata" ]]; then
touch "${DIR}/metadata"
truncate -s 2G "${DIR}/metadata"
fi

DATADEV="$(losetup --output NAME --noheadings --associated ${DIR}/data)"
if [[ -z "${DATADEV}" ]]; then
DATADEV="$(losetup --find --show ${DIR}/data)"
fi

METADEV="$(losetup --output NAME --noheadings --associated ${DIR}/metadata)"
if [[ -z "${METADEV}" ]]; then
METADEV="$(losetup --find --show ${DIR}/metadata)"
fi

SECTORSIZE=512
DATASIZE="$(blockdev --getsize64 -q ${DATADEV})"
LENGTH_SECTORS=$(bc <<< "${DATASIZE}/${SECTORSIZE}")
DATA_BLOCK_SIZE=128 # see https://www.kernel.org/doc/Documentation/device-mapper/thin-provisioning.txt
LOW_WATER_MARK=32768 # picked arbitrarily
THINP_TABLE="0 ${LENGTH_SECTORS} thin-pool ${METADEV} ${DATADEV} ${DATA_BLOCK_SIZE} ${LOW_WATER_MARK} 1 skip_block_zeroing"
echo "${THINP_TABLE}"

if ! $(dmsetup reload "${POOL}" --table "${THINP_TABLE}"); then
dmsetup create "${POOL}" --table "${THINP_TABLE}"
fi

firecracker-containerdを利用するにはMicroVM内で専用のエージェントを動作させる必要があります。以下のコマンドでエージェント導入済みのルートファイルシステムを準備します。

$ make image

準備できたら先程作成した設定ファイルで指定した場所に配置します。

$ sudo mkdir -p /var/lib/firecracker-containerd/runtime
$ sudo mv  tools/image-builder/rootfs.img  /var/lib/firecracker-containerd/runtime/default-rootfs.img

続いてMicroVM用のカーネルも取得&配置します。

$ curl -fsSL -o hello-vmlinux.bin https://s3.amazonaws.com/spec.ccfc.min/img/hello/kernel/hello-vmlinux.bin
$ sudo mv hello-vmlinux.bin /var/lib/firecracker-containerd/runtime/default-vmlinux.bin

containerd用の設定ファイルを準備します

sudo mkdir /etc/containerd/
sudo tee /etc/containerd/firecracker-runtime.json <<EOF
{
  "firecracker_binary_path": "/usr/local/bin/firecracker",
  "cpu_template": "T2",
  "log_fifo": "fc-logs.fifo",
  "log_level": "Debug",
  "metrics_fifo": "fc-metrics.fifo",
  "kernel_args": "console=ttyS0 noapic reboot=k panic=1 pci=off nomodules ro systemd.journald.forward_to_console systemd.unit=firecracker.target init=/sbin/overlay-init",
  "default_network_interfaces": [{
    "CNIConfig": {
      "NetworkName": "fcnet",
      "InterfaceName": "veth0"
    }
  }]
}
EOF

これで準備完了です!

やってみる

実際に動かしてみます。まずfirecracker-containerdのプロセスを起動します。

$ sudo firecracker-containerd --config /etc/firecracker-containerd/config.toml

起動すると以下のように出力されます。

INFO[2020-01-19T11:49:24.304547210Z] starting containerd                           revision= version=1.3.1+unknown
INFO[2020-01-19T11:49:24.347480676Z] loading plugin "io.containerd.content.v1.content"...  type=io.containerd.content.v1
INFO[2020-01-19T11:49:24.347586050Z] loading plugin "io.containerd.snapshotter.v1.devmapper"...  type=io.containerd.snapshotter.v1
INFO[2020-01-19T11:49:24.347673176Z] initializing pool device "fc-dev-thinpool"
INFO[2020-01-19T11:49:24.350407561Z] using dmsetup:
Library version:   1.02.145 (2017-11-03)
Driver version:    4.37.0
INFO[2020-01-19T11:49:24.357838236Z] loading plugin "io.containerd.snapshotter.v1.overlayfs"...  type=io.containerd.snapshotter.v1
INFO[2020-01-19T11:49:24.358035986Z] loading plugin "io.containerd.metadata.v1.bolt"...  type=io.containerd.metadata.v1
INFO[2020-01-19T11:49:24.358085041Z] metadata content store policy set             policy=shared
INFO[2020-01-19T11:49:24.358273619Z] loading plugin "io.containerd.differ.v1.walking"...  type=io.containerd.differ.v1
INFO[2020-01-19T11:49:24.358317002Z] loading plugin "io.containerd.gc.v1.scheduler"...  type=io.containerd.gc.v1
INFO[2020-01-19T11:49:24.358373313Z] loading plugin "io.containerd.service.v1.containers-service"...  type=io.containerd.service.v1
INFO[2020-01-19T11:49:24.358403712Z] loading plugin "io.containerd.service.v1.content-service"...  type=io.containerd.service.v1
INFO[2020-01-19T11:49:24.358430548Z] loading plugin "io.containerd.service.v1.diff-service"...  type=io.containerd.service.v1
INFO[2020-01-19T11:49:24.358464903Z] loading plugin "io.containerd.service.v1.images-service"...  type=io.containerd.service.v1
INFO[2020-01-19T11:49:24.358492383Z] loading plugin "io.containerd.service.v1.leases-service"...  type=io.containerd.service.v1
INFO[2020-01-19T11:49:24.358518056Z] loading plugin "io.containerd.service.v1.namespaces-service"...  type=io.containerd.service.v1
INFO[2020-01-19T11:49:24.358544418Z] loading plugin "io.containerd.service.v1.snapshots-service"...  type=io.containerd.service.v1
INFO[2020-01-19T11:49:24.358569857Z] loading plugin "io.containerd.runtime.v1.linux"...  type=io.containerd.runtime.v1
INFO[2020-01-19T11:49:24.358648695Z] loading plugin "io.containerd.runtime.v2.task"...  type=io.containerd.runtime.v2
DEBU[2020-01-19T11:49:24.358741889Z] loading tasks in namespace                    namespace=default
WARN[2020-01-19T11:49:24.358965429Z] cleaning up after shim disconnected           id=test namespace=default
INFO[2020-01-19T11:49:24.358990465Z] cleaning up dead shim
...略

別のシェルからdebianのコンテナイメージを取得してみましょう

sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \
     image pull \
     --snapshotter devmapper \
     docker.io/library/debian:latest

イメージが準備できたらコンテナの起動です!

sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \
     run \
     --snapshotter devmapper \
     --runtime aws.firecracker \
     --rm --tty --net-host \
     docker.io/library/debian:latest \
     debian-test

コンテナを起動すると、先程firecracker-containerdを起動したシェルの方に以下のようなログが流れてきます。

DEBU[2020-01-19T11:50:06.673987018Z] stat snapshot                                 key="sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0"
DEBU[2020-01-19T11:50:06.674063559Z] stat                                          key="default/6/sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0"
DEBU[2020-01-19T11:50:06.679773978Z] prepare snapshot                              key=test parent="sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0"
DEBU[2020-01-19T11:50:06.767683266Z] garbage collected                             d=1.617186ms
DEBU[2020-01-19T11:50:34.089003443Z] stat snapshot                                 key="sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0"
DEBU[2020-01-19T11:50:34.089074135Z] stat                                          key="default/6/sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0"
DEBU[2020-01-19T11:50:34.093979570Z] prepare snapshot                              key=debian-test parent="sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0"
DEBU[2020-01-19T11:50:34.094128580Z] prepare                                       key=default/8/debian-test parent="default/6/sha256:dd5242c2dc8ae9782b73f83281625f45bd6217dc79540e1019d5da0913b491b0"
DEBU[2020-01-19T11:50:34.094277581Z] creating snapshot device 'fc-dev-thinpool-snap-6' from 'fc-dev-thinpool-snap-4'
DEBU[2020-01-19T11:50:34.228530373Z] event published                               ns=default topic=/snapshot/prepare type=containerd.events.SnapshotPrepare
DEBU[2020-01-19T11:50:34.233751587Z] get snapshot mounts                           key=debian-test
DEBU[2020-01-19T11:50:34.233833659Z] mounts                                        key=default/8/debian-test
DEBU[2020-01-19T11:50:34.276741948Z] event published                               ns=default topic=/containers/create type=containerd.events.ContainerCreate
DEBU[2020-01-19T11:50:34.280182423Z] get snapshot mounts                           key=debian-test
DEBU[2020-01-19T11:50:34.280251418Z] mounts                                        key=default/8/debian-test
time="2020-01-19T11:50:34.319317129Z" level=debug msg=StartShim runtime=aws.firecracker task_id=debian-test
time="2020-01-19T11:50:34.319581654Z" level=info msg="will start a single-task VM since no VMID has been provided" runtime=aws.firecracker task_id=debian-test vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd
DEBU[2020-01-19T11:50:34.320383965Z] create VM request: VMID:"526a6692-d0ba-4b24-a5ef-a61434d2b1cd" ContainerCount:1 ExitAfterAllTasksDeleted:true
DEBU[2020-01-19T11:50:34.320418154Z] using namespace: default
DEBU[2020-01-19T11:50:34.320664216Z] starting containerd-shim-aws-firecracker      vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd
DEBU[2020-01-19T11:50:34.343828149Z] garbage collected                             d=1.784911ms
INFO[2020-01-19T11:50:34.361581942Z] starting signal loop                          namespace=default path=/var/lib/firecracker-containerd/shim-base/default/526a6692-d0ba-4b24-a5ef-a61434d2b1cd pid=44088
INFO[2020-01-19T11:50:34.361843231Z] creating new VM                               runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd
INFO[2020-01-19T11:50:34.519397998Z] Called startVMM(), setting up a VMM on firecracker.sock  runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd
INFO[2020-01-19T11:50:34.532714107Z] refreshMachineConfiguration: [GET /machine-config][200] getMachineConfigurationOK  &{CPUTemplate:T2 HtEnabled:0xc00039452b MemSizeMib:0xc000394520 VcpuCount:0xc000394518}  runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd
INFO[2020-01-19T11:50:34.533313802Z] PutGuestBootSource: [PUT /boot-source][204] putGuestBootSourceNoContent   runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd
INFO[2020-01-19T11:50:34.533355817Z] Attaching drive /var/lib/firecracker-containerd/runtime/default-rootfs.img, slot root_drive, root true.  runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd
INFO[2020-01-19T11:50:34.534035946Z] Attached drive /var/lib/firecracker-containerd/runtime/default-rootfs.img: [PUT /drives/{drive_id}][204] putGuestDriveByIdNoContent   runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd
INFO[2020-01-19T11:50:34.534077231Z] Attaching drive /var/lib/firecracker-containerd/shim-base/default/526a6692-d0ba-4b24-a5ef-a61434d2b1cd/ctrstub0, slot MN2HE43UOVRDA, root false.  runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd
INFO[2020-01-19T11:50:34.534598845Z] Attached drive /var/lib/firecracker-containerd/shim-base/default/526a6692-d0ba-4b24-a5ef-a61434d2b1cd/ctrstub0: [PUT /drives/{drive_id}][204] putGuestDriveByIdNoContent   runtime=aws.firecracker vmID=526a6692-d0ba-4b24-a5ef-a61434d2b1cd
...略

Firecrackerの諸々のAPIを実行してMicroVMを起動していることが読み取れますね。コンテナを起動したシェルを確認すると、無事debianのコンテナが起動したことが分かります。

root@microvm:/#

ここからは通常のcontainerdとfirecracker-containerdを比較してみます。まず通常のcontainerdを使ってDockerHubで公開されているnmeyerhans/stress:latestのイメージを実行してみます。

$ sudo ctr image pull docker.io/nmeyerhans/stress:latest
$ sudo ctr run --tty --env CPU_THREADS=2 --env IO_THREADS=4 docker.io/nmeyerhans/stress:latest $(uuid)

この状態でpgrep stressを実行するといくつかプロセスIDが表示されます。

$ pgrep stress
44676
44678
44679
44680
44681
44682
44683

続いてtopコマンドの出力結果です。

$ top -b n1|head -n 15
top - 13:47:54 up 18 min,  3 users,  load average: 2.39, 1.35, 1.02
Tasks: 700 total,   4 running, 352 sleeping,   0 stopped,   0 zombie
%Cpu(s):  1.6 us,  0.5 sy,  0.0 ni, 97.9 id,  0.1 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 52824844+total, 52084355+free,  1026088 used,  6378792 buff/cache
KiB Swap:        0 total,        0 free,        0 used. 52415888+avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
42372 root      20   0    7276     84      0 R 100.0  0.0   0:23.42 stress
42370 root      20   0    7276     84      0 R  94.4  0.0   0:23.41 stress
42375 root      20   0    7276     84      0 D  50.0  0.0   0:10.79 stress
42371 root      20   0    7276     84      0 R  44.4  0.0   0:10.78 stress
42373 root      20   0    7276     84      0 D  44.4  0.0   0:10.64 stress
42374 root      20   0    7276     84      0 D  44.4  0.0   0:10.76 stress
 1508 root       0 -20       0      0      0 I  11.1  0.0   0:02.37 kworker/57:1H
42382 ubuntu    20   0   43316   4516   3396 R  11.1  0.0   0:00.04 top

これらの出力結果から、通常のcontainerdにおけるコンテナはあくまでプロセスであることが分かります。続いてfirecracker-containerdを使ってコンテナを実行するパターンです。

$ sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \
     image pull \
     --snapshotter devmapper \
     docker.io/nmeyerhans/stress:latest
     
$ sudo firecracker-ctr --address /run/firecracker-containerd/containerd.sock \
     run --tty --env CPU_THREADS=2 --env IO_THREADS=4 \
     --snapshotter devmapper \
     --runtime aws.firecracker \
     docker.io/nmeyerhans/stress:latest $(uuid)     

先程と同様pgrepしてみます

$ pgrep stress

$ pgrep firecracker
42411
42449
42605

今度はstressという文字列でpgrepしても何もヒットしません。代わりにfirecrackerでpgrepすると、いくつかのプロセスがヒットします。firecrackerプロセスの内部でMicroVMが起動し、MicroVM内でコンテナが実行されているためです。同様にtopコマンドの出力結果です。

$ top -b n1|head -n 15
top - 13:49:24 up 19 min,  3 users,  load average: 1.75, 1.50, 1.10
Tasks: 699 total,   1 running, 351 sleeping,   0 stopped,   0 zombie
%Cpu(s):  1.5 us,  0.5 sy,  0.0 ni, 97.9 id,  0.1 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 52824844+total, 52071091+free,  1042564 used,  6494980 buff/cache
KiB Swap:        0 total,        0 free,        0 used. 52403798+avail Mem

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
42605 root      20   0  138032 109844 109204 S 117.6  0.0   0:58.42 firecracker
42613 ubuntu    20   0   43316   4508   3388 R  11.8  0.0   0:00.04 top
 5146 root      20   0       0      0      0 I   5.9  0.0   0:01.78 kworker/25:2
    1 root      20   0  225468   9236   6692 S   0.0  0.0   0:03.72 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.01 kthreadd
    3 root      20   0       0      0      0 I   0.0  0.0   0:01.11 kworker/0:0
    4 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 kworker/0:0H
    5 root      20   0       0      0      0 I   0.0  0.0   0:00.08 kworker/u144:0

こちらもホストOSからはfirecrackerのプロセスしか見えていないことが分かります。

まとめ

firecracker-containerdのご紹介でした。このあたりの技術を深堀りしていくことで、Lambdaの裏側に詳しくなれそうです。普段の開発では比較的高レイヤしか意識する機会が無いのですが、もうちょっと低レイヤーな技術も頑張って勉強していきたいと思います。