#これは何か
Intelが2年ほど前に仮想環境とコンテナを組み合わせたClear Containerを発表しましたが、実装を調べるのを面倒くさがっていました。
今回LPCで「Qemuと9pfsを組み合わせた」という話を聞いたので、早速MINCS(Mini Container Shellscripts)に実装して、実際にどう動くのかを試しました。ちょうどLinux Advent Calenderがあったので報告します。
#MINCS Qemu mode
MINCS Qemu modeは指定したコンテナrootfsの上で、Qemuを使ったコンテナを実行するモードです。単純に面白いので実装した感じで、隔離性は高いものの、あまり実用性はないです(笑)。Clear Containerとは違い、MINCSのコンテナの動作モードの一つとして動きます。(同じイメージを使って普通のコンテナとQemuコンテナの両方を試せます)
##インストール方法
MINCS自体はgithubからチェックアウトするだけで利用できます。
$ git clone https://github.com/mhiramat/mincs.git
MINCSはシェルスクリプトなので特にビルドの必要はないのですが、QemuモードはQemu(KVM)のインストールと、仮想マシン上で動くLinuxをビルドしなければ動きません。予めLinuxカーネルとBusyboxがビルドできる環境であれば、MINCS内のermine-breeder
を実行するとホスト環境向けの(多分x86_64向けの)ermineイメージがビルドできます。
$ cd mincs
$ ./ermine-breeder build
オプションとして--config
でコンフィグファイルを渡せるのですが、samples/ermine/smallconfig
を渡すと割といい感じに小さい目のイメージがビルドされます。
$ ./ermine-breeder build --config samples/ermine/smallconfig
お使いの環境にどうしてもカーネルビルド環境を作るのは嫌だ!という場合は、Dockerで環境を作るか、debootstrapをインストールした上でermine-breeder
にselfbuild
コマンドを渡せば、自分で環境を作ってビルドします。
$ ./ermine-breeder selfbuild
##使い方
特殊な使い方は特に必要なく、--qemu
オプションをつけるだけ動きます。例えば現在のルートファイルシステム上でQemuコンテナを動かす場合は、以下のようにします。
$ sudo ./minc --qemu
debootstrapやdockerから借りてきたコンテナのルートファイルシステムがあるなら、それを指定することも出来ます。
$ sudo ./minc --qemu -r ./debian-root/
さらに、非特権ユーザコンテナをQemuモードで動かすことも可能です。
$ ./minc --qemu --nopriv ./nonroot-root/
#Qemu Containerの基本的な仕組み
皆さんご存知の通り、Qemuではディスクイメージをブロックデバイスとして見せるだけではなく、ホストの任意のディレクトリを9pfs経由でゲストに見せる機能があります。一方、Linuxカーネル側には9pfsのクライアントとなってマウントする機能があります。IntelのClear Containerは基本的にこの仕組みを利用して、ホスト側で用意したコンテナイメージ(一般にはRootfsのディレクトリ)をゲスト側でマウントし、そこにchroot(あるいはpivot_rootかswitch_root)することで実装していると考えられます。
この機能をコンテナに実装するにあたっての前提条件を考えてみましょう。
まず、仮想マシンを使うので、仮想マシン上で動かすコンテナOS、つまりLinuxカーネル及びコンテナエンジンが必要になります。仮想マシン上でコンテナエンジンを動かさず、直接コンテナイメージをQemu上で動かす方法もありますが、一般のコンテナとアプリケーションから見た時の環境が違ってしまうので、余りお勧めはできません。IntelのClear ContainerではこのOS部分をClear Linuxと呼んでいます。
コンテナOSに最低限必要なのは、KVM環境で起動すること、9pfs(virtio 9pfs)のマウントができること、そして名前空間機能をサポートしていることです。その他、高速起動やサイズの最小化、セキュリティ隔離機能などもあると嬉しい特徴でしょう。
また、仮想マシンを動かす環境を作るためのコンテナが必要です。これは必須ではないですが、ホスト側でコンテナのイメージをマウントする様子を、他のユーザから見えないようにする効果があります。
MINCSでのコンテナOSの実装
MINCSでは仮想マシン上で動かすコンテナ用OSを実装するにあたり、busybox+libcap+Linuxを使いました(作り始めはboot2mincをベースにしていましたが、ライセンスのこともあったので完全に作りなおしました)。ちなみにBusyboxの1.24からはunshareコマンドも実装されており、基本的なコンテナの機能はBusyboxだけで実現できるようになっています。libcapが必要な理由は既に書きました。
9pfsのマウント機能や名前空間などはカーネルのコンフィグで追加しています。MINCSではこれをermine(オコジョ)1と名づけています。また、qemu上で起動させることが目的なので、ISOイメージではなく、普通にbzImageとinitramfsを使うようにしています。サイズに関して言えば、殆どデフォルトの機能を有効にしただけですが、8MB程度で実装ができています。試しに簡単に不要なオプションを削ったところ、カーネル+busyboxで5MBを切ることが出来ました。多分もう少し削ることは可能でしょう。
###9pfsによるホストディレクトリのマウント
Qemuのwikiに書いている通り設定すればホストのディレクトリをゲストからマウントできます。Qemuの起動パラメータに以下のフォーマットでホストディレクトリとmount_tagを指定します。
-virtfs fsdriver,id=[id],path=[path to share],security_model=[mapped|passthrough|none][,writeout=writeout][,readonly]
[,socket=socket|sock_fd=sock_fd],mount_tag=[mount tag]
ゲストからは、mount_tagで指定した文字列をデバイスとしてマウントすればOKです。オプションでtrans=virtioを渡すのを忘れないようにしましょう。9pfsにはネットワークを使ったマウント方法もありますが、ホストからしか渡さないのであればvirtioで直接渡すほうが効率的でしかも簡潔です。
mount -t 9p -o trans=virtio [mount tag] [mount point] -oversion=9p2000.L
###指定されたコンテナの自動実行
Ermineでは、コンテナのrootfsを指定された場合はコンテナを自動実行することになります。コンテナのrootfsを指定されたかどうかは、kernelのブートパラメータによって挙動を変えることが考えられますが、将来的にカーネルのパラメタと衝突しないかどうかが心配になります。
そこでErmineでは、単に事前に決めたmount_tag(minc
)を指定してコンテナのファイルシステムをマウントすることが可能かどうかだけで判断します。マウントが不可能であれば、そのままメンテナンス用のbusybox shell(/bin/sh)に移動します。
また、mincの起動パラメタのうち、ホスト名などはqemu内部に伝える必要があります。そこでqemu内部で起動するmincコマンドは外部から与えられるよう、9pfsでマウントしたディレクトリにあるrun.sh
を自動実行することとします。
cat > mincshell.sh << EOF
#!/bin/sh
run_minc() {
if mount -t 9p -o trans=virtio minc /mnt -oversion=9p2000.L,posixacl,cache=loose
; then
if [ -f /mnt/run.sh ]; then
/bin/cttyhack sh /mnt/run.sh
exec poweroff
fi
fi
}
このrun.sh
は、以下のようにminc-exec内部で動的に作成しています。
MINC_GUEST_OPT="-r /mnt/root --name `hostname`"
if [ -z "$MINC_DIRECT" ]; then
# if the host mounts overlayfs on rootfs, guest skips it.
MINC_GUEST_OPT="$MINC_GUEST_OPT -D"
fi
echo "#!/bin/sh" > $MINC_TMPDIR/run.sh
echo "minc $MINC_GUEST_OPT \"$@\"" >> $MINC_TMPDIR/run.sh
コメントにあるように、実はoverlayfsの処理はホスト側で行い、Qemu内部では9pfsで見えたディレクトリに直接アクセスするようにしています。こうすることで一々ゲスト側に複数のoverlay用ディレクトリを見せる必要性をなくします。
また、ゲストに見せるディレクトリも、コンテナを実行したいルートディレクトリそのものではなく、ルートディレクトリを含むもう一つ上の一時ディレクトリ($MINC_TMPDIR
)を見せています。これによって、この一時ディレクトリ上のrun.sh
がコンテナ内部から見えないようにしています。
###initramfsとpivot_root
最近のカーネルでは、initrdからinitramfsに標準のon-memory bootファイルシステムが変わっています。initramfsはLinuxカーネルでは「rootfs」という名前のファイルシステムとして認識されます。ところがこれはルートファイルシステム専用のファイルシステムなので、pivot_rootするなどして別の箇所にマウントして使うことが出来ません。
initramfsではswitch_rootを使うことで新しいルートファイルシステムにマウントポイントを切り替え可能なのですが、ermineではswitch_rootは使いません。理由は簡単で、コンテナが終了した時に仮想マシンを落としたいので、initramfs上のpoweroffコマンドを呼び出しているからです。switch_rootを使って切り替えた場合、元のファイルが消されてしまうのでpoweroffも使えないという訳です。
ただ、いずれは裏で走っているinitからpowerdownすればいいので、最終的にはswitch_rootに乗り換えるのが好ましいでしょう。(switch_rootは元のディレクトリのファイルを全部消してしまうので、まあ安全といえば安全です)
###Shutdown message
MINCSのqemuモードでは仮想環境のttyS0を現在のコンソールにリダイレクトすることで、通常のコンテナモードとの同様にコンソールから使えるようにしています。
しかしinittabをきちんと設定しないと、カーネルの起動ログや終了ログが表示されてしまい、ユーザから見た時に、qemuの部分が透過的に扱えなくなってしまいます。
基本的な方針としては、カーネルのメッセージはttyS0に出さない、しかしコンテナはttyS0に出したい、ということになります。そこでinittabで工夫をし、カーネルのコンソールをttyS0に出さないようにした上で、/sbin/initはデフォルトのコンソールに出力し(つまり表示されない)、ttyS0上でシェルを動かすようにしました。
::sysinit:/etc/rc.local
::restart:/sbin/init
::ctrlaltdel:/sbin/reboot
ttyS0::respawn:/bin/cttyhack /mincshell.sh
本当は/sbin/initの代わりに/bin/shを直接叩けば良いのかもしれないですが、いずれにしてもコンテナ終了時に自動的にpoweroffコマンドを叩いてqemuを終了させたいので現在の実装になっています。
##Qemu Container modeの利点
MINCSのQemuモード機能を使うと、非特権ユーザであっても仮想環境内では特権ユーザになるため、overlayfsなどが使いたい放題になるという特典があります。従来の非特権コンテナは使いやすくてもoverlayfsを使っていないため、コンテナのrootfsへの変更がそのまま残ってしまうという問題がありました。仮想環境を使うことでこの点を解消することが可能です。
また、システム全体をVMにすることで、別アーキテクチャのエミュレーションにも用いることが出来ます2。
Qemu Container modeの課題
ただし、欠点もあります。例えば起動・終了にかかる時間が長いこと(これは仮想マシン自体の初期化の遅さと、仮想マシン内のLinux自体の起動の遅さが原因ですので、最適化を行えばマシになります)、コンテナの終了時の戻り値が得られないこと、メモリ使用量が多いこと(これも最適化でなんとかなる&KSMで軽減が可能だが、隣からメモリ内容を予測される脆弱性の指摘もあり)などがあります。
さらに、仮想マシン自体にいくらCPUとメモリを割り当てるかを明確にしないと使いにくいのも実用上は問題になるでしょう。コンテナを使う多くの場合、目的はパッケージ化されたアプリケーション実行環境を簡単にデプロイして実行したい、という簡潔な理由だと思われます。その場合きっちりしたリソース割当設計をすると、柔軟性や簡潔性が失われて、面倒なことになります。
Ermineでは現状、Qemuデフォルトの設定なのでVMリソースは非常に少ないですが、これも最適値がよくわからないので変更がしにくいのが難点です。