Azure Image Builder と Customazed CLI による簡単 VM イメージ構築


## 概要

この記事は [Azure Advent Calendar 2020][Calendar] の 14 日目の記事です。

Azure の [Azure Image Builder] というサービスと、それを補助するために作った [Customazed CLI] というツールを紹介したいと思います。

**2020/12/19 追記**

[第30回 Tokyo Jazug Night (Online)][JAZUG] というイベントの LT に登壇して Customaze CLI を紹介しました: [Customazed CLI: カスタムVMイメージ作成支援ツール][SlideShare]

こちらでは仮想マシンの CustomScript 拡張によりスクリプトを実行する手順を説明していますので、ぜひ参考にしてください.

[Calendar]: https://qiita.com/advent-calendar/2020/azure
[Azure Image Builder]: https://docs.microsoft.com/ja-jp/azure/virtual-machines/image-builder-overview
[Customazed CLI]: https://github.com/yaegashi/customazed
[Packer]: https://www.packer.io
[Image Template]: https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/image-builder-json
[AIB HOWTO]: https://docs.microsoft.com/ja-jp/azure/virtual-machines/windows/image-builder
[Customazed CLI Releases]: https://github.com/yaegashi/customazed/releases
[Storage Blob Data Reader]: https://docs.microsoft.com/ja-jp/azure/role-based-access-control/built-in-roles#storage-blob-data-reader
[Ansible Provisioner]: https://www.packer.io/docs/provisioners/ansible
[Shared Image Gallery]: https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/imaging#azure-shared-image-gallery-sig
[Managed Image]: https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/imaging#managed-images
[AIB regions]: https://docs.microsoft.com/ja-jp/azure/virtual-machines/image-builder-overview#regions
[UUIDv5]: https://ja.wikipedia.org/wiki/UUID#%E3%83%90%E3%83%BC%E3%82%B8%E3%83%A7%E3%83%B33/5
[JAZUG]: https://jazug.connpass.com/event/197139/
[SlideShare]: https://www.slideshare.net/yaegashi/customazed-cli-vm

## Azure Image Builder

[Azure Image Builder] (AIB) は、その名の通り Azure VM の元となるイメージをビルドするためのサービスです。現時点ではプレビュー段階であり、まだ正式サービスではありません。

通常、Azure の VM イメージのカスタマイズは次のような手順で行います。

1. 作業用 Azure VM の作成
2. 必要なソフトのインストールとカスタマイズ
3. sysprep (Windows) または waagent (Linux) によるデプロビジョニング
4. シャットダウン
5. イメージのキャプチャ
6. 作業用 Azure VM の削除

全体で数十分から数時間かかる工程であり、拘束時間が長くミスを誘発しやすい作業といえますが、AIB を使えばこれらを自動化できて開発・テスト・リリースの反復が楽になります。

AIB は [HashiCorp Packer][Packer] を内部で使用しています。ただし Packer の設定ファイルや機能すべてが AIB でも使えるわけではなく、[イメージテンプレート][Image Template]という AIB 専用のリソースでサポートされていることだけが可能です。例えば [Ansible Provisioner] のような Packer の特徴的な機能は使えません。

高機能な Packer を自分で実行せずに AIB を使うのはなぜでしょうか？それはやはり AIB がマネージドサービスであることが大きいといえます。Packer をチームで反復利用する体制を整えることは容易ではなく、長時間の Packer 実行環境の維持、ビルド失敗時のリソース後始末、ログやイメージの保全といった考慮が必要になりますが、AIB を使えばそれらを Azure にまかせることができます。

## Customazed CLI

ということで適材適所で使っていきたい AIB ですが、実は AIB はセットアップが非常に面倒なサービスです。

[ハウツーのドキュメント][AIB HOWTO]を見るとわかりますが、今のところ Azure Portal での AIB の操作はできず PowerShell か Azure CLI、REST API を使う必要があります。

また Packer では手元のプライベートなファイルを VM にアップロードすることが簡単にできますが、AIB で同じことをするには次のような準備が必要になります。

1. ストレージアカウントと Blob コンテナを用意する
2. [ユーザー割り当てマネージド ID を作る](https://docs.microsoft.com/ja-jp/azure/active-directory/managed-identities-azure-resources/how-to-manage-ua-identity-portal)
3. &#x31;. の IAM を編集して 2. の ID から読み取り可能にする ([Storage Blob Data Reader])
4. &#x31;. にファイルをアップロードする
5. イメージテンプレートの JSON に 2. のリソース ID や 4. のダウンロード URL を埋め込む

あまりに面倒なのでやはり Packer を直接使うほうがよいのではと思ってしまうのですが、それでも AIB のマネージドサービスという利点は捨てがたいので、上記のような定型の操作を自動的にやってくれる CLI ツールを作りました。それが今回紹介する [Customazed CLI] です。

この CLI は Go 言語で書かれていますので、 Windows/Mac/Linux 用のビルドがあり、customazed コマンドのバイナリファイルひとつを [Customazed CLI Releases] からダウンロードしてすぐに使い始めることができます。

## チュートリアル

チュートリアルとして Customazed CLI と Azure Image Builder で VM イメージをビルドする手順をお見せします。

このチュートリアルを実行するには Azure サブスクリプションが必要です。 AIB が使用するストレージアカウントや VM、リージョン間イメージ転送の帯域に課金されますので注意してください。通常の VM のイメージビルドにかかる時間は数十分、課金は数十円～数百円程度です。

ビルドの結果は[マネージドイメージ][Managed Image]のリソースおよび[共有イメージギャラリー][Shared Image Gallery]のバージョンとして得られます。

### 準備

まず Customazed CLI のリポジトリをクローンして examples/builder_linux ディレクトリに移動してください。

```console
$ git clone https://github.com/yaegashi/customazed
$ cd customazed/examples/builder_linux
```

そこで [customazed.json](https://github.com/yaegashi/customazed/blob/master/examples/builder_linux/customazed.json) の `variables` を環境に合わせて修正してください。

```json:customazed.json(抜粋)
  "variables": {
    "tenantId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "subscriptionId": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
    "location": "westus2",
    "resourceGroup": "CustomazedRG",
    "storageAccountName": "customazed12345",
    "publisher": "CustomazedPublisher",
    "offer": "CustomazedUbuntuServer",
    "sku": "18.04-Customazed",
    "osType": "linux",
    "imageName": "{{var `publisher`}}_{{var `offer`}}_{{var `sku`}}",
    "serial": "1"
  },
```

- `storageAccountName` はまだ使われていない適当なストレージアカウント名を指定してください。
- `location` は [AIB が利用可能なリージョン][AIB regions]を指定する必要があります。残念ながら `japaneast` はまだ使えません。
  - 出力先のイメージやギャラリーには AIB とは異なるリージョンが指定可能ですが、イメージのコピーに追加の時間と費用がかかるので、最初はすべて `westus2` でテストすることをお勧めします。

[customazed_builder.json](https://github.com/yaegashi/customazed/blob/master/examples/builder_linux/customazed_builder.json) には[イメージテンプレート設定ファイル][Image Template]の一部を記述します。 

```json:customazed_builder.json
{
  "properties": {
    "buildTimeoutInMinutes": 60,
    "vmProfile": {
      "vmSize": "Standard_D2a_v4"
    },
    "source": {
      "type": "PlatformImage",
      "publisher": "Canonical",
      "offer": "UbuntuServer",
      "sku": "18.04-LTS",
      "version": "latest"
    },
    "customize": [
      {
        "type": "Shell",
        "name": "Run setup script",
        "scriptUri": "{{upload `scripts/setup.sh`}}"
      }
    ]
  }
}
```

通常は上記のように `properties` の `vmProfile` `source` `customize` だけを記述します。残りの設定は customazed.json を元に自動的に生成されます。

注目していただきたいのは ``{{upload `script/setup.sh`}}`` です。イメージテンプレートの作成時、ローカルのファイル script/setup.sh が自動的にストレージアカウントにアップロードされ、そのダウンロード URL がこの場所に埋め込まれます。

その [scripts/setup.sh](https://github.com/yaegashi/customazed/blob/master/examples/builder_linux/scripts/setup.sh) は AIB の [Shell カスタマイザ](https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/image-builder-json#shell-customizer) により VM で実行されるセットアップスクリプトです。必要なパッケージのインストールなど VM 内のカスタマイズを行います。

```bash:setup.sh
#!/bin/bash

set -ex

apt-get update
apt-get install -y build-essential
apt-get dist-upgrade -y
apt-get autoremove -y --purge
apt-get clean
```

このスクリプトをカスタマイズする場合は、途中でユーザーの入力待ちで止まってしまわないように注意してください。その場合は `buildTimeoutInMinutes` で指定した時間が経過した後に失敗します。

### customazed login

最初に `customazed login` を実行して認証を行います。 Web ブラウザで https://microsoft.com/devicelogin を開き、コードを入力してサインインしてください。

```console
$ customazed login
2020/12/13 06:55:45 Loading config file customazed.json
2020/12/13 06:55:45 To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code HJ93Q7XP5 to authenticate.
2020/12/13 06:55:56 Saving auth-dev token in .customazed/auth_dev.json
```

認証トークンはカレントディレクトリの `~/.customazed/auth_dev.json` ファイルに保存されます。

### customazed feature register

`customazed feature register` で AIB の利用に必要な Azure Resource Manager の機能とプロバイダを登録します。
この操作はサブスクリプションごとに一度だけ実行してください。

```console
$ customazed feature register
2020/12/13 15:53:57 Loading config file customazed.json
2020/12/13 15:53:57 Loading auth-dev token in .customazed/auth_dev.json
2020/12/13 15:53:57 Feature: registering Microsoft.VirtualMachineImages/VirtualMachineTemplatePreview
2020/12/13 15:54:02 Provider: registering Microsoft.VirtualMachineImages
2020/12/13 15:54:03 Provider: registering Microsoft.KeyVault
2020/12/13 15:54:04 Provider: registering Microsoft.Compute
2020/12/13 15:54:06 Provider: registering Microsoft.Storage
```

登録にはしばらく時間がかかることがあります。登録状況は `customazed feature show` で確認できます。

```console
$ customazed feature show
2020/12/13 15:54:49 Loading config file customazed.json
2020/12/13 15:54:49 Loading auth-dev token in .customazed/auth_dev.json
2020/12/13 15:54:49 Feature: Microsoft.VirtualMachineImages/VirtualMachineTemplatePreview: Registering
2020/12/13 15:54:49 Provider: Microsoft.VirtualMachineImages: Registered
2020/12/13 15:54:49 Provider: Microsoft.KeyVault: Registering
2020/12/13 15:54:49 Provider: Microsoft.Compute: Registered
2020/12/13 15:54:49 Provider: Microsoft.Storage: Registered
```

ひとつの Feature と 4 つの Provider がすべて `Registered` になるまで待ってください。

### customazed setup

`customazed setup` コマンドで Azure Image Builder に必要なリソースを作成し、アクセス権限を設定します。このコマンドは customazed.json ファイルの内容を変更したら再度実行してください。

```console
$ customazed setup
2020/12/14 08:37:47 Loading config file customazed.json
2020/12/14 08:37:47 Loading auth-dev token in .customazed/auth_dev.json
2020/12/14 08:37:47 Loading auth-dev token in .customazed/auth_dev.json
2020/12/14 08:37:48 Storage: creating resource group: CustomazedRG
2020/12/14 08:37:50 Storage: creating storage account: customazed12345
2020/12/14 08:37:53 Storage: creating blob container: customazed
2020/12/14 08:37:54 Identity: creating resource group: CustomazedRG
2020/12/14 08:37:55 Identity: creating user assigned identity: CustomazedIdentity
2020/12/14 08:37:55 Machine: missing configuration
2020/12/14 08:37:55 Image: creating resource group: CustomazedRG
2020/12/14 08:37:56 Gallery: creating resource group: CustomazedRG
2020/12/14 08:37:56 Gallery: creating gallery: CustomazedPublisher
2020/12/14 08:37:57 Gallery: creating gallery image: CustomazedPublisher_CustomazedUbuntuServer_18.04-Customazed
2020/12/14 08:38:00 Builder: creating resource group: CustomazedRG
2020/12/14 08:38:00 Machine: missing configuration
2020/12/14 08:38:00 Role: assign role to user for blob container
2020/12/14 08:38:01 Role: assign role to identity for blob container
2020/12/14 08:38:02 Role: creating custom role for identity
2020/12/14 08:38:04 Role: assign role to identity for image
2020/12/14 08:38:04 Role: assign role to identity for gallery
```

### customazed builder create

`customazed builder create` コマンドで customazed_builder.json ファイルからイメージテンプレートのリソースを作成します。

リソースを作成する前にイメージテンプレートの内容を表示して一時停止するので、内容を確認してから ENTER を押して進めてください。イメージテンプレートの詳細は[テンプレートリファレンス][Image Template]を参照してください。

```console
$ customazed builder create
2020/12/14 08:38:42 Loading config file customazed.json
2020/12/14 08:38:42 Loading auth-dev token in .customazed/auth_dev.json
2020/12/14 08:38:44 Blob: adding scripts/setup.sh
2020/12/14 08:38:44 
{
  "identity": {
    "type": "UserAssigned",
    "userAssignedIdentities": {
      "/subscriptions/58110a2c-a91b-4fdd-b8ea-3c16e75106ac/resourcegroups/CustomazedRG/providers/Microsoft.ManagedIdentity/userAssignedIdentities/CustomazedIdentity": {}
    }
  },
  "location": "westus2",
  "properties": {
    "buildTimeoutInMinutes": 60,
    "customize": [
      {
        "name": "Run setup script",
        "scriptUri": "https://customazed12345.blob.core.windows.net/customazed/1e83069e-f650-527a-a176-d5bb572580bf_1/scripts/setup.sh",
        "type": "Shell"
      }
    ],
    "distribute": [
      {
        "imageId": "/subscriptions/58110a2c-a91b-4fdd-b8ea-3c16e75106ac/resourceGroups/CustomazedRG/providers/Microsoft.Compute/images/CustomazedPublisher_CustomazedUbuntuServer_18.04-Customazed_1",
        "location": "westus2",
        "runOutputName": "ManagedImage",
        "type": "ManagedImage"
      },
      {
        "excludeFromLatest": false,
        "galleryImageId": "/subscriptions/58110a2c-a91b-4fdd-b8ea-3c16e75106ac/resourceGroups/CustomazedRG/providers/Microsoft.Compute/galleries/CustomazedPublisher/images/CustomazedPublisher_CustomazedUbuntuServer_18.04-Customazed",
        "replicationRegions": [],
        "runOutputName": "SharedImage",
        "storageAccountType": "Standard_LRS",
        "type": "SharedImage"
      }
    ],
    "source": {
      "offer": "UbuntuServer",
      "publisher": "Canonical",
      "sku": "18.04-LTS",
      "type": "PlatformImage",
      "version": "latest"
    },
    "vmProfile": {
      "vmSize": "Standard_D2a_v4"
    }
  }
}
2020/12/14 08:38:44 Current builder name: 1e83069e-f650-527a-a176-d5bb572580bf_1
2020/12/14 08:38:44 Files to upload: 1
Press ENTER to proceed: 
2020/12/14 08:38:46 Loading auth-dev token in .customazed/auth_dev.json
2020/12/14 08:38:47 Blob: destination https://customazed12345.blob.core.windows.net/customazed/1e83069e-f650-527a-a176-d5bb572580bf_1
2020/12/14 08:38:47 Blob: uploading scripts/setup.sh
2020/12/14 08:38:48 Creating image template...
```

customaze_builder.json にはなかった `identity` や `properties.distribute` の設定が補完され、また shell カスタマイザの ``{{upload `script/setup.sh`}}`` が、実際のダウンロード URL に置換されていることが確認できます。

なお、同じ名前のイメージテンプレートリソースの作成を試みるとエラーになりますので、再実行する場合は `customazed builder delete` で前回のイメージテンプレートリソースを削除するか、 customized.json の `serial` の数字を増やしてください。

イメージテンプレートリソースの作成に成功すると `IT_{builder.resourceGroup}_{builder.builderName}_{UUID}` という形式のリソースグループが作成されます。これは AIB が扱う VM やストレージアカウントを置く場所であり、ステージングリソースグループと呼ばれます。

### customazed builder run

`customazed builder run` でイメージのビルドを開始します。

```console
$ customazed builder run
2020/12/14 08:42:10 Loading config file customazed.json
2020/12/14 08:42:10 Loading auth-dev token in .customazed/auth_dev.json
2020/12/14 08:42:11 Saving auth-dev token in .customazed/auth_dev.json
2020/12/14 08:42:11 Current builder name: 1e83069e-f650-527a-a176-d5bb572580bf_1
2020/12/14 08:42:11 Running image build...
Error: Future#WaitForCompletion: context has been cancelled: StatusCode=200 -- Original Error: context deadline exceeded
```

現時点では長時間のビルドでは上記のようにタイムアウトエラーを起こして終了してしまいます。ビルドの状態や結果は `customazed builder show-status` コマンドで確認してください。

```console
$ customazed builder show-status
2020/12/14 08:42:43 Loading config file customazed.json
2020/12/14 08:42:43 Loading auth-dev token in .customazed/auth_dev.json
2020/12/14 08:42:43 Current builder name: 1e83069e-f650-527a-a176-d5bb572580bf_1
2020/12/14 08:42:43 Getting image template status...
2020/12/14 08:42:44 
{
  "lastRunStatus": {
    "startTime": "2020-12-13T23:42:11.137066996Z",
    "endTime": "0001-01-01T00:00:00Z",
    "runState": "Running",
    "runSubState": "Building",
    "message": ""
  },
  "provisioningState": "Succeeded"
}
```

ビルドログはステージングリソースグループ内に作られたランダムな名前のストレージアカウントの `packerlogs` という Blob コンテナ内にある `customization.log` という Append Blob に記録されています。

![packerlogs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61759/bd38e4ac-45d0-82f6-8e17-e062ea971fed.png)

customization.log の内容を見るコツとしては、ただ選択していくとファイルのダウンロードになってしまい扱いづらいので、上記画像のようにコンテキストメニューの Download を選ぶことです。そうすると web ブラウザ内でログの表示ができます。ただしログは何万行もの分量になることがあるものなので注意してください。

customization.log の見方については[トラブルシューティングのドキュメント](https://docs.microsoft.com/ja-jp/azure/virtual-machines/linux/image-builder-troubleshoot#troubleshoot-build-failures)を参照してください。

### イメージの確認

`customazed builder show-runs` で AIB の出力先とその結果を調べられます。

```console
$ customazed builder show-runs
2020/12/14 08:53:10 Loading config file customazed.json
2020/12/14 08:53:10 Loading auth-dev token in .customazed/auth_dev.json
2020/12/14 08:53:10 Current builder name: 1e83069e-f650-527a-a176-d5bb572580bf_1
2020/12/14 08:53:10 Getting image template run outputs...
2020/12/14 08:53:11 
[
  {
    "artifactId": "/subscriptions/58110a2c-a91b-4fdd-b8ea-3c16e75106ac/resourceGroups/CustomazedRG/providers/Microsoft.Compute/images/CustomazedPublisher_CustomazedUbuntuServer_18.04-Customazed_1",
    "name": "ManagedImage",
    "provisioningState": "Succeeded"
  },
  {
    "name": "SharedImage",
    "provisioningState": "Creating"
  }
]
```

これらから Azure Portal などを使って VM をデプロイしてみて、カスタマイズが反映された VM が作られることを確認してください。

### 後始末

AIB が出力した VM イメージ、共有イメージギャラリー、テスト作成した VM は課金されるリソースなので、チュートリアルが終わったらリソースグループごと削除してください。

ステージングリソースグループは直接消すことはできません。 `customazed builder delete` によりイメージテンプレートのリソースを削除すると一緒に消えます。

```console
$ customazed builder delete
2020/12/14 10:35:27 Loading config file customazed.json
2020/12/14 10:35:27 Loading auth-dev token in .customazed/auth_dev.json
2020/12/14 10:35:27 Saving auth-dev token in .customazed/auth_dev.json
2020/12/14 10:35:27 Current builder name: 1e83069e-f650-527a-a176-d5bb572580bf_1
2020/12/14 10:35:27 Deleting image template...
```

なお削除する前に customazed.json を編集すると、このコマンドでは削除できなくなってしまいますので、注意が必要です。その場合は Azure Portal から削除してください。

リソースグループ `builder.resourceGroup` に行き、「Show hidden types」というチェックボックスをオンにすると `microsoft.virtualmachineimages/imagetemplates` というタイプのリソースが見えるようになりますので、それを削除します。

![delete_builder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61759/24399c71-f5f4-51d1-b6fc-3469ff2db23c.png)

## Customazed CLI 設定ファイル

customazed.json や customazed_builder.josn では [Go テンプレート](https://golang.org/pkg/text/template/)により動的な設定を生成しています。

現時点で利用可能な機能は、次のテンプレート関数の呼び出しのみです。

|関数|説明|
|---|---|
|``{{var `NAME`}}``|customazed.json の JSON オブジェクトの `variables` プロパティ配下の値を返します。|
|``{{cfg `NAME`}}``|customazed.json の JSON オブジェクト配下の値を返します。|
|``{{env `NAME`}}``|環境変数 `NAME` の値を返します。|
|``{{id}}``|``{{cfg `id`}}`` と一緒です。|
|``{{prefix}}``|``{{cfg `storage.prefix`}}`` と一緒です。|
|``{{hash `WORD`}}``|`WORD` を SHA1 ハッシュ化した [UUIDv5] の文字列を返します。名前空間は `HashNS` 設定の文字列により固定されます。|
|``{{upload `PATH`}}``|ローカルのファイル `PATH` をストレージアカウントにアップロードし、そのダウンロード URL を返します。|

## まとめ

[Azure Image Builder] と、その利用を補助するツール [Customazed CLI] を紹介しました。素の AIB と比べて簡単に利用できることがおわかりいただけたかと思います。現在はこれを使って組織内で展開する Windows Virtual Desktop や自動ビルドシステムのための VM イメージのメンテナンス体制を構築しようとしています。

AIB はなかなかよいと思うのですが、使えるリージョンが米国とヨーロッパに限られており、イメージを東日本に転送するために時間と費用のロスが発生しているのが残念で、はやく GA になって、東日本でも使えるようになってほしいです。また [Ansible Provisioner] も使えるようにならないかなあとも思います。

なお Customazed CLI が対応するのは AIB だけではありません。次のサービスと組み合わせて使うことを想定して開発しています。

- [Azure Image Builder]
- [仮想マシンの Custom Script 拡張](https://docs.microsoft.com/ja-jp/azure/virtual-machines/extensions/custom-script-linux)
- [Azure Resource Manager テンプレートスペック](https://docs.microsoft.com/ja-jp/azure/azure-resource-manager/templates/template-specs) (予定)

Customazed CLI を作った当初の目的は、上記のサービスを扱うときに頻出するストレージアカウントを介したプライベートなファイルの転送を、[Packer] と同じくらい簡単にすることでした。

作り始めてから日が浅いため、まだ不備が多くろくなドキュメントもないのですが、これからがんばって開発・改善してきたいと思います。