にっき

技術的な話題はないです

開発環境の構築に systemd-nspawn を使用する

開発用の Arch マシンを新調したついでに systemd-nspawn を用いて開発環境を構築しようと試行錯誤したので、その備忘録を残しておく。 本当は LXC/LXD を使いたかったのだが、AUR の lxd がうまくインストールできなかった…

systemd-nspawn とは

端的に言うと「systemd と連携するすごい chroot」。 いわゆるシステムコンテナと呼ばれているやつで、比較的長命なコンテナを VM チックに用いる目的に特化している感じ。 Canonical がやっている LXD と似たような立ち位置だが、systemd に組み込まれているので特別なインストール手順を踏まずにサクッと使い始められるなどの利点がある。

詳細は Arch Wiki などを参照されたし。

インストール

systemd に組み込まれているので、最近のディストリビューションであれば既に使用可能になっているはず。 ネットワーク周りは systemd-networkd の使用を前提にしているみたいなので、NetworkManager や netctl など他のものを使用している場合は適宜調整が必要かも。 コンテナを作成するためのツールは用意されていないので、必要になったら適宜インストールする。

コンテナの作成・起動

devenv という名前の Ubuntu コンテナを作成してみる。 コンテナをインストールする場所は任意だが、後ほど登場する machinectl などの管理コマンド群が /var/lib/machines 直下にコンテナのルートファイルシステムが置いてあることを前提としているので、今回はそのように進めていく。

まず、/var/lib/machines 下にコンテナのイメージを保管するためのディレクトリを作成する。

# mkdir /var/lib/machines/devenv

Btrfs のサブボリュームを使用する場合は次の通り。 サブボリュームにしておくと、後でコンテナのイメージを複製するときにスナップショットを使って良しなにしてくれるらしい。

# btrfs subvolume create /var/lib/machines/devenv

作成したディレクトリに debootstrap を用いて Ubuntu をインストールする。このとき、署名チェックが行われるため ubuntu-keyringDebian の場合は debian-archive-keyring)をあらかじめインストールしておく。

# debootstrap --arch=amd64 disco /var/lib/machines/devenv \
    http://ftp.jaist.ac.jp/pub/Linux/ubuntu/

次のように systemd-nspawn コマンドを実行することでいい感じに隔離された chroot 環境が立ち上がるので、アプリケーションのインストールなど必要なセットアップを実行する。

# systemd-nspawn -D /var/lib/machines/devenv

コンテナの管理

systemd-nspawnsystemd の一部なので、コンテナの起動・管理も systemd を介して行う。 コンテナ管理用の machinectl というコマンドが付属しており、これを用いてコンテナの起動や停止、状態表示などの操作を行うことができるようになっている。

例えば、先ほど作成した devenv コンテナを起動するためには次のようにコマンドを実行する。

# machinectl start devenv

上のようにすると、 systemd-nspawn@devenv.service というサービスが作成される。 これを見るとわかるように、コンテナは systemd のサービスになっているので journalctl でログを見たり色々できる。 次のようにすることで、ホスト OS の起動時にコンテナを立ち上げることも出来る。

# systemctl enable machines.target
# systemctl enable systemd-nspawn@devenv.service   # machinectl enable devenv でも可

コンテナの複製

残念ながら、systemd-nspawn には docker pull のようにリポジトリから手軽にイメージを引っ張ってくる機能はないので、コンテナの作成にある程度の手間が生じる(逆に言えば、overlay とか Dockerfile とか細々したことを考えず rootfs だけ用意すればコンテナを実行できるという利点でもある)。 リリースサイクルがはっきりとしているのであれば、ベースとなるコンテナのイメージをあらかじめ用意しておき、コンテナ作成時にそれを複製してしまうのが手っ取り早い。 machinectl には、ちょうどそのような用途で使える clone というサブコマンドが用意されている。

例えば、すでに存在する ubuntu-bionic というコンテナを複製して devenv というコンテナを新たに作成する場合は次のようにする。

# machinectl clone ubuntu-bionic devenv

複製元(上の場合は ubuntu-bionic)が Btrfs のサブボリュームである場合、イメージの複製はスナップショットを用いてコピーが少なくなるよう上手くやってくれるらしい(このあたりよくわかっていない)。 複製元のイメージがコンテナとして実行されると内容が変更されてしまう可能性があるので、次のように読み込み専用にしておくと良いかもしれない。

# machinectl read-only ubuntu-bionic

ローリングリリースモデルを採用している Arch LinuxGentoo みたいなディストリビューションだと上のような使い方で得られる恩恵はあまりなさそうだが、単純にコンテナの設定を単純化できるので実験用途には良いかもしれない。 なお、Docker のイメージを tar 形式でエクスポートすることでそのまま使うことも出来るらしいので、DB など使えそうなイメージを適宜取り込むとコンテナ作成を効率化できるかもしれない(それで本当に楽できるかは不明だが)。

ネットワーク設定

TODO

コンテナ内で Docker を動かす

本当に書きたかったのはこのセクションで、上に書いたのはすべておまけ。 デフォルトで起動されるコンテナは非特権モードであり、そのままでは Docker を動かすことができない。 なので、コンテナの設定を変更して特権モードに切り替える必要がある。 セキュリティリスクが高まる上 systemd/Docker が公式に対応しているわけではないので、設定は最新の注意を行いつつ自己責任で行うこと。

まず、systemd-nspawn@<container>.service を編集し、systemd-nspawn 側で cgroup namespacing が使用されないようにする。

$ sudo systemctl edit systemd-nspawn@<container>.service
[Service]
Environment=SYSTEMD_NSPAWN_USE_CGNS=0

次に、/etc/systemd/nspawn/<container>.nspawn を作成し、コンテナが特権ユーザで実行されるようにする。

$ sudo -e /etc/systemd/nspawn/devenv.nspawn
[Exec]
Capability=all
SystemCallFilter=add_key keyctl
PrivateUsers=no

[Files]
Bind=/sys/fs/cgroup

[Network]
VirtualEthernet=yes

設定が完了したらコンテナを再起動し、Docker をインストール・実行して問題がなければよし。

おそらく重要なのは [Exec] セクションにある PrivateUsers=no で、これがないとコンテナ側の root とホスト側の root が対応せず btrfs のスナップショット作成などの操作ができずにコケるっぽい。 SystemCallFilter の部分を適切に設定すれば良いのかもしれないがその辺よくわかっていない…

また、/sys/fs/cgroup を直接マウントしていることからもわかるように、コンテナ内で実行される Docker は自身がコンテナ内で実行されていることを考慮していないと思われる。なので、ホスト側で Docker が起動していたり同じ設定で複数個コンテナを立ち上げたりすると恐らく破綻する。

Rootless Docker が使えるようになればそちらを使えばよいかもしれないが、まだ実験段階であるせいか上手く動作しなかったので今後に期待したい。

See Also

systemd-nspawn - ArchWiki