genpack documentation

genpack イメージの起動機序

Gentoo Linux をベースに、不変システムイメージを宣言的に生成・配布・起動するための自社開発ツールチェーンの資料です。

概要

genpack で生成された SquashFS イメージは、ディスクにインストールされた実機環境と、vm コマンドによる QEMU/KVM 準仮想化環境の 2 つの方法で起動できます。いずれの場合も共通の initramfs(dracut-genpack)が overlayfs ルートを構成し、genpack-init が system.ini に基づいて初期設定を行った後に systemd へ制御を渡す、という流れは同じです。

本ドキュメントでは両方式の起動シーケンスを詳細に解説します。

system.img 方式(ディスクインストール)

ディスクレイアウト

genpack-install でディスクにインストールすると、以下のパーティション構成が作られます。

パーティション ファイルシステム 内容
1: ブートパーティション FAT32 EFI ブートローダー、カーネル、initramfs、system.img(4GiB 未満の場合)、system.ini
2: データパーティション Btrfs overlayfs upper 層、system(4GiB 以上の場合)

ブートパーティションのサイズはイメージサイズから自動計算されます。--superfloppy オプションを指定すると、パーティションテーブルを作成せずディスク全体を FAT32 として使用します(データパーティションなし)。

MBR と GPT はディスクサイズに応じて自動選択されます(2TiB 以下かつ 512 バイトセクタなら MBR、それ以外は GPT)。

起動シーケンス

UEFI/BIOS
  → GRUB (efi/boot/bootx64.efi 等)
    → Linux カーネル (root=systemimg:<UUID> or root=systemimg:auto)
      → initramfs (dracut-genpack)
        → overlayfs ルート構成
          → genpack-init (PID 1)
            → Python プラグインで初期設定
              → exec /sbin/init (systemd)

ブートローダー

genpack-install は各アーキテクチャ向けの GRUB ブートローダーをビルドし、SquashFS イメージの /usr/lib/genpack-install/ に同梱します。

EFI ブートローダー:

grub-mkstandalone を使い、grub.cfg を内蔵した単体 EFI バイナリとして生成されます。ディスク上に外部の設定ファイルを必要としません。

バイナリ ターゲット
bootx64.efi x86_64
bootia32.efi i386
bootaa64.efi ARM64
bootriscv64.efi RISC-V 64

BIOS ブートローダー:

boot.img(MBR ステージ 1)と core.imggrub-mkimage で生成)の組み合わせです。core.img にはプレフィックス (,msdos1)/boot/grub がハードコードされており [1]、ブートパーティションの /boot/grub/grub.cfg を読み込みます。BIOS の場合、grub.cfg はバイナリに内蔵されず、genpack-install がディスクインストール時にブートパーティションへ配置します。

grub.cfg の処理フロー

EFI バイナリに内蔵された(BIOS の場合はブートパーティション上の)grub.cfg は以下の処理を行います。

1. シリアルコンソールの初期化

COM0 を 115200 baud で試行し、成功すればシリアルとコンソールの両方を入出力端末として設定します。

2. ブートパーティションの特定

GRUB 変数 $cmdpath(ブートローダーの起動元パス)からブートパーティションを推定し、probe -u で UUID を取得します。

3. システムイメージの検出

以下の順序で SquashFS イメージを検索します。

  1. ブートパーティション上の system.img
  2. データパーティション上の system(ラベル data-<UUID>d-<UUID> → パーティション番号によるフォールバックの順で検索)

4. SquashFS のマウントとカーネル検出

loopback コマンドで SquashFS をループバックマウントし、set root=loop でルートを切り替えます。イメージ内に /boot/grub/grub.cfg が存在する場合は configfile で読み込みを委譲します [2]

存在しない場合は以下の処理を続行します。

5. タイムアウトの決定

ブートパーティション上に boottime.txt が残存している場合(前回 unclean shutdown の証拠 [3])はタイムアウトを 10 秒に設定し [4]、通常時は 1 秒に設定します。

6. カスタム設定の読み込み

ブートパーティション上に system.cfg が存在すれば source で読み込みます [5]。このファイルで LINUX_ARGS 変数を設定することでカーネルコマンドラインをカスタマイズできます。

7. カーネルコマンドラインの構成

8. メニューエントリ

エントリ カーネルコマンドライン
Normal mode linux /boot/kernel root=systemimg:<UUID> $LINUX_ARGS systemd.firstboot=0
Transient mode 上記に genpack.transient=1 を追加

カーネルと initramfs は SquashFS 内の /boot/kernel/boot/initramfs が使用されます(ループバックマウント済みのため、GRUB は SquashFS 内のファイルを直接参照できます)。MemTest86 が利用可能な場合は追加のメニューエントリが表示されます。

上記の処理でシステムイメージが見つからなかった場合、データパーティション上の grub.cfg やカーネルでの直接起動を試みるフォールバックパスも存在します [6]

initramfs の処理(dracut-genpack)

dracut-genpack は 2 つのフックで構成されます。

cmdline フック(check-systemimg-root.sh):

カーネルコマンドラインの root= パラメータを確認します。root=systemimg:... 形式であれば、genpack のブートシーケンスを開始します。

mount フック(mount-genpack.sh):

  1. ブートパーティションの検出とマウント

    • root=systemimg:<UUID> の場合: 指定 UUID のパーティションをマウント
    • root=systemimg:auto の場合: 全 FAT パーティションを走査し、system.img を含むものを検出
    • FAT の場合は fsck.fat -aw で自動修復後にマウント
    • マウントポイント: /run/initramfs/boot
  2. データパーティションの検出とマウント

    • ブートパーティションの UUID を基にラベル data-<UUID> で検索
    • フォールバック: d-<UUID>, wbdata-<UUID>、またはブートパーティションの次のパーティション番号 [7]
    • 見つからない場合は virtiofs (fs タグ) を試行 [8]
    • それでもマウントできない場合は tmpfs にフォールバック(トランジェントモード)
    • genpack.transient カーネルパラメータで明示的にトランジェントモードを指定可能
    • マウントポイント: /run/initramfs/rw
  3. SquashFS イメージのマウント

    • ブートパーティション上の /run/initramfs/boot/system.img を検索
    • 見つからない場合はデータパーティション上の /run/initramfs/rw/system を使用
    • read-only で /run/initramfs/ro にマウント
  4. overlayfs の構成

    • lowerdir: /run/initramfs/ro(SquashFS、読み取り専用)
    • upperdir: /run/initramfs/rw/root(Btrfs または tmpfs) [9]
    • workdir: /run/initramfs/rw/work
    • $NEWROOT に overlay をマウント
    • lower 層の /usr タイムスタンプを upper 層に同期 [10]
  5. シャットダウンプログラムの配置

    • /run/initramfs/ro/usr/libexec/genpack-shutdown/run/initramfs/shutdown にコピー
    • シャットダウン時に overlayfs と SquashFS を安全にアンマウントするために使用

genpack-init

dracut の initramfs 処理が完了すると、$NEWROOT にルートが切り替わり、/usr/bin/genpack-init が PID 1 として起動します(init=/usr/bin/genpack-init が dracut モジュールにより cmdline に追加される)。

genpack-init は C++ + pybind11 で実装されており、以下の処理を行います。

  1. /run/initramfs/boot/system.ini(ブートパーティション経由)または /run/initramfs/rw/system.ini(データパーティション経由)を読み込む
  2. /usr/lib/genpack-init/*.py 内の全 Python モジュールをファイル名順にロード
  3. 各モジュールの configure(ini) 関数を実行(タイムゾーン、ロケール、バナー表示、マシン ID 生成など)
  4. exec /sbin/init で systemd に制御を引き渡す

パラバーチャル方式(vm コマンド)

vm コマンドの概要

vm コマンドは genpack イメージを QEMU/KVM で直接起動するためのツールです。ディスクへのインストールは不要で、SquashFS ファイルをそのまま指定して起動できます。

起動シーケンス

vm run system.squashfs
  → SquashFS からカーネルと initramfs を抽出 (memfd)
    → qemu-system-<arch> -kernel <kernel> -initrd <initramfs>
       -append "root=/dev/vda ro ..."
       -drive file=system.squashfs,...,serial=system  (virtio-blk)
       -drive file=data,...,serial=data  (virtio-blk, あれば)
      → initramfs (dracut-genpack)
        → overlayfs ルート構成
          → genpack-init (PID 1)
            → Python プラグインで初期設定
              → exec /sbin/init (systemd)

カーネルと initramfs の抽出

vm コマンドは squashfuse ライブラリを使用して、SquashFS イメージの /boot/ ディレクトリからカーネルと initramfs を直接読み出します。ディスクに展開する必要はなく、memfd_create で作成したメモリ上のファイルディスクリプタに書き出して QEMU に渡します。

検索順序:

  1. boot/kernel または boot/vmlinuz(固定名)
  2. boot/kernel-* または boot/vmlinuz-*(タイムスタンプが最新のもの)

initramfs も同様に boot/initramfs, boot/initramfs.img, boot/initrd.img の順で検索されます。

カーネルバイナリの ELF ヘッダまたは PE ヘッダからアーキテクチャ(x86_64, aarch64, riscv64 等)を自動判定し、対応する qemu-system-<arch> を起動します。

QEMU の起動構成

vm コマンドは QEMU のダイレクトカーネルブート(-kernel, -initrd, -append)を使用します。ブートローダーは介在しません。

カーネルコマンドライン:

root=/dev/vda ro net.ifnames=0 systemd.firstboot=0 systemd.hostname=<vmname> console=...

ディスクの提供:

virtio デバイス シリアル 内容
vda system SquashFS イメージ(読み取り専用)
vdb data データディスク(あれば)
vdc swap スワップファイル(あれば)

SquashFS イメージは virtio-blk-pci デバイスとして read-only で接続されます。initramfs の mount-genpack.shroot=block:* ではなくカーネルコマンドラインの root=/dev/vda を参照し、/dev/vda を直接 SquashFS として /run/initramfs/ro にマウントします。

virtiofs モード:

vm コマンドは virtiofs もサポートしています。virtiofsd を起動してホストのディレクトリをゲストに共有し、overlayfs の upper 層として使用できます。virtiofs 使用時、initramfs は root=fs rootfstype=virtiofs rw に基づいて virtiofs をルートとしてマウントします。

vsock によるホスト通信

QEMU/KVM は virtio-vsock デバイスを通じてゲストとホスト間の通信チャネルを提供します。vm コマンドはすべての VM に対して vsock を自動的に有効化します。

ゲスト CID

各 VM には vsock::determine_guest_cid() によって VM 名から一意に決定されるゲスト CID(Context Identifier)が割り当てられます。起動時に CID が表示されます:

Guest CID: 12345
`ssh user@vsock%12345` to login to the VM.

vsock を使った SSH ログインには、ゲストイメージ側に socket_vmid の systemd-ssh-generator 設定が必要です。

--sock-forward オプション

SLIRP の NAT ではホスト上の Unix ドメインソケットに直接到達できません [11]--sock-forward オプションは、ゲスト内の vsock 接続をホスト上の Unix ドメインソケットサーバーにブリッジするプロキシプロセス(socat)を vm コマンドが起動・管理する機能です。

vm run system.squashfs --sock-forward /run/user/1007/aichannel.sock

複数指定可能です:

vm run system.squashfs \
  --sock-forward /run/user/1007/foo.sock \
  --sock-forward /run/user/1007/bar.sock

vsock ポート番号の決定:

vsock のリッスンポート番号はゲスト CID と同じ値になります。複数指定した場合は CID+0、CID+1、CID+2、… と順に割り当てられます。ゲスト内から /dev/vsock への IOCTL_VM_SOCKETS_GET_LOCAL_CID ioctl で自身の CID を取得できるため、ゲスト側スクリプトは「ポート番号 = 自分の CID + オフセット」という規則だけで正しいポートに接続できます。

通信経路:

[ゲスト内アプリ]
    ↓ TCP localhost:PORT
[ゲスト内 socat ブリッジ]  ← systemd ユニットで自動起動
    ↓ VSOCK CID=2(host):PORT
[ホスト側 socat プロキシ]  ← vm コマンドが起動・管理
    ↓ UNIX-CONNECT(接続ごとに fork)
[ホスト上の Unix ドメインソケットサーバー]

fork オプションにより接続ごとに独立した Unix ソケット接続が確立されるため、並行接続・keep-alive タイムアウトの問題が発生しません [12]

ゲスト側ブリッジ:

ゲスト側の TCP → vsock ブリッジは以下のヘルパースクリプトと systemd テンプレートユニットで実現できます。

/usr/local/bin/sock-forward:

#!/usr/bin/python3
import os, fcntl, struct, socket, sys
fd = os.open("/dev/vsock", os.O_RDONLY)
cid = struct.unpack("I", fcntl.ioctl(fd, socket.IOCTL_VM_SOCKETS_GET_LOCAL_CID, bytes(4)))[0]
os.close(fd)
arg = sys.argv[1].split(":")
port, offset = arg[0], int(arg[1]) if len(arg) > 1 else 0
os.execlp("socat", "socat", f"TCP-LISTEN:{port},fork,reuseaddr", f"VSOCK-CONNECT:2:{cid + offset}")

/etc/systemd/system/[email protected]:

[Unit]
Description=VM service bridge (TCP %i -> vsock host)

[Service]
ExecStart=/usr/local/bin/sock-forward %i
Restart=on-failure

[Install]
WantedBy=multi-user.target

systemctl enable sock-forward@8080 とすれば localhost:8080 → vsock:2:<自分の CID> のブリッジが起動します。ポート番号に :1:2 … のオフセットを付加することで(例: sock-forward@8080:1)複数のホストソケットに対応できます。

vm サービスモード

vm コマンドには vm.ini ファイルを読み取るサービスモードがあります。各 VM のディレクトリ構成は以下の通りです。

/var/vm/<vmname>/
├── vm.ini        # VM 設定(メモリ、CPU、ネットワーク等)
├── system        # SquashFS イメージ(シンボリックリンク可)
├── data          # データディスク(オプション)
├── swapfile      # スワップ(オプション)
├── fs/           # virtiofs 共有ディレクトリ
├── docker        # Docker 用ディスク(オプション、serial=docker)
└── mysql         # MySQL 用ディスク(オプション、serial=mysql)

vm.initype=genpack(デフォルト)でダイレクトカーネルブートが使用されます。

system.img 方式との共通点と相違点

項目 system.img 方式 パラバーチャル方式
ブートローダー GRUB (EFI/BIOS) なし(ダイレクトカーネルブート)
カーネル格納場所 ブートパーティション上のファイル SquashFS 内から memfd に抽出
root= パラメータ systemimg:<UUID> or systemimg:auto /dev/vda
SquashFS の提供 ブート/データパーティション上のファイル virtio-blk デバイス
データ永続化 Btrfs パーティション data ディスクファイルまたは virtiofs
トランジェントモード genpack.transient カーネルパラメータ data ディスクを指定しなければ自動
system.ini FAT32 パーティション上 virtiofs 経由または fw_cfg
initramfs の動作 共通(dracut-genpack) 共通(dracut-genpack)
genpack-init の動作 共通 共通

シャットダウン

genpack イメージのシャットダウンは通常の systemd シャットダウンプロセスの後、dracut の initramfs に制御が戻り、/run/initramfs/shutdown(genpack-shutdown)が実行されます。genpack-shutdown は以下を行います。

  1. /oldroot 以下の全マウントポイントを逆順にアンマウント
  2. /run/initramfs/rw(データパーティション)と /run/initramfs/boot(ブートパーティション)を安全に移動・アンマウント
  3. ブートパーティション上の boottime.txt を削除 [3:1]
  4. reboot(2) または poweroff を実行

ソースリファレンス

このドキュメントは以下のリポジトリのスナップショットに基づいて作成されました:


  1. BIOS ブートは MBR パーティション構成が前提です。GPT ディスクの場合は EFI ブートが使用されます。 ↩︎

  2. イメージ固有のカーネルパラメータが必要な場合やスプラッシュ画面を表示したい場合など、イメージ側でブート構成をカスタマイズするための仕組みです。BOOT_PARTITIONBOOT_PARTITION_UUID がエクスポートされるため、委譲先の grub.cfg からもブートパーティション情報を参照できます。 ↩︎

  3. boottime.txt はブート時に作成されます。clean shutdown 時に削除されるため、次回ブート時にこのファイルが残存していれば前回の unclean shutdown(クラッシュや電源断)の証拠となります。 ↩︎ ↩︎

  4. 前回の unclean shutdown 後にタイムアウトを延長することで、オペレータがトランジェントモードや MemTest86 を選択する猶予を確保します。正常シャットダウン時には boottime.txt が削除されるため、通常起動では 1 秒の短いタイムアウトで自動ブートします。 ↩︎

  5. GRUB は INI ファイルフォーマットをネイティブに解析できないため、ブートローダー段階で必要な設定は GRUB スクリプト形式の system.cfg に、genpack-init 段階で必要なシステム設定は INI 形式の system.ini にという 2 層構造になっています。 ↩︎

  6. genpack 以外のシステムとの共存を想定した隠し機能です。データパーティション上に独立した grub.cfg やカーネル/initramfs があれば、そちらからの起動を試みます。常に成功する保証はありません。 ↩︎

  7. d-<UUID> が現在の正式なラベルフォーマットです(Btrfs のラベル長制限のため短縮形を使用)。data-<UUID> および wbdata-<UUID> は Walbrix(genpack の前身)時代の互換性のために残されています。 ↩︎

  8. systemimg 方式は実機専用ではなく、vm フロントエンドを介さずに QEMU 上で直接実行される場合もあります。このフォールバックにより、baremetal プロファイルのイメージも準仮想化環境で起動可能です。baremetal プロファイルが systemimg の特化として存在するのもこの理由です。 ↩︎

  9. 旧バージョンでは upperdir が rw/rw/root という冗長なパスでした。rw/root に簡略化されましたが、既存環境との互換性のため initramfs は旧パス rw/rw/root も引き続き認識します。 ↩︎

  10. systemd は /usr のタイムスタンプを参照して ld.so.cache の再生成が必要かを判定します。overlayfs では upper 層が存在すると lower 層のタイムスタンプが隠されるため、lower 層の /usr タイムスタンプを明示的に upper 層に伝播させる必要があります。 ↩︎

  11. SLIRP は TCP/UDP のみを対象としたユーザー空間 NAT です。ホスト上の Unix ドメインソケットは TCP/IP ネットワークの外に存在するため SLIRP では到達できません。ホスト上の TCP サービスへのアクセスは SLIRP の NAT で直接届くため、--sock-forward の対象外です。 ↩︎

  12. QEMU の --guestfwd オプションも Unix ドメインソケットへの転送をサポートしますが、すべての TCP 接続に対して 1 本の chardev(Unix ソケット接続)を使い回す設計のため、並行接続や keep-alive タイムアウトがある実際の使用には耐えられません。vsock + socat のアーキテクチャはこの制約を回避します。 ↩︎

当社代表のデスクトップ(※)を常時ライブ配信中

※ライブ配信専用PC

OSSの検証や自社用ツールの開発といった公開できる作業に限り、 ライブ配信専用PC上で行っています。常時配信ですのでいつでもお気軽にチャットメッセージ(公開)を残していって下さい。