もう10年以上看守していたオレが通りますよ。

上記記事の認識は間違っているとは言えないのだけど、正しいとも言い切れないと感じるので。

Jail != 仮想化

Jailに関して、一番「正しいとは言えない」のは、これ。

FreeBSD jail - Wikipedia
FreeBSD jailはOSレベル仮想化機構実装の一つである
勉強会聴講メモ 【第28回 #FreeBSD 勉強会 数千台のFreeBSD Jailホストを管理する技術、実務実践からのテクニック】 #FreeBSDStudy | しげはるblog
FreeBSD上の仮想環境

そう。「Jail = 仮想化」という認識。

「カーネル共通なんでしょ?」「それって結局言葉のあやでしょ?」ですまない、知っていないと運用上はまる重大な違いがそこにはあるので、備忘録代わりにここに書いておくことにする。

まず仮想化とは何か。Wikipediaにはこうある。

仮想化 - Wikipedia
仮想化(英語: virtualization)とは、コンピュータのリソースを抽象化することである。リソースの物理的特性を、そのリソースと相互作用するシステム/アプリケーション/エンドユーザーから隠蔽する技法。

「リソースの物理的特性を、そのリソースと相互作用するシステム/アプリケーション/エンドユーザーから隠蔽する技法」。Jailにおいて、隠蔽はなされているのか?

真のOS仮想化ではゲスト(guest OS)に相当する囚人prisoner側はなされているが、ホスト(host OS)に相当する看守jailerからはなされていない、というのがその答えになる。

具体的には、このような形になって表れる。

囚人のファイルは、看守から丸見え
たとえば/jail/abashiriに(jailによって)chrootされた囚人のパスが/foo/barだとしたら、看守からは/jail/abashiri/foo/barとして必ず見える
真の仮想化では、このようなことにはならない。ホスト側でサポートしていないファイルシステムでもゲスト側でサポートしてさえしていればよいし、ゲストのファイルシステムがあるのは仮想ディスクという名のファイルか nfs や iSCSI などのリモートディスクであり、ホスト側で強引にmountしないかぎり個々のファイルは見えない。
囚人のプロセスも、看守から丸見え
看守側でpsすれば、囚人側のプロセスもJフラグ付きで必ず見える。
これまた真の仮想化ではありえない。VirtualBoxとかbhyveとか、一つのゲストOSにつき一つのプロセスが見えるだけだ。
囚人のネットワークインターフェイスも、看守から使いたい放題(ただしVIMAGEでない場合)
以下は、実際にうちで使っているJailの一つからDNSを引いている例である。
root@ports:/ # ifconfig lo1
lo1: flags=8049 metric 0 mtu 16384
	options=600003
	inet 10.0.0.2 netmask 0xffffffff 
stf0: flags=1 metric 0 mtu 1280
root@ports:/ # cat /etc/resolv.conf
nameserver 10.0.0.2
root@ports:/ # host www.example.com
www.example.com has address 93.184.216.119
www.example.com has IPv6 address 2606:2800:220:6d:26bf:1447:1097:aa7
root@ports:/ # ps awwux
USER   PID %CPU %MEM   VSZ  RSS TT  STAT STARTED    TIME COMMAND
root 90537  0.0  0.0 14424 1436  -  SsJ  Fri02PM 0:03.44 /usr/sbin/syslogd -s
root 90725  0.0  0.0 16520  560  -  SsJ  Fri02PM 0:00.58 /usr/sbin/cron -s
root 72192  0.0  0.0 23488 3924  0  SJ    2:23PM 0:00.07 /bin/tcsh
root 72202  0.0  0.0 16588 2104  0  R+J   2:24PM 0:00.00 ps awwux
あれ?ローカルのキャッシュサーバー使っているのに、サーバープロセスが見当たらないんだが…
答えはもちろん、看守が動かしている、である。

Jailではまっている事例で最も多いのは、jexecなどで入獄してやるべき作業を看守のままやってしまったいうものなのであるが、なぜそうなのかといえば、それが出来てしまうからなのである。

私がJailを「仮想化」と呼びたくない理由が、それだ。Jailは仮想化の代わりとしても使えるし、そう使った場合仮想化コストもほぼゼロなので大変ありがたいのだが、実は何も仮想化されておらず、不可視化されているだけということを忘れると無実の囚人を殺すkillしてしまいがちだ。

「仮想化されているフリ」なので仮装化(masquerading)と呼びたいところであるが、これではダジャレにもほどがあるし、改変なしにゲストOSを運用する真の仮想化に対しホストOSにあわせて改変したゲストOSを運用することを準仮想化(paravitualization)と呼んでいるので、半仮想化(semivirtualizaion)と呼ぶのはどうか。

便利ツールいらずの当代Jail事情

通常の仮想化と半仮想化の違いが最も顕著に現れるのは、環境構築であろう。

通常の仮想化においては、まず仮想化アプリケーション上で仮想マシンを構築し、その上で実マシン(bare metal)の場合と同じようにゲストOSをインストールした後、さらにVirtualBoxであればGuest Addition、VMWareであればVMWare Toolsなどをインストールして、性能と利便性を向上させるための一種の準仮想化してから運用を開始するか、出来合いの仮想マシンファイルをもってくるかするのであるが、Jailの場合半仮想化だけあって、こうした下準備は全て看守側で行われる。

教科書どおりだと…

15.3. Creating and Controlling Jails
# setenv D /here/is/the/jail
# mkdir -p $D
# cd /usr/src
# make buildworld
# make installworld DESTDIR=$D
# make distribution DESTDIR=$D
# mount -t devfs devfs $D/dev

ということになるが、これはいかにもめんどくさい。そういうこともあってezjailやqjailといった便利ツールが発達したのであるが、FreeBSD 9以降であれば、すっぴんでもこうしたツールが不要なほど楽が出来る。さらにzfsと組み合わせれば、新規Jailの構築も一瞬である。

用地確保
ここではtankというzpoolに、/jailというprefixでjailを並べておくことにする。
# zfs create -o mountpoint=/jail tank/jail
# zfs create tank/jail/jail
種jailの構築
ここではbaseという名前にしておく。
# bsdinstall jail /jail/base
インストールするパッケージは、baselib32だけでよいだろう。
種jailへのfreebsd-updateの適用(optional)
「囚人としてできることを看守でやらない」というのはJail作法の第1条なのであるが、freebsd-updateの適用は残念ながら現時点においても囚人としてはできない。
# freebsd-update -b /jail/base -d /jail/base/var/db/freebsd-update fetch
# freebsd-update -b /jail/base -d /jail/base/var/db/freebsd-update install
としておこう。
追記: allow.chflagsを設定したJailであれば、jail内でもfreebsd-updateを発見した。
仮想ネットワークの構築(オプショナル)
Jailごとに固有のIPアドレスを用意しなければならないと思い込んでいる方は多いが、実は誤解である。Jailというのは、あくまで看守にすでにある資源の一部を囚人にとってそれが全部であるがように見せる技術であり、よって看守のIPアドレスをそのまま指定すれば普通に動くし、IPアドレスが全くないJailももちろん作れる。
余談であるが、現在のllevalでは、1 requestごとにJailを「構築」しているが、IPアドレスは看守と共通である(IPv4とIPv6が一つづつ)。
しかしそれだと仮想化している感じもまた得られないので、ここでは最も手っ取り早い方法でJail用のIPアドレスを用意することにする。
/etc/rc.conf (抜粋)
cloned_interfaces="lo1"
ipv4_addrs_lo1="10.0.0.1-15/32"
pf_enable="YES"
pf_rules="/etc/pf.conf"
/etc/pf.conf
ext_if = re0
nat_if = lo1
nat on $ext_if from $nat_if to any -> ($ext_if)
こうしておいてから
# service netif start lo1
# service pf start
とするか看守を再起動すれば、10.0.0.1-10.0.0.15が準備完了。
/etc/jail.conf の作成
9以降のFreeBSDで一番変わったのがこれである。これさえあれば、/etc/rc.confに書くのは
jail_enable="YES"
だけでよい。
# common variables
exec.start = "/bin/sh /etc/rc";
exec.stop  = "/bin/sh /etc/rc.shutdown";
exec.clean;
mount.devfs;
interface = lo1;
path = "/jail/$name";
host.hostname = $name;
# each jail
base    { ip4.addr = 10.0.0.1; }
ports   { ip4.addr = 10.0.0.2; }
ports32 { ip4.addr = 10.0.0.3; }
# .... and more
見ての通り、共通の設定項目とJailごとの設定項目を書き分けられるし、変数も使える。
動作確認
というわけでまずは種jailがきちんと動くかどうかを確認する。
# service jail start base
でbaseのみstartしておいてから、
# jexec base /bin/sh
として動作確認してみる。問題がなければexitで看守に戻る。
問題としてよくあるのは、/etc/resolv.confの書き(かえ)忘れ。特に local でキャッシュサーバーを動かしている場合、nameserver 127.0.0.1としてあると思うが、明示的に囚人に見えるようにしていない限りこれは見えない。上記のjail.confの場合、
# echo 'nameserver 10.0.0.1' > /etc/resolv.conf
とする必要がある。
種jailをclone
あとは念のため
# service jail stop base
とした上で
# zfs snapshot tank/jail/base@10.0p1
としておき、
# zfs clone tank/jail/base@10.0p1 tank/jail/ports
とすれば一瞬でいくらでも監獄が作れる。

まとめ

JailとZFSは、私にとって今時のFreeBSDの魅力の双璧である。しかも、この両者の相性は抜群だ。今やLinuxにもLXCもZFS on Linux もあるけど、LXCはJailほどには軽くないし(しかしその分FreeBSDでは標準ではないネットワーク仮想化がなされている(FreeBSDのVIMAGE相当)などの利点もあるが)、ZFS on Linuxはカーネルのバージョンアップと同期してない(おかげでUbuntuでも最近まで使えなくなっていた)。枯れているが進化はまだ続いている。これからもがんがん使っていきたい。

Happy Jailing!

Dan the (Jail|Self-Prison)er