Ubuntu Weekly Recipe

第678回distrobuilderでLXD/LXC用のカスタムイメージを作成する

distrobuilderは、Linux Containersプロジェクトで開発されている「LXD/LXC用イメージを構築するツール」です。特にLXD用のイメージサーバーで公開している各種イメージを日々ビルドするのに使われています。今回はこのツールを使って、独自のイメージをビルドする方法を紹介しましょう。

distrobuilderのインストール

distrobuilder自体は単なるGo言語製のプログラムです。Ubuntuだけでなく、Fedora/CentOS/Springdale/Alma/Rocky、Gentoo/Arch/openSUSE/Plamoといった有名どころのイメージはもちろん、Apertis/openWRT/Alpineといった特別な用途のイメージやDocker Hubで公開されているイメージをカスタマイズできます。手順はシンプルで、カスタマイズ内容を記述したYAMLファイルを用意し、distrobuilderを実行するだけです。

インストールはUbuntuならsnapパッケージを使うのが一番確実でしょう。

$ sudo snap install distrobuilder --classic

ちなみに8月9日に最新の1.3がリリースされています。snap版は8月10日時点で1.3はまだcandidateチャンネルにしか存在しませんが、まもなくstableチャンネルに移行する予定です。

Ubuntu以外ならGo言語の環境を整えた上で、go getするのが簡単です。

$ go get -v -x github.com/lxc/distrobuilder/distrobuilder

distrobuilder自体はシンプルなコマンドです。

$ distrobuilder
System container image builder for LXC and LXD

Usage:
  distrobuilder [command]

Available Commands:
  build-dir      Build plain rootfs
  build-lxc      Build LXC image from scratch
  build-lxd      Build LXD image from scratch
  help           Help about any command
  pack-lxc       Create LXC image from existing rootfs
  pack-lxd       Create LXD image from existing rootfs
  repack-windows Repack Windows ISO with drivers included

Flags:
      --cache-dir   Cache directory
      --cleanup     Clean up cache directory (default true)
      --debug       Enable debug output
  -h, --help        help for distrobuilder
  -o, --options     Override options (list of key=value)
  -t, --timeout     Timeout in seconds
      --version     Print version number

Use "distrobuilder [command] --help" for more information about a command.

helpを除くと、主に6個のサブコマンドから構築されています。これらはいずれもdistrobuilder help サブコマンドで詳細なオプションを確認できます。

  • build-dir:指定したディレクトリにルートファイルシステムを構築します
  • build-lxc:LXC向けのイメージデータを構築します
  • build-lxd:LXD向けのイメージデータを構築します
  • pack-lxcbuild-dirで構築したルートファイルシステムをベースにLXC向けのイメージデータを構築します
  • pack-lxdbuild-dirで構築したルートファイルシステムをベースにLXD向けのイメージデータを構築します
  • repack-windows:WindowsのダウンロードISOイメージを、VM版のLXDのインストールイメージに作り変えます

なお、たとえばbuild-dirの実行にはbtrfs-progsパッケージが必要になるなど、サブコマンドによっては追加でパッケージが必要になることもあります。

主に使うのはLXDイメージを作成できるbuild-lxdでしょう。LXDイメージはそのままだとsquashfsとして構築します。また--vmオプションを付けると仮想マシンで利用可能なqcow2フォーマットで構築します。

「LXC/LXD」向けと言う場合、単なるルートファイルシステムのアーカイブだけでなく、メタデータも必要です。これらによりLXCならlxc-createでインスタンスを生成する際のベースイメージとして利用できますし、LXDならlxc imageでローカルのイメージリポジトリにインポートできます。

シンプルなUbuntuイメージを作る

まずは単にUbuntuのLXDイメージを構築してみましょう。イメージの元となる情報は、すべてYAMLファイルに記述します。distrobuilderのリポジトリにはサンプルのYAMLファイルがありますので、それをそのまま使いましょう。

$ mkdir -p ~/temp/images/ubuntu && cd $_
$ wget https://raw.githubusercontent.com/lxc/distrobuilder/master/doc/examples/ubuntu.yaml

distrobuilderの実行には管理者権限が必要です。

$ sudo distrobuilder build-lxd ubuntu.yaml

上記の設定ではdebootstrapを使っています。流れとしては「debootstrapでベースイメージを作り、YAMLに従ってルートファイルシステムをカスタマイズし、mksquashfsでアーカイブする」といった形になります。VMイメージなら最後はsqhuashfsではなくqcow2イメージファイルとなります。

構築には環境によって数分から十数分かかります。実際に作られるのは次のようなファイルです.

$ ls -sh1
合計 103M
4.0K lxd.tar.xz
103M rootfs.squashfs
8.0K ubuntu.yaml

$ tar tvf lxd.tar.xz
-rw-r--r-- root/root       481 2021-08-07 20:42 metadata.yaml
drwxr-xr-x root/root         0 2021-08-07 20:42 templates/
-rw-r--r-- root/root       140 2021-08-07 20:42 templates/hosts.tpl
-rw-r--r-- root/root        21 2021-08-07 20:42 templates/hostname.tpl

rootfs.squashfsがルートファイルシステムそのものです。それに対してlxd.tar.xzは、root.squashfsをLXDで利用するためのメタデータの集まりとなっています。

実際にLXDのイメージサービスにインポートしてみましょう。--aliasオプションで適当な名前を付けておきます。

$ lxc image import lxd.tar.xz rootfs.squashfs --alias ubuntu1st
Image imported with fingerprint: 99032636054e0162c217e9cb9515b9413b2d0fa32fb32e4e59394fafe36baf42

$ lxc image list ubuntu1st
+-----------+--------------+--------+--------------+--------------+-----------+----------+------------------------------+
|   ALIAS   | FINGERPRINT  | PUBLIC | DESCRIPTION  | ARCHITECTURE |   TYPE    |   SIZE   |         UPLOAD DATE          |
+-----------+--------------+--------+--------------+--------------+-----------+----------+------------------------------+
| ubuntu1st | 99032636054e | no     | Ubuntu focal | x86_64       | CONTAINER | 102.88MB | Aug 7, 2021 at 12:28pm (UTC) |
+-----------+--------------+--------+--------------+--------------+-----------+----------+------------------------------+

無事にイメージを取り込めました。image infoコマンドでその内容を確認できます。

$ lxc image info ubuntu1st
Fingerprint: 99032636054e0162c217e9cb9515b9413b2d0fa32fb32e4e59394fafe36baf42
Size: 102.88MB
Architecture: x86_64
Type: container
Public: no
Timestamps:
    Created: 2021/08/07 11:42 UTC
    Uploaded: 2021/08/07 12:28 UTC
    Expires: 2021/09/06 11:42 UTC
    Last used: never
Properties:
    serial: 20210807_1136
    variant: default
    architecture: amd64
    description: Ubuntu focal
    name: ubuntu-disco-x86_64
    os: ubuntu
    release: focal
Aliases:
    - ubuntu1st
Cached: no
Auto update: disabled
Profiles:
    - default

このイメージを使って新しいインスタンスを作るには次のように実行します。

$ lxc launch ubuntu1st test

これだけです。簡単でしたね。

フォーマットの解説

ここまでの説明で薄々気づいたかと思いますが、distrobuilderのキモとなるのはYAMLファイルの内容です。そこで先ほど利用したubuntu.yamlを元に、具体的なフォーマットについて説明していきましょう。

image:メタデータ情報

imageにはイメージのメタデータを記録します。

image:
  name: ubuntu-disco-x86_64
  distribution: ubuntu
  release: focal
  description: |-
    Ubuntu {{ image.release }}
  architecture: x86_64

必須なのはdistributionだけです。原則として任意の文字列を指定可能ですが、値によってはYAML上の別のデータから参照されることもあります。ちなみにarchitectureは未設定だとビルドホストのアーキテクチャーが使われます。

また、上記以外にvariantというフィールドでイメージの派生版を作成し、YAMLの中で派生版だけ特殊設定を行うといった使い方も可能です。

source:ベースシステムの構築方法

sourceにはベースシステムの構築方法とイメージファイルの取得方法を記載します。

source:
  downloader: debootstrap
  same_as: gutsy
  url: http://archive.ubuntu.com/ubuntu
  keyserver: keyserver.ubuntu.com
  keys:
  - 0x790BC7277767219C42C86F933B4FE6ACC0B21F32
  - 0xf6ecb3762474eda9d21b7022871920d1991bc93c

downloaderでベースイメージの取得方法を指定し、それ以外の設定はdownloaderの値に依存します。

上記の例だとdebootstrapコマンドをベースイメージの取得・構築に使います。same_asは/usr/share/debootstrap/scripts以下のどのファイルをビルドスクリプトとして利用するかを指定し、urlがミラーサーバーの指定、keyserverとkeysがリポジトリの鍵として使われます。

実はdebootstrapだけが特殊ケースで、他はHTTP/HTTPSからイメージを取得することになります。downloaderでサポートしているフォーマットは主に以下のような値が存在します。

  • almalinux-http
  • alpinelinux-http
  • alt-http
  • apertis-http
  • archlinux-http
  • busybox
  • centos-http
  • debootstrap
  • docker-http
  • fedora-http
  • funtoo-http
  • gentoo-http
  • opensuse-http
  • openwrt-http
  • oraclelinux-http
  • plamolinux-http
  • rockylinux-http
  • rootfs-http
  • sabayon-http
  • ubuntu-http
  • voidlinux-http

おおよそLXDのイメージサーバーでインストール可能なLinuxディストリビューションは、downloaderとurlとkeysを正しく設定することで構築可能です。具体的な記述方法はLXC/LXDのCIで使われている設定ファイルが参考になるでしょう。

busyboxはBusyBoxのソースコードをダウンロード・ビルドしてイメージを構築します。できるだけ小さい環境がほしいなら、busyboxを使うと良いかもしれません。

ちなみにdocker-httpの場合は、urlにdocker pullで渡す名前をかけば大丈夫です。独自のレジストリを利用したいなら、環境変数DOCKER_REGISTRY_BASEを設定しておいてください。ちなみにこれはSabayon Linuxのために導入された仕組みです。普通のDockerイメージをLXDに対応させるには、/sbin/initを適切に設定したり[1]⁠、ネットワーク周りの設定をなんとかしたりと、いろいろな対応が必要になります。手間の割に得られるものは少ないため、よっぽど特殊な嗜好を持っている人以外はおすすめしません。

targets: 特定のターゲット向けの細かい設定

targetsでは、LXDやLXCなどイメージをデプロイするターゲット用の設定を記述します。

targets:
  lxc:
    create-message: |-
      You just created an {{ image.description }} container.

      To enable SSH, run: apt install openssh-server
      No default root or user password are set by LXC.
    config:
    - type: all
      before: 5
      content: |-
        lxc.include = LXC_TEMPLATE_CONFIG/ubuntu.common.conf

    - type: user
      before: 5
      content: |-
        lxc.include = LXC_TEMPLATE_CONFIG/ubuntu.userns.conf

    - type: all
      after: 4
      content: |-
        lxc.include = LXC_TEMPLATE_CONFIG/common.conf

    - type: user
      after: 4
      content: |-
        lxc.include = LXC_TEMPLATE_CONFIG/userns.conf

    - type: all
      content: |-
        lxc.arch = {{ image.architecture_personality }}

ubuntu.yamlの例だとLXCの設定しか記述していませんね。create-messageではイメージ構築時に表示するメッセージを、configではコンテナの設定を記述しています。

LXDの場合は、仮想マシンインスタンスを作るときに利用します。バージョン1.3の時点では、sizeでファイルシステムのサイズを、filesystemでファイルシステムをext4もしくはbtrfsのいずれにするかを選べるだけです。

files: ルートファイルシステムのカスタマイズ

filesでは、downloaderによって構築したベースファイルシステム上の各種ファイルをカスタマイズできます。ここの項目はgeneratorsfiltersの2種類のコンポーネントで形成されています。基本的にfilesの処理が行われるのは最後にルートファイルシステムをイメージとしてアーカイブする直前です。

ubuntu.yamlのfilesは長いので一部だけ抜粋して表示します。

files:
- path: /etc/hostname
  generator: hostname

- path: /etc/resolvconf/resolv.conf.d/original
  generator: remove

- path: /etc/user/profile
  generator: copy
  source: /etc/profile

- path: /var/lib/dbus/machine-id
  generator: remove

- name: ext4
  generator: fstab
  types:
  - vm

generatorにはhostnameやhosts、fstab、lxd-agent、systemdといった特定のファイル専用のタイプと、dump/copy/removeのような汎用的なタイプの2種類にわかれます。

このうち専用のものは主にコンテナの中で特殊扱いされるもの、もしくはlxd.tar.gzにテンプレートとして保存され、インスタンス生成時に自動生成されるべきものが大半です。基本的に公式イメージの書式に合わせておいたほうが無難でしょう。

removeはイメージ作成時に自動生成されたファイルを確実に削除しておきたい場合に便利です。copyはホストから特定のファイルをイメージ内部にコピーしたい場合に使います。またmode/uid/gidの指定も可能です。

dumpは特定の内容をファイルに書き出したい場合に利用します。

- path: /etc/machine-id
  generator: dump

- path: /etc/netplan/10-lxc.yaml
  generator: dump
  content: |-
    network:
      version: 2
      ethernets:
        eth0:
          dhcp4: true
          dhcp-identifier: mac
  releases:
  - bionic
  - eoan
  - focal
  - groovy
  types:
  - container
  variants:
  - default

/etc/machine-idのようにcontent未指定の場合は、ただの空ファイルとなります。

releases/types/variantsはこの設定を特定のイメージにのみ適用したい場合のフィルタリング機能です。上記の例だと、Ubuntuの18.04(bionic)以上にのみ適用する想定であり、さらにvariants未指定のコンテナイメージでのみ使われます。コンテナのみになっているのはVM版だとNICの名前がeth0ではなくenp5s0だからです。

これらはimages以下で設定された値が参照されます。またdistrobuilder実行時の末尾に-o image.variant=cloudのようにオプションを使って値を追加することも可能です。

このフィルタリング機能はfilesだけでなく、後述のpackagesやrepositories、actionsでも利用します。

- name: network-config
  generator: cloud-init
  variants:
  - cloud

- name: user-data
  generator: cloud-init
  variants:
  - cloud

- name: vendor-data
  generator: cloud-init
  variants:
  - cloud

これらはcloud-init用のテンプレートファイルを作成します。実際に作成されるのは、lxd.tar.gzの中になります。

packages: パッケージのインストールと削除

packagesでは、追加でインストールするパッケージや削除しておくパッケージを指定します。こちらもフィルタリング機能を用いて、特定のアーキテクチャー向けイメージだけ変えるといった対応が可能です。

packages:
  manager: apt
  update: true
  cleanup: true
  sets:
  - packages:
    - fuse
    - language-pack-en
    - openssh-client
    - vim
    action: install

  - packages:
    - cloud-init
    action: install
    variants:
    - cloud

  - packages:
    - os-prober
    action: remove
    types:
    - vm

またmanagerによってdnfやyum、pacmanといったパッケージ管理システムを選択できます。またcustom-managerを使うと、任意のコマンド列を「パッケージ管理システム」として指定できます。

上記の例だとaptを使って、fuseなどをインストールし、cloud設定があるならcloud-initをインストール、VMイメージならos-proberを削除しています。

  repositories:
  - name: sources.list
    url: |-
      deb http://archive.ubuntu.com/ubuntu {{ image.release }} main restricted universe multiverse
      deb http://archive.ubuntu.com/ubuntu {{ image.release }}-updates main restricted universe multiverse
      deb http://security.ubuntu.com/ubuntu {{ image.release }}-security main restricted universe multiverse
    architectures:
    - amd64
    - i386

さらにrepositoriesでパッケージリポジトリを設定できます。Ubuntuならsources.listに記述する内容ですね。ここで{{ image.release }}のようにメタデータフィールドの値を参照しています。このためubuntu.yamlのimage.releaseはUbuntuのリリースコード名が記述されることを期待しているわけです。

actions: イメージビルドの各処理の途中で実施するコマンド設定

actionsではイメージビルドの処理の途中でフック処理として任意のコマンドを実行できるようになっています。

actions:
- trigger: post-update
  action: |-
    #!/bin/sh
    set -eux

    # Create the ubuntu user account
    getent group sudo >/dev/null 2>&1 || groupadd --system sudo
    useradd --create-home -s /bin/bash -G sudo -U ubuntu
  variants:
  - default

- trigger: post-packages
  action: |-
    #!/bin/sh
    set -eux

    # Make sure the locale is built and functional
    locale-gen en_US.UTF-8
    update-locale LANG=en_US.UTF-8

    # Cleanup underlying /run
    mount -o bind / /mnt
    rm -rf /mnt/run/*
    umount /mnt

    # Cleanup temporary shadow paths
    rm /etc/*-

triggerにフックするタイミングを記述し、actionに実際に実行するシェルスクリプトを記述します。

バージョン1.3でサポートしているフックは次の4種類です。

  • post-unpack:ダウンロード・作成したベースイメージを展開したあとに実施
  • post-updatepackages.updatetrueのときのみ、パッケージ情報を更新したあとに実施
  • post-packagespackagesで記述したパッケージ関連の処理を実施したあと
  • post-filesfilesで記述されたファイルの変更処理を実施したあと

実際にフックが叩かれるのも上記の順番になります。

mappings: アーキテクチャー名のマッピング

mappingsにはCPUアーキテクチャー名のマッピングテーブルを記述します。CPUアーキテクチャー名はディストリビューションによって「x86_64」だったり「amd64」だったり「generic_64」だったりと、微妙に異なります。そこでこのマッピングテーブルで、そのあたりの差異を吸収してしまおうというわけです。

mappings:
  architecture_map: debian

architecture_mapの後ろにディストリビューション名を書けば、そのディストリビューションに合わせた設定を行います。

また、architecturesによってアーキテクチャーごとに任意のマッピングをハッシュテーブルとしても記述できます。なお記述はdistrobuider側の名前: ディストリビューション側の名前となります。distrobuilder側の名前は、実質LXDのコード上の名前と一致しています。

  • i686:Intel x86 32bit/IA-32
  • x86_64:Intel x86 64bit/AMD64/Intel 64
  • armv7l:ARMv7 32bit
  • aarch64:AAarch64 64bit
  • ppc:PowerPC 32bit
  • ppc64:PowerPC 64bit Big Endian
  • ppc64le:PowerPC 64bt Little Endian
  • s390x:S/390 Big Endian
  • mips:MIPS 32bit
  • mips64:MIPS 64bit
  • riscv32:RISC-V 32bit
  • riscv64:RISC-V 64bit

実際のところ個々のディストリビューションのサポートアーキテクチャーが上記に完全に一致するわけではありませんが、おおよそ一致しているものがひとつのカテゴリにまとめられています。

ここまでいろいろ述べましたが、そこまで難しい書式ではないため、ubuntu.yamlやすべての利用可能なオプションを網羅したscheme.yamlなどで使用例を見ながら作業すれば、自分好みのイメージが作れることでしょう。

おすすめ記事

記事・ニュース一覧