超軽量仮想環境 cage


始めに

chroot + overlayfs + btrfsスナップショット でホスト環境の複製をjailするの記事で超軽量仮想環境の基本原理を示しました。要するにchroot jailですが。

それを、ArchLinux インストール覚書より構築を始めたLinuxマシンに導入する前提で、ディレクトリ構造や設定ファイル内容を決めます。

cage

名前は cage とします。

jail は言葉が悪過ぎると思うんですよね。なんで向こうの技術者は好き好んで刑務所なんて言葉を使うのでしょう。そういえばUNIXは伝統的に悪い言葉が多いですね。生きるはまだしも、死ぬとか殺すとか平気で使うし。言い得て妙だし便利なんですがねぇ。しかし言霊の幸わう国の住人としては、もうちょっと雅な言葉を使いたい。という訳で、籠の中の鳥なんてのはいかがかと考える次第。 birds in a cage.

ディレクトリ構造

まず始めにファイルやディレクトリ構造を決めます。一応、将来的な拡張の方向を睨んで、それなりの準備を含みつつ無闇な複雑さを排除しつつ、という方針で検討します。

前提:

/ ルート
 │
 ├─ etc
 │   ├ opt
 │   │  └ cage
 │   │     └ cage.conf  ← 仮想環境全体の設定ファイル
 │   :
 │
 ├─ opt
 │   └ cage
 │      ├ bin  ← スクリプトを作成した場合は、基本的にはここに集める
 │      └ doc  ← 注意書きやマニュアルなどはここに置く
 │
 ├─ usr
 │   ├ local
 │   │  ├ bin  ← /opt/cage/bin内容物へのシンボリックリンクを置く
 │   :  :
 │
 ├ var
 │   ├ opt
 │   │  └ cage  ← 仮想環境はここに置く; 私の環境的にはiSCSIストレージをmountする
 │   │     │
 │   │     ├ <uuid>.r     ← 拡張子.r は書き込まない(overlayfsのlowerdir用)
 │   │     │  ├ cage.conf ← 依存関係などの設定情報
 │   │     │  └ root      ← 実体ディレクトリ(cage.confで指定される)
 │   │     │     ├ …
 │   │     │     :
 │   │     │
 │   │     ├ <uuid>.w     ← 拡張子.w は書き込み可能(overlayfsのupperdir用)
 │   │     │  ├ cage.conf ← 依存関係などの設定情報
 │   │     │  ├ start     ← 開始用スクリプト(cage.confで指定される)
 │   │     │  ├ stop      ← 終了用スクリプト(cage.confで指定される)
 │   │     │  ├ mnt       ← overlayfsのマウントポイント(cage.confで指定される)
 │   │     │  ├ work      ← overlayfsのworkdir(cage.confで指定される)
 │   │     │  └ root      ← 実体ディレクトリ(cage.confで指定される)
 │   │     │     ├ …
 │   │     │     :
 │   │     :
 │   │     │
 │   │     └ 00000000-0000-0000-0000-000000000000.r ← nil UUID はホスト環境を表す
 │   │        ├ cage.conf
 │   │        └ root      ← スナップショットへのシンボリックリンク
 │   :
 │
 └ .snapshots : ホスト環境のスナップショットの格納場所
     │
     ├ <年月日時分秒>.ss  : スナップショット
     ├ <年月日時分秒>.txt : スナップショットの注釈を格納するテキストファイル
     :

設定ファイル

設定ファイルのフォーマットも決めておきます。

全体

/etc/opt/cage/cage.conf
tool=/opt/cage
data=/var/opt/cage

個別

/var/opt/cage/<uuid>.w/cage.conf
uuid=<uuid>
name=<何か適当な名前、改行は含まない、一意とは限らない>
memo=<内容の説明、エスケープシーケンス¥nで改行を表すような文字列>
base=<uuid>  # overlayfsのlowerdirになるcageのuuidをコロン:区切りで書く
root=root
work=work
mountpoint=mnt
start=start
stop=stop

rootとworkとstartとstopは、ここで設定したファイル名(ディレクトリ名)であれば何でも良いです。

/var/opt/cage/<uuid>.r/cage.conf
uuid=<uuid>
name=<何か適当な名前、改行は含まない、一意とは限らない>
memo=<内容の説明、エスケープシーケンス¥nで改行を表すような文字列>
base=<uuid>  # overlayfsのlowerdirになるcageがもしあれば書く
root=root

ホスト(のスナップショット)環境を表す 00000000-0000-0000-0000-000000000000.r の場合も cage.conf の内容は同じにしておきます。こういうものは例外を作らない方が良い。

築き上げる

言わずもがなですが、シェルはbashです。

全体設定ファイル

# mkdir -pv /etc/opt/cage
mkdir: created directory '/etc/opt'
mdkir: created directory '/etc/opt/cage'
# cat - >/etc/opt/cage/conf <<___
> tool=/opt/cage
> data=/var/opt/cage
> ___

ホスト環境のスナップショット

btrfsでのスナップショットの作り方はなおなお・ArchLinux インストール覚書を参照。読み出し専用オプションを忘れないようにします。

# fn=$(date +%Y%m%d%H%M%S)
# btrfs subvolume snapshot -r / /.snapshots/$fn.ss
Create a readonly snapshot of '/' in '/.snapshots/20170616223103.ss'
# echo the snapshot of the host environment >/.snapshots/$fn.txt

ホスト環境のcage

一応、この時点で最も新しいスナップショットをcageにします。

# tp=r
# id=00000000-0000-0000-0000-000000000000
# dt=$(sed -ne"s/[[:space:]]*#.*$//; s/^data=¥(.*¥)$/¥1/p" /etc/opt/cage/cage.conf)
# mkdir -pv $dt/$id.$tp
mkdir: created directory '/var/opt/cage/00000000-0000-0000-0000-000000000000.r'
# cd $dt/$id.$tp
# ss=$(ls -dt /.snapshots/*.ss | head -n1)
# ln -sv $ss root
'root' -> '/.snapshots/20170616223103.ss'
# cat - >cage.conf <<___
> uuid=$id
> name=host_$(basename $ss .ss)
> memo=a snapshot of the host, $(date --rfc-3339=seconds -r $ss)
> root=root
> ___

新しいcage

# bs=00000000-0000-0000-0000-000000000000
# tp=w
# id=$(uuidgen)
# dt=$(sed -ne"s/[[:space:]]*#.*$//; s/^data=¥(.*¥)$/¥1/p" /etc/opt/cage/cage.conf)
# mkdir -pv $dt/$id.$tp
mkdir: created directory '/var/opt/cage/011868f2-c75f-49a0-b7ba-54a6f282419f.w'
# cd $dt/$id.$tp
# cat - >cage.conf <<___
> uuid=$id
> name=the_cage
> memo=the cage, $(date --rfc-3339=seconds)
> base=$bs
> root=root
> work=work
> mountpoint=mnt
> start=
> stop=
> ___
# mkdir -pv $(sed -ne"s/[[:space:]]*#.*$//; s/^mountpoint=¥(.*¥)$/¥1/p" cage.conf)
mkdir: created directory 'mnt'
# mkdir -pv $(sed -ne"s/[[:space:]]*#.*$//; s/^work=¥(.*¥)$/¥1/p" cage.conf)
mkdir: created directory 'work'
# rt=$(sed -ne"s/[[:space:]]*#.*$//; s/^root=¥(.*¥)$/¥1/p" cage.conf)
# mkdir -pv $rt
mkdir: created directory 'root'
# cd $rt
# mkdir -v proc sys dev tmp
mkdir: created directory 'proc'
mkdir: created directory 'sys'
mkdir: created directory 'dev'
mkdir: created directory 'tmp'

startやstopといったスクリプトは、もう少し色々と整備されてきたら用意します。ただ、色々と混乱の元なので、設定ファイルにはその旨を表記する(エントリだけは用意して値を空欄にする)ようにした方が良いでしょう。

動かす

# tp=w
# id=011868f2-c75f-49a0-b7ba-54a6f282419f
# dt=$(sed -ne"s/[[:space:]]*#.*$//; s/^data=¥(.*¥)$/¥1/p" /etc/opt/cage/cage.conf)
# cd $dt/$id.$tp
# rt=$(sed -ne"s/[[:space:]]*#.*$//; s/^root=¥(.*¥)$/¥1/p" cage.conf)
# mp=$(sed -ne"s/[[:space:]]*#.*$//; s/^mountpoint=¥(.*¥)$/¥1/p" cage.conf)
# wk=$(sed -ne"s/[[:space:]]*#.*$//; s/^work=¥(.*¥)$/¥1/p" cage.conf)
# bs_tp=r
# bs_id=$(sed -ne"s/[[:space:]]*#.*$//; s/^base=¥(.*¥)$/¥1/p" cage.conf)
# bs_rt=$(sed -ne"s/[[:space:]]*#.*$//; s/^root=¥(.*¥)$/¥1/p" $dt/$bs_id.$bs_tp/cage.conf)

この辺り、本当はbaseが複数指定された場合への対応が必要なんですけども。今回はとにかく基本手順を確立する事を優先したので手を抜きました。スクリプトを組む時にはしっかり対応するので、今は一つ、これで勘弁して下さい

# mount -t overlay overlay -o lowerdir=$dt/$bs_id.$bs_tp/$bs_rt,upperdir=$rt,workdir=$wk $mp
# cd $mp
# mount -t proc proc proc
# mount -t sysfs sys sys
# mount -t devtmpfs dev dev
# mount -t tmpfs tmpfs dev/shm
# mount -t tmpfs tmpfs tmp
# chroot .

ここから何か意味のある作業の例という事で、nginxをインストールして起動してみます。言わずと知れたHTTPサーバですな。
参考文献: nginx - ArchWiki

ちなみに選択の基準は下記の条件です。

  • デーモンであり、起動後もずっと動き続ける
  • 大抵の人が知っている
  • 初期設定が易しい、または要らない(今回の目的はcageの実証であって、サーバを実用に供する事ではない)
  • 動作検証を自動化できる

特に最後の条件は重要です。自動化は正義です。

# pacman -S nginx openssl
  《省略》
# eval $(sed -ne"s/^ExecStart=¥(.*¥)$/¥1/p" /usr/lib/systemd/system/nginx.service)

本当は eval はあんまり使いたくないんですけど。文字列処理の結果によっては予想外の解釈で実行される可能性があり、トラブルの元になり易いのです。systemdのserviceなんかにbash変数等々を駆使した怪しげな記述はあり得ないので、今回は全然問題無いとは思いますが。もしかしたら bash -c で実行した方が無難かも知れません。

それはともかく。

# ps -x | grep nginx
  354 ?        Ss     0:00 nginx: master process /usr/bin/nginx -g pid /run/nginx.pid; error_log stderr;
  357 tty1     S+     0:00 grep nginx

いけたっぽいですねぇ。このまま cage から出てみます。

# exit
exit
# ps -x | grep nginx
  354 ?        Ss     0:00 nginx: master process /usr/bin/nginx -g pid /run/nginx.pid; error_log stderr;
  359 tty1     S+     0:00 grep nginx

動き続けてくれているようです。

確かめる

HTTPサーバとしての動作を確認します。GETリクエストを投げて、レスポンスが正しいかどうか確認します。今回はちょっと動く事を確かめれば十分なので、下記の内容でお手軽お気軽にチェックします。

# exec 3<>/dev/tcp/localhost/80
# echo GET / >&3
# diff -s - $dt/$id.$tp/$mp/usr/share/nginx/html/index.html --label GET --label html-file <&3
Files GET and html-file are indentical

止める

fuser コマンドを使います。

# cd /
# fuser -km $dt/$id.$tp/$mp
/var/opt/cage/011868f2-c75f-49a0-b7ba-54a6f282419f.w/mnt:   354rce   355rce

始めた時の逆順で止めていきます。

# cd $dt/$id.$tp/$mp
# umount tmp dev/shm
# umount -l dev sys proc
# cd ..
# umount $mp

取り壊す

ディレクトリを丸ごと削除するだけです。

# cd $dt
# rm -r $id.$tp

終わりに

勿論、完成したとは考えていません。改善の余地は山程あります。特にスクリプトにまとめて自動化するのは必須でしょう。

が、とにもかくにも汎用 chroot jail の手順をまとめられた点は大きいと自負しております。超軽量仮想環境の第一歩としてはまずまずではないでしょうか。ゆくゆくはアプリケーション配布を簡素化してDockerと競合したいものです。