こんにちは、テクマトリックスの酒井です。

Microsoft Azure(以下、Azure)にて日本語版の Windows Server を使いたい場合、通常はベースイメージから VM (仮想マシン)を作成して言語パックをインストールする作業が必要です。この作業を繰り返し実行することに苦痛を感じていたため、 Packer を使って日本語化された Windows Server のイメージを作成してみましたので紹介します。

どのような VM イメージを作成するのか?

VM イメージから VM を起動した時、 Windows システムおよびログオンユーザーの言語の設定が日本語になっていて、地域やタイムゾーンの設定も日本になっているような VM を作成します。ついでに頻繁に利用する OpenSSH Server, Git, JDK 11 といったアプリケーションもすぐに利用できるようにします。

アプリケーションを除外しても、このような VM イメージは残念ながら Azure Marketplace には見つかりませんでした。そのため通常は英語版の Windows Server の VM イメージからVMを作成して、言語パックを適用し、地域や言語などの設定を行う必要があります。この作業を実施済みの VM を作成することにより、繰り返し作業、作業ミス、作業時間を削減できます。

なぜ Packer を使うのか?

Packer は、単一のソーステンプレートから複数のプラットフォーム用の同一のマシンイメージを作成できるオープンソースツールです。Packer の動作は、Packer が従うべき一連の宣言とコマンドで構成される Packer テンプレートによって決定されます。テンプレートファイルに言語パックのインストールや設定を記述して Packer を実行することで VM イメージを作成できます。

日本語化のための作業をコードとして記述することにより、人手に頼らず作業を繰り返し実行可能になります。またベースの VM イメージが変更された場合の作り直しにも素早く対応可能になります。

Azure Resource Manager を利用することにより同様のことは実現可能です。しかし、 Azure 以外のクラウドサービスでもテンプレートファイルを利用して VM イメージを作成できることから、今回は Packer を選択しました。

なお Packer の一般的な利用方法については、 Packer のドキュメントをご参照ください。

Packer のテンプレートファイルの作成

Packer は指定された VM イメージから VM を作成し、その VM に対して何らかの操作を行い、最後にその VM から新しい VM イメージを生成します。この一連の動作を指定するために jdk11-windowsserver-2019.pkr.hcl という名前でテンプレートファイルを作成し、以下の内容を記述していきます。

Packer のプラグイン

Packer を利用して Azure Resource Manager のマネージドイメージをビルドするには、 Packer のプラグインによって提供される Azure Resource Manager Builder が必要です。この機能を提供するプラグインが必要であることを packer ブロックに記述します。

packer {
  required_plugins {
    azure = {
      version = " >= 1.0.0"
      source = "github.com/hashicorp/azure"
    }
  }
}

ベースとなる VM イメージ

今回ベースとして利用する VM イメージは「[smalldisk] Windows Server 2019 Datacenter – Gen2」としました。この VM イメージをテンプレートファイル内の source ブロックで指定します。各設定値は Packer のドキュメントに記載のとおり Azure CLI を利用して調べました。

source "azure-arm" "windowsserver-2019" {
  # ...

  # [smalldisk] Windows Server 2019 Datacenter - Gen2
  os_type = "Windows"
  image_publisher = "MicrosoftWindowsServer"
  image_offer = "WindowsServer"
  image_sku = "2019-Datacenter-smalldisk"
  image_version = "latest"

  # ...
}

VM の操作

作成される VM に対して次の操作を行います。

  1. 言語パックのダウンロードとインストールを行う Powershell スクリプトを実行します。スクリプトは別のファイルで用意します。
  2. 言語パックのインストールを完了するために VM を再起動します。
  3. 応答ファイル (unattend.xml) を作成します。応答ファイルでは、これから作成する VM イメージから VM を起動した時に、 Windows のセットアップ中やユーザーの既定の地域や言語などを指定しています。
  4. アプリケーションをインストールする Powershell スクリプトを実行します。ここでは、 OpenSSH Server を有効にしているのと、 Chocolatey をインストールし、 Chocolatey を使って Git や JDK などをインストールします。
  5. Sysprep (システム準備) を実行して VM イメージ作成の準備を行います。記述の内容はほぼ Packer のプラグインのドキュメントに記載のとおりですが、3.でコピーした応答ファイルをシステム(Windows)にキャッシュするために Sysprep.exe の引数を追加しました。

これらの操作を、テンプレートファイル内の build ブロックに記述します。

build {
  # ベースとなるVMイメージ
  sources = [
    "source.azure-arm.windowsserver-2019"
  ]

  # 言語パックのダウンロードとインストール
  provisioner "powershell" {
    scripts = [
      "${path.root}/scripts/1_install_language_pack.ps1",
    ]
  }

  # 再起動
  provisioner "windows-restart" {}

  # 応答ファイルの作成
  provisioner "file" {
    source = "${path.root}/scripts/Unattend.xml"
    destination = "C:/Unattend.xml"
  }

  # アプリケーションのインストール
  provisioner "powershell" {
    elevated_user     = var.username
    elevated_password = var.password
    scripts = [
      "${path.root}/scripts/5_enable_openssh_server.ps1",
      "${path.root}/scripts/9_install_apps.ps1",
    ]
  }

  # Sysprep の実行
  provisioner "powershell" {
    inline = [
      " # NOTE: the following *3* lines are only needed if the you have installed the Guest Agent.",
      "  while ((Get-Service RdAgent).Status -ne 'Running') { Start-Sleep -s 5 }",
      "  while ((Get-Service WindowsAzureGuestAgent).Status -ne 'Running') { Start-Sleep -s 5 }",

      "& $env:SystemRoot\\System32\\Sysprep\\Sysprep.exe /oobe /generalize /quiet /quit /mode:vm /unattend:C:\\Unattend.xml",
      "while($true) { $imageState = Get-ItemProperty HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Setup\\State | Select ImageState; if($imageState.ImageState -ne 'IMAGE_STATE_GENERALIZE_RESEAL_TO_OOBE') { Write-Output $imageState.ImageState; Start-Sleep -s 10  } else { break } }"
    ]
  }
}

VM の操作時に利用する Powershell スクリプトや応答ファイルは scripts サブディレクトリに保存しました。

作成された VM イメージの保存先

作成された VM イメージの保存先は、予め作成しておいた Azure コンピューティング ギャラリーとします。この Azure コンピューティング ギャラリーを、ベースとなる VM イメージを指定したのと同じ source ブロックで指定します。

source "azure-arm" "windowsserver-2019" {
  # ...

  # VMイメージの保存先
  shared_image_gallery_destination {
    subscription         = var.subscription_id
    resource_group       = var.image_resource_group_name
    gallery_name         = "sakai_image_gallery"
    image_name           = "jdk11-windowsserver-2019"
    image_version        = var.image_version
    replication_regions  = [var.location]
    storage_account_type = "Standard_LRS"
  }

  # ...
}

その他の設定

Packer のテンプレートファイルには他にも Azure の認証や Packer の変数などの記述がありますが、この記事では割愛します。詳しくは Packer のドキュメントをご参照ください。

Packerの実行

Packer のテンプレートファイルができたら、次のように Packer を実行して VM イメージを作成します。

# 初期化。プラグインもインストールされる
packer init jdk11-windowsserver-2019.pkr.hcl

# テンプレートファイルの検証
packer validate jdk11-windowsserver-2019.pkr.hcl

# VMイメージの作成
packer build jdk11-windowsserver-2019.pkr.hcl

コマンドの開始から約32分で VM イメージを作成することができました。そのうち11分が言語パックのインストールにかかっていました。 VM イメージの作成中に私は別の作業ができるため、時間を効率良く使えるようになりました。

加えて、 Packer のテンプレートファイルやスクリプトを Git でバージョン管理し、更新されたら Jenkins を経由して自動的に VM イメージを作成するようにしました。これによって他のメンバーがイメージを更新することも簡単にできます。

一つ残念なのは、この VM イメージから VM を作成しても、タイムゾーンは UTC のままとなっていることです。現状ではログオン後に手動で設定していますが、いずれはその必要もなくしたいと思います。

まとめ

日本語版の Windows Server の VM イメージを作成する手順をコード化することができました。またこの VM イメージから日本語版の VM を素早く作成することができるようになりました。今回ご紹介した方法を応用すれば様々な環境の VM イメージを簡単に用意できるため、開発現場の要望に応じて様々な環境を用意しなければならないインフラの担当の方にお勧めしたい方法だと思います。

By tsakai

Jenkins関連のサービスやCloudBees製品を主に担当しています。 Certified CloudBees Jenkins Engineer (CCJE) および CloudBees CI DevOps Associate です。