Ubuntu Weekly Recipe

第929回Imagecraft 1.0でUbuntuの仮想マシンイメージを自作する

2026年9月、CanonicalからImagecraftのバージョン1.0がリリースされました。Imagecraftは、UbuntuをベースとしたOSイメージを作成するためのツールです。用途に合わせて必要なパッケージやファイルシステムなどを定義し、独自のUbuntuイメージを作成できます。

今回はこのImagecraftを使って、実際に起動可能なUbuntuの仮想マシンイメージを作る方法を解説します。

Imagecraftとは

歴史的な経緯もあり、ブート可能なUbuntuのイメージを作成するためには、様々なツールが存在しています。そのため自分でカスタムした仮想マシンイメージを作ろうとすると、従来は複数のツールを使いこなすための知識が必要でした。

現在Canonicalは、ソフトウェアや成果物のビルド処理に必要なツールの共通化を進めています。具体的な実装としては、SnapパッケージをビルドするSnapcraftや、Juju Charmのパッケージングを行うCharmcraftなどです。これらのツールは共通して、宣言的なYAML、隔離されたクリーンなビルド環境、再利用可能なプラグインといった特徴を持っています。そして数あるCraftツール群の中で、ブート可能なOSイメージを作るためのツールがImagecraftです。

Imagecraftは手順を逐一スクリプトとして記述するのではなく、作成したいイメージの構成をYAMLファイルとして宣言的に記述します。そのためYAMLファイルを見るだけで、作成されるイメージのおおまかな構成を把握できます。またテキスト形式のYAMLファイルであるため、Gitなどでバージョン管理しやすいのもメリットだと言えるでしょう。同じ定義ファイルからイメージを繰り返し生成できるため、独自のカスタムを施したイメージを継続的にビルドするといった用途にも向いています。

Imagecraftのインストール

ImagecraftはSnapパッケージとして提供されています。まずは次のコマンドでインストールしてください。

$ sudo snap install imagecraft --classic

Imagecraftはイメージのビルドを、ホストから隔離された環境で行います。今回ビルド環境にはMultipassを使用しますので、こちらもSnapパッケージからインストールしておきましょう。

$ sudo snap install multipass

仮想マシンイメージの定義

それではイメージを作ってみましょう。なお今回作成するイメージは、最後にqemu-system-x86_64を使って動作確認を行います。QEMUのGUIウィンドウから仮想マシンを操作するため、以下の作業はUbuntu Desktop上で行うものとして解説します。

まずは作業用のディレクトリを作成します。

$ mkdir ~/ubuntu-image
$ cd ~/ubuntu-image

このディレクトリ内で、imagecraft initを実行してください。これで構成を記述したimagecraft.yamlというファイルの雛形が作成されます。

$ imagecraft init

作成されたファイルを、テキストエディタで以下のように書き換えてください。

name: ubuntu
base: bare
build-base: ubuntu@24.04
version: '0.1'
summary: Custom Ubuntu virtual machine image
description: |
  A custom Ubuntu virtual machine image
  built with Imagecraft.

platforms:
  amd64:

volumes:
  disk:
    schema: gpt
    structure:
      - name: rootfs
        role: system-data
        type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
        filesystem: ext4
        filesystem-label: root
        size: 8 G
      - name: efi
        role: system-boot
        type: C12A7328-F81F-11D2-BA4B-00A0C93EC93B
        filesystem: vfat
        filesystem-label: uefi
        size: 512M

filesystems:
  default:
    - device: (volume/disk/rootfs)
      mount: /
    - device: (volume/disk/efi)
      mount: /boot/efi/

parts:
  rootfs:
    plugin: mmdebstrap
    mmdebstrap-suite: noble
    override-build: |
      craftctl default
      mkdir $CRAFT_PART_INSTALL/boot/efi/
      cat << EOF > $CRAFT_PART_INSTALL/etc/apt/sources.list.d/ubuntu.sources
      Types: deb
      URIs: http://archive.ubuntu.com/ubuntu
      Suites: noble noble-updates noble-backports
      Components: main restricted universe multiverse
      Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

      Types: deb
      URIs: http://security.ubuntu.com/ubuntu
      Suites: noble-security
      Components: main universe restricted multiverse
      Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
      EOF
    organize:
      '*': (overlay)/
  fstab:
    plugin: nil
    overlay-script: |
      cat << EOF > $CRAFT_OVERLAY/etc/fstab
      LABEL=root    /             ext4    discard,errors=remount-ro    0    1
      LABEL=uefi    /boot/efi/    vfat    umask=0077                   0    1
      EOF
  packages:
    plugin: nil
    overlay-packages:
      - ubuntu-server-minimal
      - linux-image-generic
      - grub2-common
      - grub-pc
      - shim-signed
      - login
      - cloud-init
      - git
      - docker.io

それなりの分量がありますので、上から順番に見ていきましょう。

プロジェクトの説明

冒頭の部分は、このプロジェクトそのものについての設定です。

name: ubuntu
base: bare
build-base: ubuntu@24.04
version: '0.1'
summary: Custom Ubuntu virtual machine image
description: |
  A custom Ubuntu virtual machine image
  built with Imagecraft.

name、version、summary、descriptionは、それぞれイメージの名前、バージョン、簡単な概要、詳細な説明です。

baseには、ビルドするイメージのベースとなる環境を指定します。今回は既存のイメージをベースとするのではなく、mmdebstrapを使ってまっさらなルートファイルシステムを構築するためbareを指定しています。

build-baseは(Multipass内で動く)イメージをビルドする環境のUbuntuバージョンを指定します。Imagecraftの設定上はUbuntu 26.04も指定できますが、2026年9月現在、イメージ取得元であるsnapcraft remoteに26.04が用意されておらず、使用できません。そのため今回はUbuntu 24.04を指定しました。[1]

ビルドするプラットフォームの設定

platformsでは、作成するイメージのプラットフォームを指定します。今回は一般的なx86_64 CPUを搭載したPCで利用するため、amd64だけを指定しました。

platforms:
  amd64:

ディスクとパーティションの設定

volumes以下では、作成するディスクイメージを設定します。

volumes:
  disk:
    schema: gpt
    structure:
      - name: rootfs
        role: system-data
        type: 0FC63DAF-8483-4772-8E79-3D69D8477DE4
        filesystem: ext4
        filesystem-label: root
        size: 8 G
      - name: efi
        role: system-boot
        type: C12A7328-F81F-11D2-BA4B-00A0C93EC93B
        filesystem: vfat
        filesystem-label: uefi
        size: 512M

schemaをgptとしているため、ここではGPT形式のパーティションテーブルが作成されます。

structure以下で、パーティションを設定します。8GBのrootfsと、512MBのefiという、2つのパーティションを作成しました。typeには、GPTのパーティションタイプGUIDを指定します。これはタイプごとに決まっており、rootfsはLinux filesystem dataなので「0FC63DAF-8483-4772-8E79-3D69D8477DE4⁠」⁠、efiはEFIシステムパーティションなので「C12A7328-F81F-11D2-BA4B-00A0C93EC93B」となります。またそれぞれのパーティションをext4/vfatでフォーマットし、識別用のラベルも付与しています。

volumesはあくまでパーティションを定義するだけなので、これだけでは利用できません。続くfilesystems以下で、イメージのパーティションとマウントポイントのマッピングを指定します。

filesystems:
  default:
    - device: (volume/disk/rootfs)
      mount: /
    - device: (volume/disk/efi)
      mount: /boot/efi/

これでビルド環境内において、rootfsパーティションが/に、efiパーティションが/boot/efi/にそれぞれマウントされます。

なおここで指定しているのは、Imagecraftがイメージを構築する際における、各パーティションとイメージ内のマウントポイントとの対応です。この設定だけで、完成したOSが起動時にパーティションを自動的にマウントしてくれるわけではありません。起動後のOSがどのパーティションをどこへマウントするかは、後述する/etc/fstabで別途設定します。

Ubuntuのルートファイルシステムを作成する

parts以下では、イメージを構築するための処理を、複数のpartに分けて設定します。それぞれのpartでは、プラグインを使ってファイルを生成したり、パッケージを追加したり、任意のコマンドを実行したりできます。

parts:
  rootfs:
    plugin: mmdebstrap
    mmdebstrap-suite: noble
    override-build: |
      craftctl default
      mkdir $CRAFT_PART_INSTALL/boot/efi/
      cat << EOF > $CRAFT_PART_INSTALL/etc/apt/sources.list.d/ubuntu.sources
      Types: deb
      URIs: http://archive.ubuntu.com/ubuntu
      Suites: noble noble-updates noble-backports
      Components: main restricted universe multiverse
      Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg

      Types: deb
      URIs: http://security.ubuntu.com/ubuntu
      Suites: noble-security
      Components: main universe restricted multiverse
      Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
      EOF
    organize:
      '*': (overlay)/

parts内では、様々なプラグインを利用できます。rootfsというpartではmmdebstrapプラグインを利用して、最小のルートファイルシステムを作っています。プラグインには、それぞれ固有のキーを指定できます。ここではmmdebstrap-suiteにnobleを指定し、Ubuntu 24.04 LTS(Noble Numbat)のルートファイルシステムを作ることを指示しています。

続いてoverride-buildがあります。これはプラグインが実行する通常のビルド処理を、独自の処理で上書きする指定です。

ここで重要なのが、craftctl defaultです。これは、プラグイン本来のビルド処理を明示的に実行させる指定です。というのもoverride-buildを指定すると、本来行うビルド処理全体が置き換えられてしまうのです。今回は「プラグイン本来の処理を行った上で、追加の処理を行いたい」ため、まず明示的にcraftctl defaultを実行し、その上で今回必要になる処理を追加しているというわけです。

追加している処理は見ての通りで、EFIシステムパーティションのマウントポイントを用意した上で、APTのリポジトリをubuntu.sourcesに設定しています。

Imagecraftでは、各partで生成したファイルを、最終的なイメージへ配置します。このpartで作成されたルートファイルシステム全体を、最終イメージのoverlayへ配置する指定が、末尾にあるorganizeとなります。ここでは全ファイルを、overlayのルートへ配置しています。

/etc/fstabを作成する

既にパーティションとファイルシステムは設定しましたが、起動時にこれらをどこへマウントするかを、作成後のOSに教えてあげる必要があります。そのための設定が/etc/fstabです。今回はfstabという別のpartを用意しました。

  fstab:
    plugin: nil
    overlay-script: |
      cat << EOF > $CRAFT_OVERLAY/etc/fstab
      LABEL=root    /             ext4    discard,errors=remount-ro    0    1
      LABEL=uefi    /boot/efi/    vfat    umask=0077                   0    1
      EOF

プラグインにはnilを指定しています。nilプラグインは、特別なビルド処理を行わないプラグインです。上記のようにプラグイン固有のビルド処理が不要で、自分が指定したコマンドだけを実行したいような場合に便利です。ここではcatコマンドを実行し、ルートファイルシステムを/、EFIシステムパーティションを/boot/efi/へマウントするfstabを作成しています。またファイルシステムの指定には、先ほど各ファイルシステムに付けたrootとuefiというラベルを使っています。

必要なパッケージを追加する

最後のpackagesというpartでは、イメージに追加するパッケージを指定しています。

  packages:
    plugin: nil
    overlay-packages:
      - ubuntu-server-minimal
      - linux-image-generic
      - grub2-common
      - grub-pc
      - shim-signed
      - login
      - cloud-init
      - git
      - docker.io

ubuntu-server-minimalに加えて、LinuxカーネルやGRUBなど、Ubuntuを仮想マシンとして起動するために最低限必要なパッケージを追加しています。さらに今回はloginも明示的に追加しています。後ほどQEMUのコンソールから、ユーザー名とパスワードを入力して対話的にログインするためです。

cloud-initも追加しています。作成したOSイメージには、ホスト名やユーザー名など、仮想マシンごとに異なる情報をなるべく直接埋め込みたくありません。またパスワードを直接イメージに埋め込むと、セキュリティ的なリスクにもなるでしょう。こうした情報は、仮想マシン起動時の初期化処理で行うのが一般的です。そのためイメージには、初期化を行うcloud-initパッケージを追加しているというわけです。

最後のgitとdocker.ioは、Imagecraftによるカスタマイズの具体例です。このように必要なパッケージをimagecraft.yamlへ追加しておけば、最初からGitやDockerを利用できる自分専用のUbuntuイメージを作成できます。開発チームごとに、必要なツールをプリインストールしたイメージを作るのもよい考えでしょう。

イメージのビルド

設定ファイルができたら、実際にイメージをビルドしてみましょう。今回はビルド環境にMultipassを利用するため、以下のコマンドでproviderを設定します。

$ sudo snap set imagecraft provider=multipass

続いて、imagecraft.yamlのあるディレクトリで以下のコマンドを実行します。

$ imagecraft pack

するとImagecraftはMultipassの仮想マシンを作成し、その中にビルド環境を用意します。今回はYAML内で指定したbuild-base: ubuntu@24.04に従って、Ubuntu 24.04環境が利用され、その中で実際にmmdebstrapなどを使ったイメージの構築が行われます。

ビルドが正常に終了すると、カレントディレクトリにdisk.imgが作成されます。これが完成したUbuntuの仮想ディスクです。

cloud-initで初期設定を行う

この段階で、作成したdisk.imgからUbuntuを起動できます。ただし、このまま起動してもまだ実用的には使えません。今回のイメージ内には、ユーザーがひとりも作成されていません。またrootユーザーのパスワードもロックされているため、そのままでは誰もログインできないのです。そこで初回起動時にユーザーを作成する設定を、cloud-initから与えます。

今回はcloud-initに渡す設定として、user-dataとmeta-dataという2つのファイルを作成します。まずuser-dataを以下の内容で作成してください。

#cloud-config

hostname: imagecraft-test

users:
  - name: ubuntu
    shell: /bin/bash
    sudo: ALL=(ALL) NOPASSWD:ALL
    lock_passwd: false

chpasswd:
  expire: false
  users:
    - name: ubuntu
      password: ubuntuユーザーに設定するパスワード
      type: text

bootcmd:
  - rm -f /etc/resolv.conf
  - ln -s /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf

内容を簡単に説明しましょう。まずhostnameによって、仮想マシンのホスト名をimagecraft-testに設定します。users以下では、ubuntuというユーザーを作成しています。またパスワードなしでsudoを利用できるようにしています。さらにchpasswdで、ユーザーのパスワードを設定しています。

最後にあるbootcmdでは、ブートプロセス時に実行するコマンドを設定しています。

現在のUbuntuでは、DNSリゾルバーの設定ファイルである/etc/resolv.confは通常、/run/systemd/resolve/stub-resolv.confへのシンボリックリンクになっています。そして実際の名前解決はsystemd-resolvedが担当します。ところが今回の方法でmmdebstrapからイメージを作成すると、/etc/resolv.confをシンボリックリンクへ置き換える処理が正常に行われず、完成したイメージに、0バイトの/etc/resolv.confが残ってしまう問題が発生しました。そのため仮想マシンを起動すると、IPアドレスを使った通信はできるにもかかわらず、DNSによる名前解決ができませんでした。

ただしsystemd-resolvedそのものは正常に起動しており、/run/systemd/resolve/stub-resolv.confも作成されています。そこで今回はcloud-initを使い、初回起動時に/etc/resolv.confを正しいシンボリックリンクへ置き換える処理を実行しているというわけです[2]。

続いてmeta-dataを以下の内容で作成します。

instance-id: imagecraft-test
local-hostname: imagecraft-test

これらのファイルをcloud-initから読み込ませるには、NoCloud形式のデータソースに変換する必要があります。以下のコマンドでcloud-image-utilsパッケージをインストールしてください。

$ sudo apt install -U -y cloud-image-utils

以下のコマンドで、user-dataとmeta-dataからseed.imgを作成します。

$ cloud-localds seed.img user-data meta-data

仮想マシンを起動する

disk.imgとseed.imgが揃ったら、いよいよQEMUで仮想マシンを起動してみましょう。今回はUEFIで起動するイメージを作成したため、QEMU本体に加えてUEFIファームウェアであるOVMFも必要です。以下のコマンドでインストールしてください。

$ sudo apt install -U -y qemu-system-x86 ovmf

OVMFでは、UEFIのファームウェア本体と、設定を保存する領域が別々のファイルになっています。ファームウェア本体であるOVMF_CODE_4M.fdは読み取り専用で使用できますが、仮想マシンのUEFI設定が保存されるOVMF_VARS_4M.fdは、書き込み可能である必要があります。そこでシステムにインストールされているファイルを直接使うのではなく、プロジェクトのディレクトリへコピーしておきましょう。

$ cp /usr/share/OVMF/OVMF_VARS_4M.fd ~/ubuntu-image/

ここまでの準備ができたら、以下のコマンドで仮想マシンを起動してください[3]。

$ qemu-system-x86_64 \
  -accel kvm \
  -m 4G \
  -cpu host \
  -smp 2 \
  -drive if=pflash,format=raw,readonly=on,file=/usr/share/OVMF/OVMF_CODE_4M.fd \
  -drive if=pflash,format=raw,file=./OVMF_VARS_4M.fd \
  -drive file=./disk.img,format=raw,index=0,media=disk \
  -drive file=./seed.img,format=raw,if=virtio

仮想マシンが正常に起動すると、QEMUのウィンドウにUbuntuのログインプロンプトが表示されます。cloud-initで作成したubuntuユーザーを使ってログインできることや、パスワードなしでsudoができること、インターネットと通信ができること、gitやdockerコマンドがインストールされていることなどを確認しておきましょう。

図1 QEMUで起動した仮想マシン

なおdisk.imgは一般的なraw形式のディスクイメージなので、QEMUだけでなくProxmox VEなどにインポートしても利用できます[4]。

図2 作成したdisk.imgは、Proxmox VEにインポートしても動かせる

単純にUbuntuの仮想マシンが欲しいだけなら、既存のUbuntu Cloud Imageを利用する方が簡単です。ですがImagecraftのメリットは、そうした「完成済みイメージのコピー」ではなく、イメージそのものの構成を自分で定義しゼロから作れる所です。

まだ1.0がリリースされたばかりということもあり、今回遭遇したresolv.confのように若干荒削りな部分もあります。しかし自分でUbuntuイメージを作るという、これまでは少々敷居が高かった作業を、YAMLの記述とコマンドひとつという所まで単純化できるため、なかなか面白いツールだと言えるでしょう。

imagecraft.yamlやcloud-initの設定をGitで管理し、CI/CDでクラウド向けのイメージを継続的にビルドするといった使い方もできそうです。ぜひ活用してみてください。

おすすめ記事

記事・ニュース一覧