Ubuntu Weekly Recipe

第611回Packerでmultipass用の仮想マシンイメージを作る

第590回で紹介したマルチプラットフォームな仮想マシン管理ツールである「Multipass」は、原則としてUbuntuサーバーのイメージを起動するように作られています。しかしながら実は任意の仮想マシンイメージの指定も可能です。そこでPackerで仮想マシンイメージを作成し、Multipassで管理する方法を紹介しましょう。

MultipassとPacker

第590回「Windows/macOS/Linuxで使える仮想マシン管理ツール『multipass⁠⁠」で紹介したように、MultipassはWindowsやmacOS、Linuxでも使える仮想マシンを管理するツールです。DockerやLXDのようにコマンドラインから気軽に仮想マシンを起動・終了するような使い方を想定しています。

multipassは特に指定しなければサーバー版のUbuntuをベースイメージとして使います。もし自動的にカスタマイズしたいなら、cloud-initを使うか、インスタンス作成後にプロビジョニングツールを使うことを想定しています。しかしながらあらかじめ構築済みのイメージやUbuntu以外の仮想マシンを立ち上げたいこともあるはずです。

そこで今回は自分で仮想マシンイメージを作る方法とそれをmultipassで利用する方法を紹介します。Ubuntu版multipassの標準のバックエンドはQEMUなので、QEMUで任意のOSのインストーラーを使ってディスクイメージを作成し、それを利用しても良いのですが若干面倒です。よってマシンイメージの作成自動化ツールとして広く使われているPackerを使うことにしましょう。PackerであればWindowsのHyper-V用イメージにも対応しています。

PackerはUbuntuならパッケージ管理ツールからインストール可能です。しかしながら18.04だと1.0.4、現在開発中の20.04ですら1.3.4と古いバージョンしかインストールできません[1]⁠。DebianでもFTBFSな状態なので誰かがメンテナンスを引き継がなければ将来的にパッケージそのものがなくなってしまう可能性があります[2]⁠。

よって素直にPackerのダウンロードページからバイナリをインストールしましょう。PackerはGo言語で作られたバイナリなので、ホスト側に必要なのはモダンなlibcだけです。おそらくUbuntuであれば、サポートしているどのバージョン・アーキテクチャーでも特に問題はないでしょう。該当ページの「Linux」から、使っているマシンのCPUアーキテクチャーに合わせてアーカイブファイルをダウンロードしてください。今回はAMD64版を使うことにします。

$ wget https://releases.hashicorp.com/packer/1.5.4/packer_1.5.4_linux_amd64.zip
$ unzip packer_1.5.4_linux_amd64.zip
$ sudo cp packer /usr/local/bin/

バイナリひとつ配置すればいいので簡単ですね。また、Packer自身がQEMUを使うように設定するため、QEMUパッケージをインストールしておく必要があります。multipassパッケージ同梱のQEMUを使ってもいいのですが、いろいろ調整が必要なので、パッケージ版をインストールしておきましょう。

$ sudo apt install qemu-system-x86 qemu-utils

これでインストールは完了です[3]⁠。

Packer用ファイルとcloud-initデータの作成

PackerはJSONファイルに基づいて、仮想マシンイメージを構築します。builderがISOファイルやルートファイルシステムのアーカイブをダウンロードした上で仮想マシンを起動し、provisionerがインストール後の諸々の設定を行います。builderがOSごとのインストール方法や作成するイメージ形式の違いを吸収し、パッケージのインストールや設定ファイルの作成はprovisionerが担うと思っておけば良いでしょう。正確にはbuilderがOSのインストールを行うわけではありません。しかしながら今回はcloud-initによって、builderによる仮想マシン起動の際にインストールも事実上「自動化」します。

ベースイメージはUbuntu Cloud Imagesのデータを使うことにします。これは各種クラウドサービスで使用するUbuntuのベースイメージを配布しているサイトです。リリースごとに最新のアップデートを適用したイメージが、ほぼデイリーでビルドされるので、ベースイメージとしてはもってこいなのです。

ちなみにこのイメージにはアカウントは設定されていません。ユーザーごとに異なる設定はクラウドサービスごとに合わせたcloud-initで設定する、というスタンスです。

Packerでイメージを構築する際、Packerのprovisionerが仮想マシンインスタンス内部にSSHログインできる必要があります[4]⁠。つまりあらかじめアカウントが必要です。そこでQEMU用のbuilderからcloud-initの設定を行うことにします。これには第561回「ローカルインストール時もcloud-initを活用する」で紹介した知識が利用可能です。

cloud-initをサポートしたイメージは、どこかからcloud-initの設定ファイルを取得しなくてはなりません。一般的なクラウドサービスは、たとえばシリアルコンソール経由などサービスごとに取得方法を用意しており、ユーザーはそれを気にすることなくデータだけを渡せば良いようになっています。それに対してQEMUはもともとcloud-initには対応していないため、cloud-initのプログラム側で考慮する必要があります。それがNoCloudという仕組みです。

NoCloudでは次のいずれかの方法で、cloud-initの設定ファイルを取得可能です。

  • ボリュームラベルが「cidata」「CIDATA」な、vfatもしくはiso9660のストレージ
  • カーネルコマンドラインで指定されたパラメーター
  • SMBIOS上のシリアル番号に記載された文字列

後者のふたつに関しては、cloud-initデータが存在するファイルの位置を指定可能です。つまり任意のURLにcloud-initファイルを配置し、それをダウンロードできるのです。

Packerのbuilderはhttp_directoryというパラメーターを指定することで、インスタンス作成時にそのディレクトリを見せるHTTPサーバーを立ち上げます。よってここにcloud-initファイルを置くことで、仮想マシンイメージ構築時にcloud-initを実行できるようになります。

最後に注意が必要なのは、⁠multipass自身もインスタンス作成時にcloud-initを使用している」点です。つまり作成した仮想マシンイメージとmultipassのcloud-init設定がバッティングしてしまうと、その仮想マシンイメージをmultipassで立ち上げられません。そこでprovisionerで、いくつかの調整を行います[5]⁠。

まとめるとmultipass用のイメージをPackerで構築するために、次のファイルを準備する必要があります。

  • cloud-init用の設定ファイルmeta-datauser-data
  • Packer用の設定ファイルubuntu.json

それでは順番に作っていきましょう。

cloud-init用の設定ファイルの作成

cloud-init用の設定ファイルは比較的単純です。要するにPackerのcommunicatorがインスタンス内部にログインできるようにしておけば良いのですから。

$ mkdir cloud-data
$ touch cloud-data/meta-data
$ openssl passwd -6 ubuntupassword
$6$tXF3sTmGEMKhsanM$LyJQLz8lfqQITkNXTiVyAErUXpGkplHX3ftXZCB5/shRGqO7EuOnkRQ5cVjHNmqgAYzMwyOTPkDbZbuj26H6W0
$ cat <<'EOF' > cloud-data/user-data
#cloud-config
ssh_pwauth: true
apt_mirror: http://jp.archive.ubuntu.com/ubuntu/
users:
  - name: ubuntu
    sudo: ALL=(ALL) NOPASSWD:ALL
    passwd: $6$tXF3sTmGEMKhsanM$LyJQLz8lfqQITkNXTiVyAErUXpGkplHX3ftXZCB5/shRGqO7EuOnkRQ5cVjHNmqgAYzMwyOTPkDbZbuj26H6W0
    lock_passwd: false
EOF

cloud-data/meta-dataはcloud-initで使うインスタンスIDやホストネームを指定するために使うファイルですが、今回は何も設定しません。そこで空ファイルのみを作っています。ちなみにインスタンスIDはPackerのほうで設定しています。

cloud-init本体の設定のポイントは次のとおりです。

  • ssh_pwauthの設定でパスワードログインを許可しておく
  • sudoの設定でパスワード無しのsudoを許可しておく
  • passwdの設定でパスワードを設定しておく
  • lock_passwdの設定でパスワードログインを許可しておく

PackerのSSH communicatorはSSHの公開鍵ログインも利用可能です。複数のユーザーで運用するPackerファイルを作るなら公開鍵の設定にしておくべきでしょう。しかしながら今回はPackerファイルにパスワードを直接書いてパスワードログインすることにします。Packerファイルには生のパスワードを直接記述しますが、cloud-initのほうはハッシュ化したものを記述します。これはcloud-initのデータが作成後のルートファイルシステムの中に残るためです。

また上記では、Packerのprovisionerが設定を行う際にsudoコマンドをパスワードなしで実行できるような設定も行っています。

今回はPacker用のアカウントを「ubuntu」にしましたが、これはmultipassが自動で作るアカウントと同じ名前です。Packer用のアカウントということなら、別の名前にしても良いかもしれません。

Packer用の設定ファイルの作成

次にPacker用の設定ファイルを作成しましょう。

$ cat <<'EOF' > ubuntu.json
{
  "builders": [
    {
      "disk_discard": "unmap",
      "disk_image": true,
      "disk_interface": "virtio-scsi",
      "disk_size": "5120M",
      "headless": true,
      "http_directory": "cloud-data",
      "iso_checksum_type": "sha256",
      "iso_checksum_url": "http://cloud-images.ubuntu.com/releases/bionic/release/SHA256SUMS",
      "iso_url": "http://cloud-images.ubuntu.com/releases/bionic/release/ubuntu-18.04-server-cloudimg-amd64.img",
      "qemuargs": [
        [
          "-smbios",
          "type=1,serial=ds=nocloud-net;instance-id=packer;seedfrom=http://{{ .HTTPIP }}:{{ .HTTPPort }}/"
        ]
      ],
      "ssh_password": "ubuntupassword",
      "ssh_username": "ubuntu",
      "type": "qemu",
    }
  ],
  "provisioners": [
    {
      "execute_command": "sudo sh -c '{{ .Vars }} {{ .Path }}'",
      "inline": [
        "mv /etc/netplan/50-cloud-init.yaml /root/",
        "mv /etc/sudoers.d/90-cloud-init-users /root/",
        "/usr/bin/truncate --size 0 /etc/machine-id",
        "rm -r /var/lib/cloud /var/lib/dbus/machine-id ",
        "for i in group gshadow passwd shadow subuid subgid; do mv /etc/$i- /etc/$i; done",
        "/bin/sync",
        "/sbin/fstrim -v /"
      ],
      "remote_folder": "/tmp",
      "type": "shell"
    }
  ]
}
EOF

前半はbuildersの設定です。詳細はQEMU用のbuilderを確認してもらうとして、今回重要なポイントは次のとおりです。

  • http_directoryでcloud-initファイルのディレクトリを指定
  • iso_urliso_checksum_XXXでダウンロードするイメージを指定
  • qemuargsでcloud-initのダウンロードもとを指定
  • ssh_passwordssh_usernameでcommunicatorが使うパスワードとアカウントを指定
  • typeでQEMUを利用することを指定

iso_urliso_checksum_XXXUbuntu Cloud Imagesを参考に、使用するリリースなどにあわせて変更してください。もちろん、ローカルのファイルを指定してもかまいません。

qemuargsはPackerがイメージ構築時に利用するQEMUコマンドの引数です。ここにSMBIOSの設定を渡すことで、provisionerが動く前にcloud-initを実行できるようにしています。

{{ .HTTPIP }}{{ .HTTPPort }}は、builderが自動的に展開するIPアドレスとポート番号です。QEMUの場合、IPアドレスはユーザーネットワークのゲートウェイアドレスが割り当てられ、ポート番号はランダムな値が使われます。もちろん直接アドレスとポート番号を指定すれば、PackerのHTTPサーバー以外も利用可能です。

後半はprovisionerが実行するコマンドのリストです。Shell Provisionerによって、inlineでリストアップされたコマンドを順番にsudoで実行しています。

主にbuilderが実行したcloud-initで生成されたファイルのクリーンナップ処理です。これによりmultipassによるcloud-initがキレイに実行されることになります。

/etc/machie-idは初回起動時にsystemd-machine-id-setupで生成される、インストールされたマシン固有のIDを保存するファイルです。空でない場合はマシンIDを生成しません。つまりPackerで構築時にイメージを起動した時点でマシンIDが生成されるため、構築完了後にIDを削除しておかないと、同じイメージファイルを使うすべてのインスタンスが同じマシンIDを持ってしまうのです。空のファイルとして存在する必要があるため、truncateコマンドで中身を消しています。

/etc/group他のmvコマンドの実行は「cloud-init前の設定に戻す」処理です。cloud-initの内部で実行されるuseraddなどのコマンドは、設定ファイルを書き換える際に-を付けたオリジナルファイルのバックアップを残すようになっています。このファイルをもとに戻すことで、cloud-init時に作成したユーザーを「なかったこと」にできるわけです。

今回はPackerとmultipassで同じユーザーではありますが、multipassがユーザーを作れるように元に戻しています。またPacker用にユーザーを作るにしても、それを構築完了後に削除したいなら最後にこのようなコマンドを実行すると良いでしょう。

fstrimはファイルの削除等によって消された領域がイメージファイルから確実に削除になるようにするツールです。builderのほうでdisk_discardunmapに設定しているため、fstrimがイメージサイズの削減にも繋がります。今回のように削除しているファイルがそうでもない場合はあまり恩恵はありませんが、シンプロビジョニングなイメージを作る際の手順として覚えておきましょう。

イメージのビルドとmultipassでの利用

作成したPackerファイルはvalidateサブコマンドでチェックできます。

$ packer validate ubuntu.json
Template validated successfully.

またfixサブコマンドを使うと、JSONファイルそのものを整形できます。他人と共有するPackerファイルは、設定ファイルの書き方や位置を揃えるためにもfixコマンドを積極的に活用していきましょう。

$ packer fix ubuntu.json > new.json

イメージの作成はbuildサブコマンドです。

$ packer build ubuntu.json
qemu: output will be in this color.

==> qemu: Retrieving ISO
==> qemu: Trying http://cloud-images.ubuntu.com/releases/bionic/release/ubuntu-18.04-server-cloudimg-amd64.img
==> qemu: Trying http://cloud-images.ubuntu.com/releases/bionic/release/ubuntu-18.04-server-cloudimg-amd64.img?checksum=sha256%3A3c3a67a142572e1f0e524789acefd465751224729cff3a112a7f141ee512e756
==> qemu: Copying hard drive...
==> qemu: Resizing hard drive...
==> qemu: Starting HTTP server on port 8110
==> qemu: Found port for communicator (SSH, WinRM, etc): 3636.
==> qemu: Looking for available port between 5900 and 6000 on 127.0.0.1
==> qemu: Starting VM, booting disk image
    qemu: The VM will be run headless, without a GUI. If you want to
    qemu: view the screen of the VM, connect via VNC without a password to
    qemu: vnc://127.0.0.1:5950
==> qemu: Overriding defaults Qemu arguments with QemuArgs...
==> qemu: Waiting 10s for boot...
==> qemu: Connecting to VM via VNC (127.0.0.1:5950)
==> qemu: Typing the boot command over VNC...
==> qemu: Using ssh communicator to connect: 127.0.0.1
==> qemu: Waiting for SSH to become available...
==> qemu: Connected to SSH!
==> qemu: Provisioning with shell script: /tmp/packer-shell037271588
    qemu: /: 3.7 GiB (3969331200 bytes) trimmed
==> qemu: Halting the virtual machine...
==> qemu: Converting hard drive...
==> qemu: Error getting file lock for conversion; retrying...
Build 'qemu' finished.

==> Builds finished. The artifacts of successful builds are:
--> qemu: VM files in directory: output-qemu

作成したイメージはoutput-qemu/packer-qemuとして保存されます。

$ file output-qemu/packer-qemu
output-qemu/packer-qemu: QEMU QCOW2 Image (v3), 5368709120 bytes

あとはこのファイルをmultipassのインスタンス作成時に指定するだけです。

$ multipass launch file://$PWD/output-qemu/packer-qemu \
  --disk 5G --name packer
Launched: packer

これで任意の「構築済み仮想マシンイメージ」を作れるようになりました。cloud-initをサポートしているOSなら、Ubuntu以外でも同様の方法でイメージの作成が可能です。それはまたの機会に。

おすすめ記事

記事・ニュース一覧