【Packer】GitHubActionsでAzure VMのイメージをビルドする

今回は以下の記事でご紹介した Windows Server 2022 の日本語化イメージ作成を GitHubActions で実行できるようにしてみました。

https://qiita.com/m-oka-system/items/fc7428a566b3d6f4b724

# 事前準備

以下の記事を参考にGitHubActions から OpenID Connect で認証できるようにしておきます。

https://qiita.com/m-oka-system/items/eb88e0e93aa322342f34

# ワークフローを作成
課題もありますが、完成系はこんな感じです。
```yaml:.github\workflows\main.yml
name: Use Packer to create a Japanese-language image of an Azure Windows VM

on:
  # push:
  #   branches:
  #     - main
  workflow_dispatch:
    # inputs:
    #   image_version:
    #     description: "VM image version name for the semantic version pattern."
    #     required: true
    #     type: string
    #     default: "1.0.0"

permissions:
  id-token: write
  contents: read

env:
  PKR_VAR_project: ${{ vars.PROJECT }}
  PKR_VAR_location: ${{ vars.LOCATION }}
  PKR_VAR_resource_group_name: ${{ vars.RESOURCE_GROUP_NAME }}
  PKR_VAR_vm_size: ${{ vars.VM_SIZE }}
  PKR_VAR_gallery_name: ${{ vars.GALLERY_NAME }}
  PKR_VAR_image_definition: ${{ vars.IMAGE_DEFINITION }}
  PKR_VAR_replication_regions: ${{ vars.REPLICATION_REGIONS }}
  PKR_VAR_winrm_password: ${{ secrets.WINRM_PASSWORD }}
  keep_latest_count: ${{vars.KEEP_LATEST_COUNT}}

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Get runner's Public IP
        id: ip
        uses: haythem/public-ip@v1.2

      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          enable-AzPSSession: true

      - name: packer validate
        run: packer validate -var 'inbound_ip_addresses=["${{ steps.ip.outputs.ipv4 }}"]' .

      - name: packer build
        run: packer build -var 'inbound_ip_addresses=["${{ steps.ip.outputs.ipv4 }}"]' .

      - name: Delete versions of images prior to the specified number
        uses: Azure/powershell@v1
        with:
          inlineScript: |
            $imageVersions = Get-AzGalleryImageVersion -ResourceGroupName ${{ env.PKR_VAR_resource_group_name }} -GalleryName ${{ env.PKR_VAR_gallery_name }} -GalleryImageDefinitionName ${{ env.PKR_VAR_image_definition }}
            if ($imageVersions.Count -gt ${{ env.keep_latest_count }}) {
                $sortedImageVersions = $imageVersions | Sort-Object { $_.PublishingProfile.PublishedDate } -Descending
                for ($i = ${{ env.keep_latest_count }}; $i -lt $sortedImageVersions.Count; $i++) {
                    $imageVersion = $sortedImageVersions[$i]
                    Remove-AzGalleryImageVersion -ResourceGroupName ${{ env.PKR_VAR_resource_group_name }} -GalleryName ${{ env.PKR_VAR_gallery_name }} -GalleryImageDefinitionName ${{ env.PKR_VAR_image_definition }} -Name $imageVersion.Name -Force
                }
            }
          azPSVersion: "latest"

      - name: logout
        run: |
          az logout

```


# ワークフローの解説
## トリガー
今回は `on.push` ではなく、 `on.workflow_dispatch` を使って手動でワークフローを実行できるようにします。
~~この時、Azure Compute Galleryに保存するイメージのバージョン `image_version` を手入力するようにしました。セマンティックバージョニングのよい管理方法がわからなかったので、とりあえず手入力で回避。~~
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402286/0d953993-50d4-7ced-566c-e245f4defbab.png)

 → **`on.workflow_dispatch.inputs` をやめて、日付から `image version` 生成する形に修正しました。（例：2023.0330.1200）**
```hcl:azure-winvm-image.pkr.hcl
locals {
  timestamp = formatdate("YYYYMMDDhhmmss", timeadd(timestamp(), "9h"))
  image_version = formatdate("YYYY.MMDD.hhmm", timeadd(timestamp(), "9h"))
}

...

  shared_image_gallery_destination {
    resource_group      = var.resource_group_name
    gallery_name        = var.gallery_name
    image_name          = var.image_definition
    image_version       = local.image_version
    replication_regions = var.replication_regions
  }
```

## Packerに渡す変数
`auto.pkrvars.hcl` で渡す変数の値をどうしようか悩みました。
~~はじめは `packer build -var key=value` で全部渡そうとしたのですが、ワークフローの中で `auto.pkrvars.hcl` を作成したほうがスッキリしてわかりやすい気がしたのでヒアドキュメントで作成する方法にしました。このあたりのお作法はまだよくわかっていません。~~

 → **`PKR_VAR_name` で環境変数を指定すれば良いだけだったので修正しました。**
環境変数は冒頭にまとめています。
- `vars.` でリポジトリの変数を参照
- `inputs.` で入力値を参照
- `secrets.` でリポジトリのシークレットを参照

```yaml
env:
  PKR_VAR_project: ${{ vars.PROJECT }}
  PKR_VAR_location: ${{ vars.LOCATION }}
  PKR_VAR_resource_group_name: ${{ vars.RESOURCE_GROUP_NAME }}
  PKR_VAR_vm_size: ${{ vars.VM_SIZE }}
  PKR_VAR_gallery_name: ${{ vars.GALLERY_NAME }}
  PKR_VAR_image_definition: ${{ vars.IMAGE_DEFINITION }}
  PKR_VAR_replication_regions: ${{ vars.REPLICATION_REGIONS }}
  PKR_VAR_winrm_password: ${{ secrets.WINRM_PASSWORD }}
  keep_latest_count: ${{vars.KEEP_LATEST_COUNT}}
```
## Azureサインイン
必須ではありませんが、Azure PowerShellを使えるように `enable-AzPSSession: true` を設定しています。ワークフローの最後にAzure PowerShellを使います。
```yaml
      - name: Azure Login
        uses: azure/login@v1
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          enable-AzPSSession: true
```

## Packerの処理
GitHubActionsのランナーのグローバルIPは `haythem/public-ip@v1.2` アクションで取得して、`packer validate`, `packer build` の時に`-var` で渡しています。

なお、ランナーのUbuntu環境ではデフォルトで `packer` コマンドが使えるようになっていますが、Packerのバージョンを固定する場合は [hashicorp/setup-packer](https://github.com/hashicorp/setup-packer) アクションを使った方が良さそうです。

```yaml
jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Get runner's Public IP
        id: ip
        uses: haythem/public-ip@v1.2
```
```yaml
      - name: packer validate
        run: packer validate -var 'inbound_ip_addresses=["${{ steps.ip.outputs.ipv4 }}"]' .

      - name: packer build
        run: packer build -var 'inbound_ip_addresses=["${{ steps.ip.outputs.ipv4 }}"]' .
```

## イメージの世代管理
ここは任意ですが、`keep_latest_count` で定義する数だけ残して、それより古いイメージを削除するようにしています。

```yaml
      - name: Delete versions of images prior to the specified number
        uses: Azure/powershell@v1
        with:
          inlineScript: |
            $imageVersions = Get-AzGalleryImageVersion -ResourceGroupName ${{ env.PKR_VAR_resource_group_name }} -GalleryName ${{ env.PKR_VAR_gallery_name }} -GalleryImageDefinitionName ${{ env.PKR_VAR_image_definition }}
            if ($imageVersions.Count -gt ${{ env.keep_latest_count }}) {
                $sortedImageVersions = $imageVersions | Sort-Object { $_.PublishingProfile.PublishedDate } -Descending
                for ($i = ${{ env.keep_latest_count }}; $i -lt $sortedImageVersions.Count; $i++) {
                    $imageVersion = $sortedImageVersions[$i]
                    Remove-AzGalleryImageVersion -ResourceGroupName ${{ env.PKR_VAR_resource_group_name }} -GalleryName ${{ env.PKR_VAR_gallery_name }} -GalleryImageDefinitionName ${{ env.PKR_VAR_image_definition }} -Name $imageVersion.Name -Force
                }
            }
          azPSVersion: "latest"
```

# GitHubの変数とシークレットの登録
`var.` で参照するリポジトリ変数をGitHubに登録します。
```bash
gh variable set -f .env
```
```.env
PROJECT="MyProject"
LOCATION="japaneast"
RESOURCE_GROUP_NAME="packer-rg""
VM_SIZE="Standard_DS1_v2"
GALLERY_NAME="windows_dev_sig"
IMAGE_DEFINITION="2022-datacenter-smalldisk-g2"
REPLICATION_REGIONS=["japaneast"]
KEEP_LATEST_COUNT=1
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402286/44b0350b-8275-4614-5782-dbeed06c2ab4.png)

Azureサインインに必要なシークレットは事前準備の中で登録済みですが、`winrm_password` にセットするパスワードのみ追加で登録します。パスワードは任意のものに置き換えてください。

```bash
gh secret set WINRM_PASSWORD --body "input_your_password"
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402286/7696a44b-23a4-a3be-598b-8b9abafaab9f.png)

# ワークフロー実行
GitHubの Actions タブからワークフロー実行します。約33分で完了しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402286/34abbd3d-3501-28bf-e029-5acadcd1d8e0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/402286/67916a43-5654-d584-ad7a-e0499dad6c6a.png)

# まとめ
今回はGitHubActions上でPackerを実行してVMイメージを作成できるようにしてみました。いちおう動く形にはなりましたが、ローカル実行と違ってCI/CDに組み込む場合は色々と考慮すべき点があることがわかり、良い気づきになりました。