systemd-nspawn やそれを使った machinectl でのノウハウがたまってきたので、何回かにわけてまとめていきたいと思います。

動作確認環境

  • Apple M1 Pro の macOS Monterey 12.6
  • limactl version 0.12.0
  • aarch64 の Debian GNU/Linux 11 (bullseye)
  • aarch64 の Ubuntu 22.04.1 LTS (jammy)

環境作成

limactl start --name=debian-nspawn template://debianlimactl start --name=ubuntu-nspawn template://ubuntu-lts で作成した環境で試しました。

特に差がない部分については、Debian での実行結果を載せています。

初期設定

コンテナのネットワーク設定との連携の都合で、ネットワークの設定を ifupdown から systemd-networkdsystemd-resolved に変更しておきます。

Ubuntu 22.04 では、細かく確認したわけではないのですが、 netplan (/etc/netplan/50-cloud-init.yaml) で systemd-networkdsystemd-resolved の設定がされているようなので、そのままで大丈夫でした。

Debian 11 では https://wiki.debian.org/SystemdNetworkd を参考にして lan0.network を作成しておきます。

% limactl start --name=debian-nspawn template://debian
(略)
% limactl shell debian-nspawn
$ sudo mv /etc/network/interfaces{,.save}
$ printf '[Match]\nName=eth0\n\n[Network]\nDHCP=ipv4\n' | sudo tee /etc/systemd/network/lan0.network
[Match]
Name=eth0

[Network]
DHCP=ipv4
$ systemctl status systemd-networkd systemd-resolved
● systemd-networkd.service - Network Service
	 Loaded: loaded (/lib/systemd/system/systemd-networkd.service; disabled; vendor preset: enabled)
	 Active: inactive (dead)
TriggeredBy: ● systemd-networkd.socket
	   Docs: man:systemd-networkd.service(8)

● systemd-resolved.service - Network Name Resolution
	 Loaded: loaded (/lib/systemd/system/systemd-resolved.service; disabled; vendor preset: enabled)
	 Active: inactive (dead)
	   Docs: man:systemd-resolved.service(8)
			 man:org.freedesktop.resolve1(5)
			 https://www.freedesktop.org/wiki/Software/systemd/writing-network-configuration-managers
			 https://www.freedesktop.org/wiki/Software/systemd/writing-resolver-clients
$ sudo systemctl enable systemd-networkd systemd-resolved
Created symlink /etc/systemd/system/dbus-org.freedesktop.network1.service → /lib/systemd/system/systemd-networkd.service.
Created symlink /etc/systemd/system/multi-user.target.wants/systemd-networkd.service → /lib/systemd/system/systemd-networkd.service.
Created symlink /etc/systemd/system/sockets.target.wants/systemd-networkd.socket → /lib/systemd/system/systemd-networkd.socket.
Created symlink /etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service → /lib/systemd/system/systemd-networkd-wait-online.service.
Created symlink /etc/systemd/system/dbus-org.freedesktop.resolve1.service → /lib/systemd/system/systemd-resolved.service.
Created symlink /etc/systemd/system/multi-user.target.wants/systemd-resolved.service → /lib/systemd/system/systemd-resolved.service.
$ sudo poweroff
% limactl start debian-nspawn
(略)
% limactl shell debian-nspawn

固定アドレスのときは Network で以下のようにちゃんと設定しておきます。

[Match]
Name=eth0

[Network]
Address=192.168.0.2/24
Gateway=192.168.0.1
DNS=192.168.0.1

インストール

systemd-container パッケージをインストールすると /bin/machinectl/usr/bin/systemd-nspawn が使えるようになります。

$ sudo apt install systemd-container

btrfs を使う

machinectlbtrfs と組み合わせたときのみ使える機能があるようなので、 btrfs を使うようにしています。 / 自体が btrfs なら sudo btrfs subvolume create /var/lib/machines でサブボリュームを作成しておくだけで良さそうです。

他に適当な場所がなかったので、 / 直下に fallocate でディスクイメージファイルを作成しています。 それを btrfs でフォーマットして、 /var/lib/machines にマウントします。 マウントオプションは defaults に含まれる relatime に追加で lazytime を指定しているのと、ちょっと圧縮を強めにした compress=ztd:5 を指定しています。 zstd のデフォルトの 3 から 5 にしたのは適当で、ちゃんとした根拠はありません。

新しいファイルシステムをマウントするとパーミッションが変わってしまうので、 /usr/lib/tmpfiles.d/systemd-nspawn.conf の設定を反映させるために systemd-tmpfiles --create を実行しています。

$ sudo fallocate -l 50G /machines.img
$ sudo apt install btrfs-progs
$ sudo mkfs.btrfs /machines.img
btrfs-progs v5.10.1
See http://btrfs.wiki.kernel.org for more information.

Label:              (null)
UUID:               228788fc-dbbd-479f-8aee-f0e22791931a
Node size:          16384
Sector size:        4096
Filesystem size:    50.00GiB
Block group profiles:
  Data:             single            8.00MiB
  Metadata:         DUP             256.00MiB
  System:           DUP               8.00MiB
SSD detected:       no
Incompat features:  extref, skinny-metadata
Runtime features:
Checksum:           crc32c
Number of devices:  1
Devices:
   ID        SIZE  PATH
	1    50.00GiB  /machines.img

$ echo /machines.img /var/lib/machines btrfs defaults,lazytime,compress=zstd:5 0 0 | sudo tee -a /etc/fstab
/machines.img /var/lib/machines btrfs defaults,lazytime,compress=zstd:5 0 0
$ sudo mount /var/lib/machines
$ grep ' /var/lib/machines ' /usr/lib/tmpfiles.d/systemd-nspawn.conf
Q /var/lib/machines 0700 - - -
$ sudo systemd-tmpfiles --create

Ubuntu 22.04 では btrfs-progs がインストール済みでバージョンも Debian 11 より新しいものでした。

$ sudo mkfs.btrfs /machines.img
btrfs-progs v5.16.2
See http://btrfs.wiki.kernel.org for more information.

NOTE: several default settings have changed in version 5.15, please make sure
      this does not affect your deployments:
      - DUP for metadata (-m dup)
      - enabled no-holes (-O no-holes)
      - enabled free-space-tree (-R free-space-tree)

Label:              (null)
UUID:               d8d6616f-f2d4-478f-905b-4ecaee87d34f
Node size:          16384
Sector size:        4096
Filesystem size:    50.00GiB
Block group profiles:
  Data:             single            8.00MiB
  Metadata:         DUP             256.00MiB
  System:           DUP               8.00MiB
SSD detected:       no
Zoned device:       no
Incompat features:  extref, skinny-metadata, no-holes
Runtime features:   free-space-tree
Checksum:           crc32c
Number of devices:  1
Devices:
   ID        SIZE  PATH
    1    50.00GiB  /machines.img

mmdebstrap で bullseye を試す

debootstrap よりも mmdebstrap の方が新しくて良さそうだったので、そちらを使って、Debian のホスト側と同じ bullseye を試します。

Ubuntu では debian-archive-keyring もインストールしておきます。(Debian ではすでに入っています。)

--arch は省略できますが、今後の都合で指定しています。

他は普通に使うのに必要な最低限の設定を追加しています。

dbusmachinectl での制御に必須なので明示的に追加しています。

ホストとコンテナが同じ arch なら ifupdown の代わりに systemd-networkdsystemd-resolved でネットワークの自動設定ができるので、 /etc/network/interfaces を無効化して、 systemd-networkd などを有効化しています。

設定する SUITE として bullseye を指定しています。

TARGET として tarball を作成するようにしています。 /var/lib/machines/bullseye のようにディレクトリを指定して作成することもできるのですが、 ファイルの owner などの問題が起きることがあったので、一度 tarball にしてから machinectl import-tar で取り込む手順にしています。 その方が繰り返し import からやり直すこともできるという利点もあります。

$ sudo apt install mmdebstrap
(略)
$ sudo apt install debian-archive-keyring
(略)
$ mmdebstrap --arch=arm64 --include=dbus \
   --customize-hook='chroot "$1" mv /etc/network/interfaces /etc/network/interfaces.save' \
   --customize-hook='chroot "$1" systemctl enable systemd-networkd systemd-resolved' \
   bullseye /tmp/arm64-bullseye.tar
I: automatically chosen mode: unshare
I: chroot architecture arm64 is equal to the host's architecture
I: automatically chosen format: tar
I: using /tmp/mmdebstrap.cUm6DaB4cZ as tempdir
I: running apt-get update...
done
I: downloading packages with apt...
done
I: extracting archives...
done
I: installing essential packages...
done
I: downloading apt...
done
I: installing apt...
done
I: installing remaining packages inside the chroot...
done
done
I: running --customize-hook in shell: sh -c 'chroot "$1" mv /etc/network/interfaces /etc/network/interfaces.save' exec /tmp/mmdebstrap.cUm6DaB4cZ
I: running --customize-hook in shell: sh -c 'chroot "$1" systemctl enable systemd-networkd systemd-resolved' exec /tmp/mmdebstrap.cUm6DaB4cZ
Created symlink /etc/systemd/system/dbus-org.freedesktop.network1.service → /lib/systemd/system/systemd-networkd.service.
Created symlink /etc/systemd/system/multi-user.target.wants/systemd-networkd.service → /lib/systemd/system/systemd-networkd.service.
Created symlink /etc/systemd/system/sockets.target.wants/systemd-networkd.socket → /lib/systemd/system/systemd-networkd.socket.
Created symlink /etc/systemd/system/network-online.target.wants/systemd-networkd-wait-online.service → /lib/systemd/system/systemd-networkd-wait-online.service.
Created symlink /etc/systemd/system/dbus-org.freedesktop.resolve1.service → /lib/systemd/system/systemd-resolved.service.
Created symlink /etc/systemd/system/multi-user.target.wants/systemd-resolved.service → /lib/systemd/system/systemd-resolved.service.
I: cleaning package lists and apt cache...
done
done
I: creating tarball...
I: done
I: removing tempdir /tmp/mmdebstrap.cUm6DaB4cZ...
I: success in 15.0921 seconds

tarball が作成できたら、 machinectl import-tar で取り込みます。

$ sudo machinectl import-tar /tmp/arm64-bullseye.tar arm64-bullseye
Enqueued transfer job 1. Press C-c to continue download in background.
Importing '/tmp/arm64-bullseye.tar', saving as 'arm64-bullseye'.
Imported 0%.
Imported 47%.
Imported 82%.
Imported 94%.
Imported 95%.
Imported 96%.
Imported 97%.
Imported 99%.
Operation completed successfully.
Exiting.
$ sudo ls -al /var/lib/machines
total 20
drwx------  1 root root   28 Sep 27 04:00 .
drwxr-xr-x 25 root root 4096 Sep 27 03:57 ..
drwxr-xr-x  1 root root  122 Sep 27 03:59 arm64-bullseye
$ sudo btrfs subvolume list /var/lib/machines
ID 257 gen 10 top level 5 path arm64-bullseye
$ machinectl list-images
NAME           TYPE      RO  USAGE CREATED                     MODIFIED
arm64-bullseye subvolume no 101.6M Tue 2022-09-27 04:00:56 UTC n/a

1 images listed.
$ machinectl list
No machines.

インポートできたら machinectl start で起動します。 machinectl shellroot 権限が必要です。

$ sudo machinectl start arm64-bullseye
$ machinectl list
MACHINE        CLASS     SERVICE        OS     VERSION ADDRESSES
arm64-bullseye container systemd-nspawn debian 11      192.168.9.39…

1 machines listed.
$ machinectl shell arm64-bullseye
Failed to get shell PTY: Access denied
$ sudo machinectl shell arm64-bullseye
Connected to machine arm64-bullseye. Press ^] three times within 1s to exit session.
root@lima-debian-nspawn:~#

これで apt update などが動きます。

一般ユーザー権限で machinectl shell を実行すると、 Debian では上の例のように Failed to get shell PTY: Access denied になりますが、 Ubuntu 22.04 では以下のようにパスワードをきかれてしまいます。

$ machinectl shell arm64-bullseye
==== AUTHENTICATING FOR org.freedesktop.machine1.shell ===
Authentication is required to acquire a shell in a local container.
Authenticating as: root
Password:

トラブルシューティング

shell が開けない

Failed to get shell PTY: Access denied は権限不足なので sudo で解決します。

machinectl の操作がきかない

machinectl shellFailed to get shell PTY: Protocol error になるときは dbus が動いていません。

sudo systemd-nspawn -U -D /var/lib/machines/$NAME を使って、システムコンテナではなく直接シェルを起動して dbus をインストールすれば解決します。

-U を忘れて apt update してしまうと、ファイルの owner がずれてしまいます。 そうなると復旧は困難なので、必要なファイルを直接 /var/lib/machines/$NAME の下からコピーしてバックアップして、コンテナを作りなおすのが無難です。

$ sudo systemd-nspawn -U -D /var/lib/machines/bullseye
# apt update
# apt install dbus

他にもシェルからの操作で直す必要があるときは systemd-nspawn -U -D が使えます。

ファイルの編集だけで直せるときは /var/lib/machines/$NAME の下のファイルを直接編集してしまっても良いと思います。

ネットワーク問題

machinectl listADDRESSES- のときはネットワークにつながっていません。

$ machinectl list
MACHINE        CLASS     SERVICE        OS     VERSION ADDRESSES
arm64-bullseye container systemd-nspawn debian 11      -

1 machines listed.

ホスト側で systemd-networkd を使わずに ifupdown/etc/network/interfaces での設定を使っていると n/aunmanaged になります。

$ networkctl
WARNING: systemd-networkd is not running, output will be incomplete.

IDX LINK            TYPE     OPERATIONAL SETUP
  1 lo              loopback n/a         unmanaged
  2 eth0            ether    n/a         unmanaged
  3 ve-bullseyeHYh7 ether    n/a         unmanaged

3 links listed.

コンテナ側でのネットワーク設定ができていないと no-carrierconfiguring になります。

$ networkctl
IDX LINK            TYPE     OPERATIONAL SETUP
  1 lo              loopback carrier     unmanaged
  2 eth0            ether    routable    configured
  3 ve-bullseyeHYh7 ether    no-carrier  configuring

4 links listed.

正常なときは routableconfigured になります。

$ networkctl
IDX LINK            TYPE     OPERATIONAL SETUP
  1 lo              loopback carrier     unmanaged
  2 eth0            ether    routable    configured
  3 ve-bullseyeHYh7 ether    routable    configured

3 links listed.

systemd-networkdsystemd-resolved を使うなら、 mmdebstrap の引数で以下のように設定しておきます。

--customize-hook='chroot "$1" mv /etc/network/interfaces /etc/network/interfaces.save'
--customize-hook='chroot "$1" systemctl enable systemd-networkd systemd-resolved'

Debian のデフォルトの ifupdown をそのまま使うなら、 mmdebstrap の引数で以下のように host0 の設定を追加しておきます。

--customize-hook='printf "auto host0\niface host0 inet dhcp\n" > "$1"/etc/network/interfaces.d/host0'

ホスト名を変更する

tarball 作成前なら mmdebstrap の引数で以下のようにホスト名の設定をしておきます。

--customize-hook='echo '"$NAME"' > "$1/etc/hostname"'
--customize-hook='echo "127.0.1.1 '"$NAME"'" >> "$1/etc/hosts"'

作成済みのコンテナで同様の設定をするなら以下のようになります。

$ sudo machinectl shell arm64-bullseye
Connected to machine arm64-bullseye. Press ^] three times within 1s to exit session.
root@lima-debian-nspawn:~# cat /etc/hostname
lima-debian-nspawn
root@lima-debian-nspawn:~# echo arm64-bullseye > /etc/hostname
root@lima-debian-nspawn:~# echo 172.0.1.1 arm64-bullseye >> /etc/hosts
root@lima-debian-nspawn:~# poweroff

Connection to machine arm64-bullseye terminated.
$ sudo machinectl start arm64-bullseye
$ sudo machinectl shell arm64-bullseye
Connected to machine arm64-bullseye. Press ^] three times within 1s to exit session.
root@arm64-bullseye:~#

自動起動

machinectl enable でホスト起動時にコンテナも自動起動するように設定できます。 machinectl disable で戻せます。

$ sudo machinectl enable arm64-bullseye
Created symlink /etc/systemd/system/machines.target.wants/systemd-nspawn@arm64-bullseye.service → /lib/systemd/system/systemd-nspawn@.service.
$ sudo machinectl disable arm64-bullseye
Removed /etc/systemd/system/machines.target.wants/systemd-nspawn@arm64-bullseye.service.
$ sudo systemctl enable systemd-nspawn@arm64-bullseye.service
Created symlink /etc/systemd/system/machines.target.wants/systemd-nspawn@arm64-bullseye.service → /lib/systemd/system/systemd-nspawn@.service.

systemd-nspawn@arm64-bullseye.service の操作なので、 sudo systemctl enable systemd-nspawn@arm64-bullseye.service のように systemctl を使っても同じですが、 短いので machinectl を使っています。

systemd service

systemd の unit なので、 systemctl edit systemd-nspawn@arm64-bullseye.service で個別の設定をしたり、 systemctl edit systemd-nspawn@.service で共通の設定をしたりできます。

/tmp の容量不足問題対応

systemd-nspawn のデフォルトでは /tmptmpfs になって、容量が少なくて困ることがあったので、 systemd-nspawnのtmpディレクトリに書いてあるように SYSTEMD_NSPAWN_TMPFS_TMP=0 を設定しています。

$ sudo systemctl edit systemd-nspawn@.service
$ cat /etc/systemd/system/systemd-nspawn\@.service.d/override.conf
[Service]
Environment=SYSTEMD_NSPAWN_TMPFS_TMP=0

参考サイトでは /etc/systemd/system/systemd-nspawn@.service を作成して変更しているようですが、 バージョンアップ時に困ることがあるので、 systemd の作法にのっとって、 /etc/systemd/system/systemd-nspawn@.service.d/*.conf を作成するのがオススメです。

nspawn ファイル

/lib/systemd/system/systemd-nspawn@.service では ExecStart=systemd-nspawn --quiet --keep-unit --boot --link-journal=try-guest --network-veth -U --settings=override --machine=%i となっていて、このオプションを変更したいこともあると思いますが、 これは /etc/systemd/system/systemd-nspawn@.service.d/*.conf ではなく /etc/systemd/nspawn/*.nspawn で変更するのがオススメです。

--settings=override がついているので、 systemd.nspawn(5) に説明されているような設定が変更できます。 設定項目は systemd のバージョンによって使えるかどうかが違うことがあるので、対象の環境で man systemd.nspawn で確認してください。

/etc/systemd/system/systemd-nspawn@NAME.d/override.confmachinectl remove NAME をしても残りますが、 /etc/systemd/nspawn/NAME.nspawnmachinectl remove NAME で一緒に削除されるようなので、 設定を使い回して作りなおしたいときは気を付けてください。

コンテナを終了する

machinectl poweroff が普通の終了方法で、 machinectl terminate が強制終了です。

うまく終了してくれないと思ったときは、 sudo systemctl stop systemd-nspawn@arm64-bullseye.service のように systemctl stop を使うと安全な方法から順番に試して最終的に全部止めてくれます。

コンテナの削除

止めた状態で machinectl remove NAME で削除できます。

まとめ

まず今回は systemd-nspawnmachinectl による最低限のシステムコンテナの作り方を紹介しました。

最終的に最低限のコンテナを増やして使って削除する手順はまとめなおすと以下のようになります。

$ NAME=arm64-bullseye
$ mmdebstrap --arch=arm64 --include=dbus \
   --customize-hook='chroot "$1" mv /etc/network/interfaces /etc/network/interfaces.save' \
   --customize-hook='chroot "$1" systemctl enable systemd-networkd systemd-resolved' \
   --customize-hook='echo '"$NAME"' > "$1/etc/hostname"' \
   --customize-hook='echo "127.0.1.1 '"$NAME"'" >> "$1/etc/hosts"' \
   bullseye /tmp/$NAME.tar
$ sudo machinectl import-tar /tmp/$NAME.tar $NAME
$ sudo machinectl start $NAME
$ sudo machinectl shell $NAME
# 作業
$ sudo machinectl poweroff $NAME
$ sudo machinectl terminate $NAME
$ sudo machinectl remove $NAME

継続的に使うなら、 sudo machinectl enable $NAME で自動起動します。

Disqus Comments

Kazuhiro NISHIYAMA

Ruby のコミッターとかやってます。 フルスタックエンジニア(って何?)かもしれません。 About znzに主なアカウントをまとめました。

znz znz


Published