Linuxにおけるsystemd-nspawn入門:Docker不要で超軽量な隔離コンテナを作成・管理する

Linux tutorial - IT technology blog
Linux tutorial - IT technology blog

本番サーバーにおける現実的な課題

約6ヶ月前、社内のサーバー環境で厄介な問題に直面しました。同一のVPS上で、PHP 7.4を使用するレガシーアプリケーションとNode.js 20のマイクロサービスを並行稼働させる必要が生じたのです。これら2つのサービスが要求するシステムライブラリ(glibcやOpenSSLなど)のバージョンは、完全に競合していました。

このような状況では、まずDockerを導入するのが一般的な選択肢でしょう。しかし、スペックの限られたVPS(2 vCPU、4GB RAM)環境では、Dockerのオーバーヘッドが無視できませんでした。dockerd、containerd、docker-proxyの3つのプロセスだけで、待機状態にもかかわらず150MB〜300MBものRAMを消費してしまいます。さらに、Dockerはiptablesを大きく書き換えるため、既存のUFWファイアウォールと競合を引き起こすリスクもありました。

かといって、ホストOS上で直接実行するのはどうでしょうか。依存関係の衝突や、特定プロセスの不具合によってCPUリソースを100%食い潰されるリスクは、本番環境において到底容認できません。

なぜchrootとDockerのどちらも最適解ではなかったのか?

中間的なソリューションが必要となった理由を理解するために、両極端とも言える2つの手法を比較してみましょう。

  • chrootの限界: chrootコマンドはルートディレクトリを変更するだけで、namespaceによる隔離は行われません。内部のプロセスからは/proc経由でホスト側の全プロセスが見えてしまい、ネットワークスタックやIPCも共有されます。さらに危険なのは、内部のroot権限を持つプロセスが容易にホストへ脱出(chroot escape)できてしまう点です。
  • Dockerのオーバーヘッド: Linuxカーネルの観点で見れば、Dockerも本質的にはcgroups、namespaces、seccomp、capabilitiesを利用しているに過ぎません。その一方で、常駐デーモンや、予期せず肥大化しがちなoverlayfsレイヤー、独自のネットワークブリッジなどを管理するためのオーバーヘッドをシステムに強いられます。

求めていた要件は極めてシンプルでした。chroot並みに軽量でありながら、PID、Network、Mount、IPCを完全に隔離できること。そして何より、Linuxのinitシステム以外の余計なデーモンを一切常駐させないことです。

検討した3つのアプローチ

最終決定を下す前に、以下の3つのアプローチを検証しました。

  1. 手動cgroupsとchrootの組み合わせ: /sys/fs/cgroupを直接操作し、unshareコマンドを組み合わせる手法です。極めて軽量なものの、マウントポイントのクリーンアップやプロセスのライフサイクル管理を自作スクリプトで行うと、ゾンビプロセスが残留しやすくなります。
  2. Podman(Rootless): デーモン不要で非常にすっきりとした構成です。しかし、UID/GIDマッピング機構やfuse-overlayfsストレージレイヤーの影響により、大容量ファイルを扱う際のディスクI/Oパフォーマンスが15%〜20%低下しました。
  3. LXC / LXD: システムコンテナのモデルとしては非常に強力です。ただし、Ubuntu上のLXDはSnapパッケージに依存しており、不要な仮想マウントポイントや補助サービスが付いて回る点が懸念点でした。

最適解:systemd-nspawnの導入

綿密なベンチマークの結果、すべての要件を過不足なく満たしたのがsystemd-nspawnでした。このツールはsystemd-containerパッケージに含まれており、外部のランタイムを導入することなく、systemd既存のサブシステムを直接活用できます。

実際のUbuntu 22.04 VPS環境において、Dockerからsystemd-nspawnへ移行したことで、常駐メモリを約380MB即座に解放できました。また、デーモンを介さないため、ワーカーの起動時間も2.4秒から0.6秒未満へと大幅に短縮されました。

ステップ1:基本ツールのインストール

Debian/Ubuntu環境では、systemdのコンテナパッケージと、最小限のOSイメージを構築するためのdebootstrapをインストールするだけで準備完了です。

sudo apt update
sudo apt install -y systemd-container debootstrap

ステップ2:コンテナ用ルートファイルシステム(rootfs)の作成

systemd-nspawnは、完全なLinuxディレクトリツリー(rootfs)を含むディレクトリ上で直接動作します。ここでは、最小構成のUbuntu 22.04(Jammy)を/var/lib/machines/app-workerにブートストラップします。

sudo mkdir -p /var/lib/machines/app-worker
sudo debootstrap jammy /var/lib/machines/app-worker http://archive.ubuntu.com/ubuntu/

このコマンドにより、約300MBの基本パッケージ群がダウンロードされます。なお、/var/lib/machinesはsystemdによって自動検出される標準ディレクトリです。

ステップ3:対話型シェルでの初期設定

コンテナ内に初回アクセスし、アカウント設定や必要なパッケージの導入を行います。

sudo systemd-nspawn -D /var/lib/machines/app-worker

この時点でコンテナの隔離環境内に入っています。rootパスワードを設定し、必要なツールをインストールしましょう。

passwd
apt update && apt install -y curl htop
exit

exitと入力するとホスト環境に戻ります。

ステップ4:独立したOSとしてコンテナをブートする

-b(boot)フラグを付与することで、systemd-nspawnはコンテナ内部で独立したPID 1(systemd)プロセスを起動します。これにより、小型のVPSのようにコンテナ自身がサービスを管理できるようになります。

sudo systemd-nspawn -b -D /var/lib/machines/app-worker

コンテナをバックグラウンドで実行したままコンソールから抜けるには、キーボードショートカットのCtrl + ]を素早く3回連続で押します。

ステップ5:machinectlによる一元管理

手動で操作する代わりに、machinectlを使用することでコンテナのライフサイクルを効率的に管理できます。

# コンテナをバックグラウンドで起動
sudo machinectl start app-worker

# 稼働中のマシン一覧を表示
sudo machinectl list

# コンテナ内の対話型シェルを開く
sudo machinectl shell app-worker

# 実際のリソース(CPU・RAM)消費状況を確認
sudo machinectl status app-worker

ステップ6:自動起動とリソース制限の設定

サーバー再起動時にコンテナを自動起動させるには、標準で用意されているsystemdのサービステンプレートを有効化します。

sudo systemctl enable --now [email protected]

コンテナのリソース上限を最大1GBのRAMおよび50%のCPUに制限したい場合は、ドロップインによるオーバーライドファイルを作成します。

sudo systemctl edit [email protected]

以下の設定を貼り付けて保存します。

[Service]
MemoryMax=1G
CPUQuota=50%

systemdデーモンをリロードして変更を即座に適用します。

sudo systemctl daemon-reload
sudo systemctl restart [email protected]

6ヶ月間の実運用で見えた効果

本番環境で半年間運用を続けた結果、バックグラウンドデーモンに起因するメモリリークなども一切発生せず、サーバー群は極めて安定して稼働しています。

特に、バックアップ作業が格段にシンプルになりました。コンテナ全体が単一のディレクトリとして完結しているため、定期的なrsyncによる同期や、Btrfs/ZFSを利用している場合のファイルシステムスナップショットだけで済みます。リソースの限られたVPSを管理しており、Dockerのオーバーヘッドをかけずに軽量かつ安全な隔離環境を構築したいなら、systemd-nspawnは間違いなく試す価値のある選択肢です。

Share: