使用 Terraform 通过 Packer 自定义映像创建 Azure 虚拟机规模集
使用 Terraform 可以定义、预览和部署云基础结构。 使用 Terraform 时,请使用 HCL 语法来创建配置文件。 利用 HCL 语法,可指定 Azure 这样的云提供程序和构成云基础结构的元素。 创建配置文件后,请创建一个执行计划,利用该计划,可在部署基础结构更改之前先预览这些更改。 验证了更改后,请应用该执行计划以部署基础结构。
Azure 虚拟机规模集允许配置相同的 VM。 VM 实例的数目可以根据需求或计划进行调整。 有关详细信息,请参阅自动缩放 Azure 门户中的虚拟机规模集
在本文中,学习如何:
- 设置 Terraform 部署
- 使用变量和输出部署 Terraform
- 创建和部署网络基础结构
- 使用 Packer 创建自定义虚拟机映像
- 使用自定义映像创建和部署虚拟机规模集
- 创建和部署 jumpbox
1.配置环境
- Azure 订阅:如果没有 Azure 订阅,请在开始之前创建一个免费帐户。
配置 Terraform:如果尚未执行此操作,请使用以下选项之一配置 Terraform:
2. 创建 Packer 映像
-
要点:
- 若要确保你有权访问 Packer 可执行文件,请运行以下命令:
packer -v
。 - 根据你的环境,你可能需要设置路径,再重新打开命令行。
- 若要确保你有权访问 Packer 可执行文件,请运行以下命令:
若要创建一个资源组来保存 Packer 映像,请运行 az group create。
az group create -n myPackerImages -l eastus
若要启用 Packer 来使用服务主体向 Azure 进行身份验证,请运行 az ad sp create-for-rbac。
az ad sp create-for-rbac --role Contributor --scopes /subscriptions/<subscription_id> --query "{ client_id: appId, client_secret: password, tenant_id: tenant }"
要点:
- 记下输出值(
appId
、client_secret
、tenant_id
)。
- 记下输出值(
若要显示当前 Azure 订阅,请运行 az account show。
az account show --query "{ subscription_id: id }"
创建名为
ubuntu.pkr.hcl
并插入以下代码的 Packer 模板变量文件。 使用服务主体和 Azure 订阅信息更新突出显示的行。packer { required_plugins { azure = { source = "github.com/hashicorp/azure" version = "~> 2" } } } variable client_id { type = string default = null } variable client_secret { type = string default = null } variable subscription_id { type = string default = null } variable tenant_id { type = string default = null } variable location { default = "eastus" } variable "image_resource_group_name" { description = "Name of the resource group in which the Packer image will be created" default = "myPackerImages" } variable "oidc_request_url" { default = null } variable "oidc_request_token" { default = null } # arm builder source "azure-arm" "builder" { client_id = var.client_id client_secret = var.client_secret image_offer = "UbuntuServer" image_publisher = "canonical" image_sku = "16.04-LTS" location = var.location managed_image_name = "myPackerImage" managed_image_resource_group_name = var.image_resource_group_name os_type = "Linux" subscription_id = var.subscription_id tenant_id = var.tenant_id oidc_request_url = var.oidc_request_url oidc_request_token = var.oidc_request_token vm_size = "Standard_DS2_v2" azure_tags = { "dept" : "Engineering", "task" : "Image deployment", } } build { sources = ["source.azure-arm.builder"] provisioner "shell" { execute_command = "chmod +x {{ .Path }}; {{ .Vars }} sudo -E sh '{{ .Path }}'" inline = [ "apt-get update", "apt-get upgrade -y", "apt-get -y install nginx", "/usr/sbin/waagent -force -deprovision+user && export HISTSIZE=0 && sync", ] } }
要点:
- 将
client_id
、client_secret
和tenant_id
字段设置为服务主体各自的值。 - 将
subscription_id
字段设置为 Azure 订阅 ID。
- 将
生成 Packer 映像。
packer build ubuntu.json
3. 实现 Terraform 代码
创建用于测试和运行示例 Terraform 代码的目录,并将其设为当前目录。
创建名为
main.tf
的文件并插入下列代码:terraform { required_version = ">=0.12" required_providers { azurerm = { source = "hashicorp/azurerm" version = "~>3.0" } azapi = { source = "Azure/azapi" version = "~> 1.0" } local = { source = "hashicorp/local" version = "2.4.0" } random = { source = "hashicorp/random" version = "3.5.1" } tls = { source = "hashicorp/tls" version = "4.0.4" } } } provider "azurerm" { features { resource_group { prevent_deletion_if_contains_resources = false } } } resource "random_pet" "id" {} resource "azurerm_resource_group" "vmss" { name = coalesce(var.resource_group_name, "201-vmss-packer-jumpbox-${random_pet.id.id}") location = var.location tags = var.tags } resource "random_string" "fqdn" { length = 6 special = false upper = false numeric = false } resource "azurerm_virtual_network" "vmss" { name = "vmss-vnet" address_space = ["10.0.0.0/16"] location = var.location resource_group_name = azurerm_resource_group.vmss.name tags = var.tags } resource "azurerm_subnet" "vmss" { name = "vmss-subnet" resource_group_name = azurerm_resource_group.vmss.name virtual_network_name = azurerm_virtual_network.vmss.name address_prefixes = ["10.0.2.0/24"] } resource "azurerm_public_ip" "vmss" { name = "vmss-public-ip" location = var.location resource_group_name = azurerm_resource_group.vmss.name allocation_method = "Static" domain_name_label = random_string.fqdn.result tags = var.tags } resource "azurerm_lb" "vmss" { name = "vmss-lb" location = var.location resource_group_name = azurerm_resource_group.vmss.name frontend_ip_configuration { name = "PublicIPAddress" public_ip_address_id = azurerm_public_ip.vmss.id } tags = var.tags } resource "azurerm_lb_backend_address_pool" "bpepool" { loadbalancer_id = azurerm_lb.vmss.id name = "BackEndAddressPool" } resource "azurerm_lb_probe" "vmss" { loadbalancer_id = azurerm_lb.vmss.id name = "ssh-running-probe" port = var.application_port } resource "azurerm_lb_rule" "lbnatrule" { loadbalancer_id = azurerm_lb.vmss.id name = "http" protocol = "Tcp" frontend_port = var.application_port backend_port = var.application_port backend_address_pool_ids = [azurerm_lb_backend_address_pool.bpepool.id] frontend_ip_configuration_name = "PublicIPAddress" probe_id = azurerm_lb_probe.vmss.id } data "azurerm_resource_group" "image" { name = var.packer_resource_group_name } data "azurerm_image" "image" { name = var.packer_image_name resource_group_name = data.azurerm_resource_group.image.name } resource "azapi_resource" "ssh_public_key" { type = "Microsoft.Compute/sshPublicKeys@2022-11-01" name = random_pet.id.id location = azurerm_resource_group.vmss.location parent_id = azurerm_resource_group.vmss.id } resource "azapi_resource_action" "ssh_public_key_gen" { type = "Microsoft.Compute/sshPublicKeys@2022-11-01" resource_id = azapi_resource.ssh_public_key.id action = "generateKeyPair" method = "POST" response_export_values = ["publicKey", "privateKey"] } resource "random_password" "password" { count = var.admin_password == null ? 1 : 0 length = 20 } locals { admin_password = try(random_password.password[0].result, var.admin_password) } resource "azurerm_virtual_machine_scale_set" "vmss" { name = "vmscaleset" location = var.location resource_group_name = azurerm_resource_group.vmss.name upgrade_policy_mode = "Manual" sku { name = "Standard_DS1_v2" tier = "Standard" capacity = 2 } storage_profile_image_reference { id = data.azurerm_image.image.id } storage_profile_os_disk { name = "" caching = "ReadWrite" create_option = "FromImage" managed_disk_type = "Standard_LRS" } storage_profile_data_disk { lun = 0 caching = "ReadWrite" create_option = "Empty" disk_size_gb = 10 } os_profile { computer_name_prefix = "vmlab" admin_username = var.admin_user admin_password = local.admin_password } os_profile_linux_config { disable_password_authentication = true ssh_keys { path = "/home/azureuser/.ssh/authorized_keys" key_data = azapi_resource_action.ssh_public_key_gen.output.publicKey } } network_profile { name = "terraformnetworkprofile" primary = true ip_configuration { name = "IPConfiguration" subnet_id = azurerm_subnet.vmss.id load_balancer_backend_address_pool_ids = [azurerm_lb_backend_address_pool.bpepool.id] primary = true } } tags = var.tags } resource "azurerm_public_ip" "jumpbox" { name = "jumpbox-public-ip" location = var.location resource_group_name = azurerm_resource_group.vmss.name allocation_method = "Static" domain_name_label = "${random_string.fqdn.result}-ssh" tags = var.tags } resource "azurerm_network_interface" "jumpbox" { name = "jumpbox-nic" location = var.location resource_group_name = azurerm_resource_group.vmss.name ip_configuration { name = "IPConfiguration" subnet_id = azurerm_subnet.vmss.id private_ip_address_allocation = "Dynamic" public_ip_address_id = azurerm_public_ip.jumpbox.id } tags = var.tags } resource "azurerm_virtual_machine" "jumpbox" { name = "jumpbox" location = var.location resource_group_name = azurerm_resource_group.vmss.name network_interface_ids = [azurerm_network_interface.jumpbox.id] vm_size = "Standard_DS1_v2" storage_image_reference { publisher = "Canonical" offer = "UbuntuServer" sku = "16.04-LTS" version = "latest" } storage_os_disk { name = "jumpbox-osdisk" caching = "ReadWrite" create_option = "FromImage" managed_disk_type = "Standard_LRS" } os_profile { computer_name = "jumpbox" admin_username = var.admin_user admin_password = local.admin_password } os_profile_linux_config { disable_password_authentication = true ssh_keys { path = "/home/azureuser/.ssh/authorized_keys" key_data = azapi_resource_action.ssh_public_key_gen.output.publicKey } } tags = var.tags }
创建名为
variables.tf
的文件来包含项目变量,并插入以下代码:variable "packer_resource_group_name" { description = "Name of the resource group in which the Packer image will be created" default = "myPackerImages" } variable "packer_image_name" { description = "Name of the Packer image" default = "myPackerImage" } variable "resource_group_name" { description = "Name of the resource group in which the Packer image will be created" default = null } variable "location" { default = "eastus" description = "Location where resources will be created" } variable "tags" { description = "Map of the tags to use for the resources that are deployed" type = map(string) default = { environment = "codelab" } } variable "application_port" { description = "Port that you want to expose to the external load balancer" default = 80 } variable "admin_user" { description = "User name to use as the admin account on the VMs that will be part of the VM scale set" default = "azureuser" } variable "admin_password" { description = "Default password for admin account" default = null }
创建名为
output.tf
的文件来指定 Terraform 会显示哪些值,然后插入以下代码:output "vmss_public_ip_fqdn" { value = azurerm_public_ip.vmss.fqdn } output "jumpbox_public_ip_fqdn" { value = azurerm_public_ip.jumpbox.fqdn } output "jumpbox_public_ip" { value = azurerm_public_ip.jumpbox.ip_address }
4. 初始化 Terraform
运行 terraform init,将 Terraform 部署进行初始化。 此命令将下载管理 Azure 资源所需的 Azure 提供程序。
terraform init -upgrade
要点:
- 参数
-upgrade
可将必要的提供程序插件升级到符合配置版本约束的最新版本。
5. 创建 Terraform 执行计划
运行 terraform plan 以创建执行计划。
terraform plan -out main.tfplan
要点:
terraform plan
命令将创建一个执行计划,但不会执行它。 它会确定创建配置文件中指定的配置需要执行哪些操作。 此模式允许你在对实际资源进行任何更改之前验证执行计划是否符合预期。- 使用可选
-out
参数可以为计划指定输出文件。 使用-out
参数可以确保所查看的计划与所应用的计划完全一致。
6. 应用 Terraform 执行计划
运行 terraform apply,将执行计划应用到云基础结构。
terraform apply main.tfplan
要点:
- 示例
terraform apply
命令假设你先前运行了terraform plan -out main.tfplan
。 - 如果为
-out
参数指定了不同的文件名,请在对terraform apply
的调用中使用该相同文件名。 - 如果未使用
-out
参数,请调用不带任何参数的terraform apply
。
7. 验证结果
从
terraform apply
命令的输出中,你会看到以下内容的值:- 虚拟机 FQDN
- Jumpbox FQDN
- Jumpbox IP 地址
浏览到虚拟机 URL,来确认显示“欢迎使用 nginx!”的默认页面。
使用在变量文件中定义的用户名和在运行
terraform apply
时指定的密码通过 SSH 连接到 jumpbox VM。 例如:ssh azureuser@<ip_address>
。
8.清理资源
删除虚拟机规模集
不再需要通过 Terraform 创建的资源时,请执行以下步骤:
运行 terraform plan 并指定
destroy
标志。terraform plan -destroy -out main.destroy.tfplan
要点:
terraform plan
命令将创建一个执行计划,但不会执行它。 它会确定创建配置文件中指定的配置需要执行哪些操作。 此模式允许你在对实际资源进行任何更改之前验证执行计划是否符合预期。- 使用可选
-out
参数可以为计划指定输出文件。 使用-out
参数可以确保所查看的计划与所应用的计划完全一致。
运行 terraform apply 以应用执行计划。
terraform apply main.destroy.tfplan
删除 Packer 映像和资源组
若要删除用于包含 Packer 映像的资源组,请运行 az group delete。 这还会删除 Packer 映像。
az group delete --name myPackerImages --yes
Azure 上的 Terraform 故障排除
排查在 Azure 上使用 Terraform 时遇到的常见问题