Azure Terraform: Part 10 – Deploy a Windows VM

Reading Time: 6 minutes

As we approach the end of in our journey, we will unveil the steps to deploying an Azure Virtual Machine using Terraform. We will build on the infrastructure deployed in our previous outings,

and deploy a Windows Server VM using infrastructure as code.

As part of the deployment we will deploy a Public IP resource and a Network Interface resource.

Public IP

In Azure a public IP is a configurable resource that can be created independently of the network interface. Once created the public IP address can be associated with different Azure resources,

  • VM Network Interface
  • VM Scale Sets
  • Public Load Balancers
  • Virtual Network Gateways
  • Azure Firewall

This is not a complete list, bit it gives you an idea of the type of resources a Public IP address can be associated with. Keep in mind the Public IP addresses resource can only be associated with one resource at a time.

The following code block details the code we will use to deploy a public IP.

resource "azurerm_public_ip" "ftpip" {
  name                = "ftpip01"
  location            = var.location
  resource_group_name = azurerm_resource_group.ftrg001.name
  allocation_method   = "Dynamic"

  tags = {
    environment = "dev"
  }

}

Detailed configuration options when creating a Azure Public IP address can be found in the Terraform documentation here.

In our code base we are using the variable var.location to define what location the resource will be created in.

allocation_method is set to dynamic not static in our example.

Network Interface

The code block below details the code we will use to define a network interface card that we can attach to our VM. The network interface resource will use the public IP address resource we defined by referencing the name.id of the public IP resource,

  • azurerm_public_ip.ftpip.id
resource "azurerm_network_interface" "tfnetint01" {
  name                = "tfnetint01"
  location            = "uksouth"
  resource_group_name = azurerm_resource_group.ftrg001.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.ftsubnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.ftpip.id
  }

  tags = {
    environment = "dev"
  }
}

The Terraform documentation for creating an Azure Network Interface is located here.

Windows Server VM Code

We will be using the code defined in the code block below for deploying our Windows 2019 Server,

resource "azurerm_windows_virtual_machine" "tfvm01" {
  name                  = "tfvm01"
  resource_group_name   = azurerm_resource_group.ftrg001.name
  location              = "uksouth"
  size                  = "Standard_B1s"
  admin_username        = "azuradmin"
  admin_password        = var.admin_password
  network_interface_ids = [azurerm_network_interface.tfnetint01.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2019-Datacenter"
    version   = "latest"
  }
  tags = {
    environment = "dev"
  }

}

It is not best practice to define password or secrets in your code. As we our learning I have added the following variable admin_password to the variables file and referenced it in the code.

For the variable no default has not been defined. This will require you to enter a password into the terminal when running terraform plan or apply.

We will cover how to handle secrets in future posts.

variable "admin_password" {
  type        = string
  description = "value for admin_password"

}

In Visual Studio Code copy all blocks to your main.tf file,

From the terminal run,

  • terraform init
  • terraform validate

Your main.tf file should now read as follows,

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "3.82.0"
    }
  }
}

provider "azurerm" {
  features {}
}

locals {
  tags = {
    environment = "dev"
  }
}

resource "azurerm_resource_group" "ftrg001" {
  name     = "FT23-RG-001"
  location = var.location

  tags = {
    environment = "dev"
  }
}

resource "azurerm_virtual_network" "ftvnet" {
  name                = "ftvnet01"
  address_space       = ["10.10.0.0/16"]
  location            = var.location
  resource_group_name = azurerm_resource_group.ftrg001.name

  tags = {
    environment = "dev"
  }

}

resource "azurerm_subnet" "ftsubnet" {
  name                 = "ftsubnet01"
  resource_group_name  = azurerm_resource_group.ftrg001.name
  virtual_network_name = azurerm_virtual_network.ftvnet.name
  address_prefixes     = ["10.10.0.0/24"]
}

resource "azurerm_network_security_group" "ftnsg01" {
  name                = "ft-test-nsg01"
  location            = var.location
  resource_group_name = azurerm_resource_group.ftrg001.name

  security_rule = [
    {
      name                                       = "SSH"
      priority                                   = 1001
      direction                                  = "Inbound"
      access                                     = "Allow"
      protocol                                   = "Tcp"
      source_port_range                          = "*"
      destination_port_range                     = "22"
      source_address_prefix                      = "*"
      destination_address_prefix                 = "*"
      description                                = "Allow SSH"
      destination_address_prefixes               = null
      source_application_security_group_ids      = null
      source_port_ranges                         = null
      destination_application_security_group_ids = null
      destination_port_ranges                    = null
      source_address_prefixes                    = null
    },
    {
      name                                       = "RDP"
      priority                                   = 1002
      direction                                  = "Inbound"
      access                                     = "Allow"
      protocol                                   = "Tcp"
      source_port_range                          = "*"
      destination_port_range                     = "3389"
      source_address_prefix                      = "*"
      destination_address_prefix                 = "*"
      description                                = "Allow RDP"
      destination_address_prefixes               = null
      source_application_security_group_ids      = null
      source_port_ranges                         = null
      destination_application_security_group_ids = null
      destination_port_ranges                    = null
      source_address_prefixes                    = null
    }
  ]


  tags = {
    environment = "dev"
  }
}

resource "azurerm_subnet_network_security_group_association" "tfnsg-tfsubnet01" {
  subnet_id                 = azurerm_subnet.ftsubnet.id
  network_security_group_id = azurerm_network_security_group.ftnsg01.id

}

resource "azurerm_public_ip" "ftpip" {
  name                = "ftpip01"
  location            = var.location
  resource_group_name = azurerm_resource_group.ftrg001.name
  allocation_method   = "Dynamic"

  tags = {
    environment = "dev"
  }

}


resource "azurerm_network_interface" "tfnetint01" {
  name                = "tfnetint01"
  location            = "uksouth"
  resource_group_name = azurerm_resource_group.ftrg001.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.ftsubnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.ftpip.id
  }

  tags = {
    environment = "dev"
  }
}

resource "azurerm_windows_virtual_machine" "tfvm01" {
  name                  = "tfvm01"
  resource_group_name   = azurerm_resource_group.ftrg001.name
  location              = "uksouth"
  size                  = "Standard_B1s"
  admin_username        = "azuradmin"
  admin_password        = var.admin_password
  network_interface_ids = [azurerm_network_interface.tfnetint01.id]
  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }
  source_image_reference {
    publisher = "MicrosoftWindowsServer"
    offer     = "WindowsServer"
    sku       = "2019-Datacenter"
    version   = "latest"
  }
  tags = {
    environment = "dev"
  }

}

Terraform validate should return the configuration is valid,

  • terraform plan

If you have been following along in this series you should get the return message to add 3 resources

  • Public IP
  • Network Interface
  • Windows Virtual Machine

If there are no errors run terraform apply

Once successfully completed the terminal should return the message

Run the following command to confirm the VM has been successfully created

  • az vm list –output table

To confirm your VM has been allocated both a public and private IP address run the following command

  • az vm list-ip-addresses –output table

In part 5 of our Azure Terraform journey we created an NSG with an inbound allow rule for RDP 3389. We can test this by trying to connect to our VM using its public IP address,

Enter the username and password you defined in the code block when creating the VM

You should successfully login to the VM

In Conclusion

As the curtain falls on our magical journey, remember Terraform allows you to craft your environment with precision. The script that we have created is a basic introduction to Terraform, covering some of Terraforms key concepts to help you get started on your journey.

We also deployed the basic infrastructure required before you can deploy and IaaS VM in Azure,

  • Resource group
  • Subnet
  • VNET

And for fun we deployed an NSG with a few rules defined.

Remember, running services in Azure incur a cost. Now that we have finished with our test environment it is time to clean up. You can destroy everything you have created by running the terraform destroy command,

Terraform destroy will create a plan detailing all resources to be destroyed review before confirming.

It will as you to confirm before destroying any resource.

For further reading on the destroy command refer to the Terraform documentaton here.

Hope you have enjoyed this this introduction and it has inspired you to continue your Terraform journey. Thank you for reading.