PowerShell:如何使用 Packer 在 Azure 中创建虚拟机映像

适用于:✔️ Windows VM

Azure 中的每个虚拟机 (VM) 都是基于定义 Windows 分发和操作系统版本的映像创建的。 映像可以包括预安装的应用程序和配置。 Azure 市场为最常见的操作系统和应用程序环境提供许多第一和第三方映像,或者也可创建满足自身需求的自定义映像。 本文详细介绍了如何使用开源工具 Packer 在 Azure 中定义和生成自定义映像。

本文最后一次使用 Packer 版本 1.8.1 于 2020 年 8 月 5 日进行了测试。

注意

Azure 现推出一项服务,即 Azure 映像生成器,用于定义和创建你自己的自定义映像。 Azure 映像生成器基于 Packer 构建,因此你可以将现有 Packer shell 配置程序脚本与之配合使用。 若要开始使用 Azure 映像生成器,请参阅使用 Azure 映像生成器创建 Windows VM

创建 Azure 资源组

生成过程中,Packer 将在生成源 VM 时创建临时 Azure 资源。 要捕获该源 VM 用作映像,必须定义资源组。 Packer 生成过程的输出存储在此资源组中。

使用 New-AzResourceGroup 创建资源组。 以下示例在“chinaeast2”位置创建名为“myPackerGroup”的资源组:

$rgName = "myPackerGroup"
$location = "China East 2"
New-AzResourceGroup -Name $rgName -Location $location

创建 Azure 凭据

使用服务主体通过 Azure 对 Packer 进行身份验证。 Azure 服务主体是可与应用、服务和自动化工具(如 Packer)结合使用的安全性标识。 用户控制和定义服务主体可在 Azure 中执行的操作的权限。

使用 New-AzADServicePrincipal 创建服务主体。 -DisplayName 的值必须唯一;请根据需要将其替换为你自己的值。

$sp = New-AzADServicePrincipal -DisplayName "PackerPrincipal" -role Contributor -scope /subscriptions/yyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyy
$plainPassword = (New-AzADSpCredential -ObjectId $sp.Id).SecretText

然后输出密码和应用程序 ID。

$plainPassword
$sp.AppId

若要向 Azure 进行身份验证,还需使用 Get-AzSubscription 获取 Azure 租户和订阅 ID:

$subName = "mySubscriptionName"
$sub = Get-AzSubscription -SubscriptionName $subName

定义 Packer 模板

若要生成映像,请创建一个模板作为 JSON 文件。 在模板中,定义执行实际生成过程的生成器和设置程序。 Packer 具有用于 Azure 的生成器,可用于定义 Azure 资源,如在前面创建的服务主体凭据。

创建名为 windows.json 的文件并粘贴以下内容。 为以下内容输入自己的值:

参数 获取位置
client_id 通过 $sp.AppId 查看服务主体 ID
client_secret 使用 $plainPassword 查看自动生成的密码
tenant_id $sub.TenantId 命令的输出
subscription_id $sub.SubscriptionId 命令的输出
managed_image_resource_group_name 在第一步中创建的资源组的名称
managed_image_name 创建的托管磁盘映像的名称
cloud_environment_name 部署目标 Azure 云的名称。 有效选项在 Public, China, Germany, or USGovernment 列表中
{
  "builders": [{
    "type": "azure-arm",

    "client_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx",
    "client_secret": "ppppppp-pppp-pppp-pppp-ppppppppppp",
    "tenant_id": "zzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
    "subscription_id": "yyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyy",

    "managed_image_resource_group_name": "myPackerGroup",
    "managed_image_name": "myPackerImage",

    "os_type": "Windows",
    "image_publisher": "MicrosoftWindowsServer",
    "image_offer": "WindowsServer",
    "image_sku": "2016-Datacenter",

    "communicator": "winrm",
    "winrm_use_ssl": true,
    "winrm_insecure": true,
    "winrm_timeout": "5m",
    "winrm_username": "packer",

    "azure_tags": {
        "dept": "Engineering",
        "task": "Image deployment"
    },

    "build_resource_group_name": "myPackerGroup",
    "cloud_environment_name": "China",
    "location": "China East 2",
    "vm_size": "Standard_D2_v2"
  }],
  "provisioners": [{
    "type": "powershell",
    "inline": [
      "Add-WindowsFeature Web-Server",
      "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",
      "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 } }"
    ]
  }]
}

还可以创建一个名为 windows.pkr.hcl 的文件,并在以下内容中粘贴自己的用于上述参数表的值。

source "azure-arm" "autogenerated_1" {
  azure_tags = {
    dept = "Engineering"
    task = "Image deployment"
  }
  build_resource_group_name         = "myPackerGroup"
  client_id                         = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxx"
  client_secret                     = "ppppppp-pppp-pppp-pppp-ppppppppppp"
  communicator                      = "winrm"
  image_offer                       = "WindowsServer"
  image_publisher                   = "MicrosoftWindowsServer"
  image_sku                         = "2016-Datacenter"
  managed_image_name                = "myPackerImage"
  managed_image_resource_group_name = "myPackerGroup"
  os_type                           = "Windows"
  subscription_id                   = "yyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyy"
  tenant_id                         = "zzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz"
  vm_size                           = "Standard_D2_v2"
  winrm_insecure                    = true
  winrm_timeout                     = "5m"
  winrm_use_ssl                     = true
  winrm_username                    = "packer"
}

build {
  sources = ["source.azure-arm.autogenerated_1"]

  provisioner "powershell" {
    inline = ["Add-WindowsFeature Web-Server", "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", "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 } }"]
  }

}

此模板将生成 Windows Server 2016 VM,安装 IIS,然后用 Sysprep 一般化 VM。 IIS 安装展示了如何使用 PowerShell 预配程序来运行其他命令。 最终的 Packer 映像包括必需的软件安装和配置。

Windows 来宾代理参与 Sysprep 过程。 必须先完全安装代理,然后才能对 VM 执行 sysprep。 若要确保这一点,则在执行 sysprep.exe 之前,所有代理服务必须都在运行。 上述 JSON 代码片段显示了在 PowerShell 配置程序中执行此操作的一种方法。 仅当将 VM 配置为安装代理(默认设置)时,才需要此代码片段。

生成 Packer 映像

如果本地计算机上尚未安装 Packer,请按照 Packer 安装说明进行安装。

按如下所述打开 cmd 提示并指定 Packer 模板文件,以便生成映像:

packer build windows.json

还可以通过指定 windows.pkr.hcl 文件来生成映像,如下所示:

packer build windows.pkr.hcl

前面命令的输出示例如下所示:

azure-arm output will be in this color.

==> azure-arm: Running builder ...
    azure-arm: Creating Azure Resource Manager (ARM) client ...
==> azure-arm: Creating resource group ...
==> azure-arm:  -> ResourceGroupName : 'packer-Resource-Group-pq0mthtbtt'
==> azure-arm:  -> Location          : 'China East 2'
==> azure-arm:  -> Tags              :
==> azure-arm:  ->> task : Image deployment
==> azure-arm:  ->> dept : Engineering
==> azure-arm: Validating deployment template ...
==> azure-arm:  -> ResourceGroupName : 'packer-Resource-Group-pq0mthtbtt'
==> azure-arm:  -> DeploymentName    : 'pkrdppq0mthtbtt'
==> azure-arm: Deploying deployment template ...
==> azure-arm:  -> ResourceGroupName : 'packer-Resource-Group-pq0mthtbtt'
==> azure-arm:  -> DeploymentName    : 'pkrdppq0mthtbtt'
==> azure-arm: Getting the certificate's URL ...
==> azure-arm:  -> Key Vault Name        : 'pkrkvpq0mthtbtt'
==> azure-arm:  -> Key Vault Secret Name : 'packerKeyVaultSecret'
==> azure-arm:  -> Certificate URL       : 'https://pkrkvpq0mthtbtt.vault.azure.cn/secrets/packerKeyVaultSecret/8c7bd823e4fa44e1abb747636128adbb'
==> azure-arm: Setting the certificate's URL ...
==> azure-arm: Validating deployment template ...
==> azure-arm:  -> ResourceGroupName : 'packer-Resource-Group-pq0mthtbtt'
==> azure-arm:  -> DeploymentName    : 'pkrdppq0mthtbtt'
==> azure-arm: Deploying deployment template ...
==> azure-arm:  -> ResourceGroupName : 'packer-Resource-Group-pq0mthtbtt'
==> azure-arm:  -> DeploymentName    : 'pkrdppq0mthtbtt'
==> azure-arm: Getting the VM's IP address ...
==> azure-arm:  -> ResourceGroupName   : 'packer-Resource-Group-pq0mthtbtt'
==> azure-arm:  -> PublicIPAddressName : 'packerPublicIP'
==> azure-arm:  -> NicName             : 'packerNic'
==> azure-arm:  -> Network Connection  : 'PublicEndpoint'
==> azure-arm:  -> IP Address          : '40.76.55.35'
==> azure-arm: Waiting for WinRM to become available...
==> azure-arm: Connected to WinRM!
==> azure-arm: Provisioning with Powershell...
==> azure-arm: Provisioning with shell script: /var/folders/h1/ymh5bdx15wgdn5hvgj1wc0zh0000gn/T/packer-powershell-provisioner902510110
    azure-arm: #< CLIXML
    azure-arm:
    azure-arm: Success Restart Needed Exit Code      Feature Result
    azure-arm: ------- -------------- ---------      --------------
    azure-arm: True    No             Success        {Common HTTP Features, Default Document, D...
    azure-arm: <Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS><I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil /><PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj></Objs>
==> azure-arm: Querying the machine's properties ...
==> azure-arm:  -> ResourceGroupName : 'packer-Resource-Group-pq0mthtbtt'
==> azure-arm:  -> ComputeName       : 'pkrvmpq0mthtbtt'
==> azure-arm:  -> Managed OS Disk   : '/subscriptions/guid/resourceGroups/packer-Resource-Group-pq0mthtbtt/providers/Microsoft.Compute/disks/osdisk'
==> azure-arm: Powering off machine ...
==> azure-arm:  -> ResourceGroupName : 'packer-Resource-Group-pq0mthtbtt'
==> azure-arm:  -> ComputeName       : 'pkrvmpq0mthtbtt'
==> azure-arm: Capturing image ...
==> azure-arm:  -> Compute ResourceGroupName : 'packer-Resource-Group-pq0mthtbtt'
==> azure-arm:  -> Compute Name              : 'pkrvmpq0mthtbtt'
==> azure-arm:  -> Compute Location          : 'China East 2'
==> azure-arm:  -> Image ResourceGroupName   : 'myResourceGroup'
==> azure-arm:  -> Image Name                : 'myPackerImage'
==> azure-arm:  -> Image Location            : 'chinaeast2'
==> azure-arm: Deleting resource group ...
==> azure-arm:  -> ResourceGroupName : 'packer-Resource-Group-pq0mthtbtt'
==> azure-arm: Deleting the temporary OS disk ...
==> azure-arm:  -> OS Disk : skipping, managed disk was used...
Build 'azure-arm' finished.

==> Builds finished. The artifacts of successful builds are:
--> azure-arm: Azure.ResourceManagement.VMImage:

ManagedImageResourceGroupName: myResourceGroup
ManagedImageName: myPackerImage
ManagedImageLocation: chinaeast2

Packer 需要几分钟时间来生成 VM、运行设置程序并清理部署。

基于 Packer 映像创建 VM

现在可以使用 New-AzVM 从映像创建 VM。 如果提供支持的网络资源尚不存在,则会创建这些资源。 出现提示时,输入要在 VM 上创建的管理用户名和密码。 以下示例基于 myPackerImage 创建一个名为 myVM 的 VM。

New-AzVm `
    -ResourceGroupName $rgName `
    -Name "myVM" `
    -Location $location `
    -VirtualNetworkName "myVnet" `
    -SubnetName "mySubnet" `
    -SecurityGroupName "myNetworkSecurityGroup" `
    -PublicIpAddressName "myPublicIpAddress" `
    -OpenPorts 80 `
    -Image "myPackerImage"

如果你希望在与 Packer 映像不同的资源组或区域中创建虚拟机,请指定映像 ID 而不是映像名称。 可以使用 Get-AzImage 获取映像 ID。

基于 Packer 映像创建 VM 需要几分钟时间。

测试 VM 和 Web 服务器

使用 Get-AzPublicIPAddress 获取 VM 的公共 IP 地址。 以下示例获取前面创建的“myPublicIP” 的 IP 地址:

Get-AzPublicIPAddress `
    -ResourceGroupName $rgName `
    -Name "myPublicIPAddress" | select "IpAddress"

若要在运行中查看你的 VM,包括 Packer 预配程序中的 IIS 安装,请在 Web 浏览器中输入公共 IP 地址。

IIS default site

后续步骤

你还可以将现有的 Packer 配置程序脚本与 Azure 映像生成器配合使用。