How to Deploy a Windows VM Scale Set in Azure using Terraform

In this story, we will learn how to deploy a Windows VM Scale Set in Azure using Terraform.

We are using a VM Scale Set Extension to execute a PowerShell script that will take of the setup of the servers.

Note: The code was tested with Terraform v0.14 and v0.15.

Prerequisites

Before we can create our Azure Virtual Machine, we will need an Azure SPN (Service Principal) to execute our Terraform code (check step 1 of this story if you need help creating an SPN).

Creating a Terraform file for Azure Authentication

First, we create a file called provider-variables.tf, used by Azure authentication variables.

In this story, we will use a Service Principal with a Client Secret. Check the link below for more info about Azure authentication for Terraform: https://www.terraform.io/docs/providers/azurerm/guides/service_principal_client_secret.html

#Azure authentication variablesvariable "azure_subscription_id" {
type = string
description = "Azure Subscription ID"
}
variable "azure_client_id" {
type = string
description = "Azure Client ID"
}
variable "azure_client_secret" {
type = string
description = "Azure Client Secret"
}
variable "azure_tenant_id" {
type = string
description = "Azure Tenant ID"
}

After that, we edit the file terraform.tfvars and add the Azure credential information:

#Azure authentication variablesazure_subscription_id = "your-azure-subscription-id"
azure_client_id = "your-azure-client-id"
azure_client_secret = "your-azure-client-secret"
azure_tenant_id = "your-azure-tenant-id"

Finally, we create the provider-main.tf, used to configure Terraform and the Azure provider:

# Define Terraform provider
terraform {
required_version = ">= 0.15"
}
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">=2.50.0"
}
}
}
# Configure the Azure provider
provider "azurerm" {
features {}
environment = "public"
subscription_id = var.azure-subscription-id
client_id = var.azure-client-id
client_secret = var.azure-client-secret
tenant_id = var.azure-tenant-id
}

Creating a Terraform file for the Network

In this step, we will create the file network-variables.tf to configure network variables, and add the following code:

variable "network-vnet-cidr" {
type = string
description = "The CIDR of the network VNET"
}
variable "vm-subnet-cidr" {
type = string
description = "The CIDR for the vm subnet"
}

Then we create the network-main.tf to configure the network, and add the following code:

# Create a resource group for network
resource "azurerm_resource_group" "network-rg" {
name = "network-rg"
location = var.location
}
# Create the network VNET
resource "azurerm_virtual_network" "network-vnet" {
name = "network-vnet"
address_space = [var.network-vnet-cidr]
resource_group_name = azurerm_resource_group.network-rg.name
location = azurerm_resource_group.network-rg.location
}
# Create a subnet for VM
resource "azurerm_subnet" "vm-subnet" {
name = "vm-subnet"
address_prefixes = [var.vm-subnet-cidr]
virtual_network_name = azurerm_virtual_network.network-vnet.name
resource_group_name = azurerm_resource_group.network-rg.name
}

Windows VM Scale Set Variables

The first step is to create the windows-vm-ss-variables.tf file, used to store variables for the Windows VM Scale Set and we add the variables for each Windows VM:

## Windows VM - Variables# Windows VM Admin User
variable "windows-admin-username" {
type = string
description = "Windows VM Admin User"
default = "tfadmin"
}
# Windows VM Admin Password
variable "windows-admin-password" {
type = string
description = "Windows VM Admin Password"
default = "S3cr3ts24"
}
# Windows VM Hostname (limited to 15 characters long)
variable "windows-vm-hostname" {
type = string
description = "Windows VM Hostname"
default = "tfazurevm"
}
# Windows VM Virtual Machine Size
variable "windows-vm-size" {
type = string
description = "Windows VM Size"
default = "Standard_B1s"
}

after that, we add the Windows OS variables to the same file:

## OS Image# Windows Server 2019 SKU used to build VMs
variable "windows-2019-sku" {
type = string
description = "Windows Server 2019 SKU used to build VMs"
default = "2019-Datacenter"
}
# Windows Server 2016 SKU used to build VMs
variable "windows-2016-sku" {
type = string
description = "Windows Server 2016 SKU used to build VMs"
default = "2016-Datacenter"
}
# Windows Server 2012 R2 SKU used to build VMs
variable "windows-2012-sku" {
type = string
description = "Windows Server 2012 R2 SKU used to build VMs"
default = "2012-R2-Datacenter"
}

PowerShell File to Configure the VM Scale Set

The next step is to create a bootstrap file called setup.ps1 to configure each Windows Server in the scale set.

Start-Transcript -Path 'C:/terraform-log.txt' -append;
$VerbosePreference = 'Continue';
$InformationPreference = 'Continue';
Install-WindowsFeature -name Web-Server -IncludeManagementTools;
Stop-Transcript;

Windows VM Scale Set Main File

Finally, create the file windows-vm-ss-main.tf. This file will create the virtual machines and Network Security Group (NSG). This is the code compatible with AzureRM v2.x:

# Bootstrapping Script
data "template_file" "tf-script" {
template = file("setup.ps1")
}
# Create Network Security Group to Access VM from Internet
resource "azurerm_network_security_group" "web-nsg" {
name = "web-nsg"
location = azurerm_resource_group.network-rg.location
resource_group_name = azurerm_resource_group.network-rg.name
security_rule {
name = "AllowRDP"
description = "Allow RDP"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "3389"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}
security_rule {
name = "AllowHTTP"
description = "Allow HTTP"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}
security_rule {
name = "AllowHTTPS"
description = "Allow HTTPS"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "Internet"
destination_address_prefix = "*"
}
}
# Associate the NSG with the Subnet
resource "azurerm_subnet_network_security_group_association" "web-nsg-association" {
subnet_id = azurerm_subnet.vm-subnet.id
network_security_group_id = azurerm_network_security_group.web-nsg.id
}
# Create Windows Virtual Machine Scale Set
resource "azurerm_windows_virtual_machine_scale_set" "web-ss" {
name = "web-ss"
location = azurerm_resource_group.network-rg.location
resource_group_name = azurerm_resource_group.network-rg.name
sku = var.windows-vm-size
instances = 2
computer_name_prefix = var.windows-vm-hostname
admin_username = var.windows-admin-username
admin_password = var.windows-admin-password
source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = var.windows-2019-sku
version = "latest"
}
os_disk {
storage_account_type = "Standard_LRS"
caching = "ReadWrite"
}
network_interface {
name = "${var.windows-vm-hostname}-network"
primary = true
ip_configuration {
name = "${var.windows-vm-hostname}-internal"
primary = true
subnet_id = azurerm_subnet.vm-subnet.id
}
}
extension {
name = "CustomScript"
publisher = "Microsoft.Compute"
type = "CustomScriptExtension"
type_handler_version = "1.10"
auto_upgrade_minor_version = true
settings = jsonencode({ "commandToExecute" = "powershell -command \" System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${base64encode(data.template_file.tf-script.rendered)}')) | Out-File -filepath setup.ps1\" && powershell -ExecutionPolicy Unrestricted -File setup.ps1" }) protected_settings = jsonencode({ "managedIdentity" = {} })
}
}

Update the source_image_reference on this section of the windows-vm-ss-main.tf. file, to configure the distro and version of Windows.

source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = var.windows-2019-sku
version = "latest"
}

Creating the Input Definition Variables File

In the last step, we are going to create input definition variables file terraform.tfvars and add the following code to the file:

# Network
network-vnet-cidr = "10.10.0.0/16"
vm-subnet-cidr = "10.10.1.0/24"
# Windows VM
windows-vm-hostname = "tfwinsrv" // Limited to 15 characters
windows-vm-size = "Standard_B2s"
windows-admin-username = "tfadmin"
windows-admin-password = "S3cr3ts24"
# Authentication
azure-subscription-id = "complete-this"
azure-client-id = "complete-this"
azure-client-secret = "complete-this"
azure-tenant-id = "complete-this"

The full code compatible for AzureRM v2.x is available at https://github.com/guillermo-musumeci/terraform-azure-windows-vm-scale-set

And that’s all folks. If you liked this story, please show your support by 👏 this story. Thank you for reading!

Certified AWS, Azure & GCP Architect | HashiCorp Ambassador | Terraform SME | KopiCloud Founder | Entrepreneur & Innovator | Book Author | Husband & Dad of ✌

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store