Skip to main content

Terraform Azure VMs

Introduction

Azure Virtual Machines (VMs) are one of the most fundamental and widely used resources in Microsoft Azure. They provide on-demand, scalable computing resources that allow you to run your applications and workloads in the cloud. When combined with Terraform, you can automate the creation, configuration, and management of your Azure VMs in a consistent and repeatable way.

In this guide, we'll explore how to use Terraform to provision and manage Azure Virtual Machines. You'll learn how to define VM configurations as code, handle dependencies, and implement best practices for VM deployment in Azure.

Prerequisites

Before we begin, make sure you have:

  • Terraform installed (version 1.0.0 or later)
  • Azure CLI installed and configured
  • A basic understanding of Azure and Terraform concepts

Basic Azure VM Provisioning

Let's start with a simple example of creating an Azure VM using Terraform.

Step 1: Set up the Azure Provider

First, we need to configure the Azure provider in our Terraform configuration:

hcl
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}

provider "azurerm" {
features {}
}

Step 2: Create a Resource Group

Every Azure resource needs to be part of a resource group:

hcl
resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "East US"
}

Step 3: Create a Virtual Network and Subnet

Before creating a VM, we need networking components:

hcl
resource "azurerm_virtual_network" "example" {
name = "example-network"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
}

resource "azurerm_subnet" "example" {
name = "internal"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.2.0/24"]
}

Step 4: Create a Network Interface

Each VM needs at least one network interface:

hcl
resource "azurerm_network_interface" "example" {
name = "example-nic"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name

ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.example.id
private_ip_address_allocation = "Dynamic"
}
}

Step 5: Create the Virtual Machine

Now we can create the VM itself:

hcl
resource "azurerm_linux_virtual_machine" "example" {
name = "example-vm"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
size = "Standard_B1s"
admin_username = "adminuser"
network_interface_ids = [
azurerm_network_interface.example.id,
]

admin_ssh_key {
username = "adminuser"
public_key = file("~/.ssh/id_rsa.pub")
}

os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}

source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "18.04-LTS"
version = "latest"
}
}

Complete Example

Here's the complete Terraform configuration:

hcl
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}

provider "azurerm" {
features {}
}

resource "azurerm_resource_group" "example" {
name = "example-resources"
location = "East US"
}

resource "azurerm_virtual_network" "example" {
name = "example-network"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
}

resource "azurerm_subnet" "example" {
name = "internal"
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.2.0/24"]
}

resource "azurerm_network_interface" "example" {
name = "example-nic"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name

ip_configuration {
name = "internal"
subnet_id = azurerm_subnet.example.id
private_ip_address_allocation = "Dynamic"
}
}

resource "azurerm_linux_virtual_machine" "example" {
name = "example-vm"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
size = "Standard_B1s"
admin_username = "adminuser"
network_interface_ids = [
azurerm_network_interface.example.id,
]

admin_ssh_key {
username = "adminuser"
public_key = file("~/.ssh/id_rsa.pub")
}

os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}

source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "18.04-LTS"
version = "latest"
}
}

Apply the Configuration

To create the VM, run:

bash
terraform init
terraform plan
terraform apply

Output:

Apply complete! Resources: 5 added, 0 changed, 0 destroyed.

Outputs:
vm_id = "..."

Customizing Azure VMs

Let's explore some ways to customize your Azure VMs with Terraform.

Windows VM Example

Creating a Windows VM is similar but has some differences:

hcl
resource "azurerm_windows_virtual_machine" "example" {
name = "example-windows-vm"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
size = "Standard_D2s_v3"
admin_username = "adminuser"
admin_password = "P@$$w0rd1234!"
network_interface_ids = [
azurerm_network_interface.example.id,
]

os_disk {
caching = "ReadWrite"
storage_account_type = "Standard_LRS"
}

source_image_reference {
publisher = "MicrosoftWindowsServer"
offer = "WindowsServer"
sku = "2019-Datacenter"
version = "latest"
}
}
warning

Never hardcode passwords in your Terraform configuration files. Use variables or secrets management solutions instead.

VM Extensions

You can configure VM extensions to perform post-deployment configurations:

hcl
resource "azurerm_virtual_machine_extension" "example" {
name = "CustomScriptExtension"
virtual_machine_id = azurerm_linux_virtual_machine.example.id
publisher = "Microsoft.Azure.Extensions"
type = "CustomScript"
type_handler_version = "2.0"

settings = <<SETTINGS
{
"commandToExecute": "apt-get update && apt-get install -y nginx"
}
SETTINGS
}

Custom Data for Bootstrap Scripts

You can provide custom data to run scripts during VM initialization:

hcl
resource "azurerm_linux_virtual_machine" "example" {
# Other configuration remains the same

custom_data = base64encode(<<-EOF
#!/bin/bash
echo "Hello, World!" > /tmp/hello.txt
apt-get update
apt-get install -y nginx
systemctl enable nginx
systemctl start nginx
EOF
)
}

Working with VM Scale Sets

For scalable applications, you might want to use VM Scale Sets instead of individual VMs:

hcl
resource "azurerm_linux_virtual_machine_scale_set" "example" {
name = "example-vmss"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
sku = "Standard_B1s"
instances = 2
admin_username = "adminuser"

admin_ssh_key {
username = "adminuser"
public_key = file("~/.ssh/id_rsa.pub")
}

source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "18.04-LTS"
version = "latest"
}

os_disk {
storage_account_type = "Standard_LRS"
caching = "ReadWrite"
}

network_interface {
name = "example"
primary = true

ip_configuration {
name = "internal"
primary = true
subnet_id = azurerm_subnet.example.id
}
}
}

Variables and Outputs

To make your configuration more flexible, use variables:

hcl
variable "vm_size" {
description = "Size of the VM"
type = string
default = "Standard_B1s"
}

variable "admin_username" {
description = "Administrator username for the VM"
type = string
default = "adminuser"
}

variable "location" {
description = "Azure region to deploy resources"
type = string
default = "East US"
}

And outputs to expose important information:

hcl
output "vm_id" {
description = "ID of the created virtual machine"
value = azurerm_linux_virtual_machine.example.id
}

output "vm_public_ip" {
description = "Public IP address of the virtual machine"
value = azurerm_public_ip.example.ip_address
}

Real-World Example: Web Server Farm

Let's create a more complex example: a web server farm with load balancing.

hcl
# Resource Group
resource "azurerm_resource_group" "webfarm" {
name = "webfarm-resources"
location = "West Europe"
}

# Virtual Network
resource "azurerm_virtual_network" "webfarm" {
name = "webfarm-network"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.webfarm.location
resource_group_name = azurerm_resource_group.webfarm.name
}

# Subnet
resource "azurerm_subnet" "webfarm" {
name = "internal"
resource_group_name = azurerm_resource_group.webfarm.name
virtual_network_name = azurerm_virtual_network.webfarm.name
address_prefixes = ["10.0.2.0/24"]
}

# Public IP for Load Balancer
resource "azurerm_public_ip" "webfarm" {
name = "webfarm-lb-pip"
location = azurerm_resource_group.webfarm.location
resource_group_name = azurerm_resource_group.webfarm.name
allocation_method = "Static"
sku = "Standard"
}

# Load Balancer
resource "azurerm_lb" "webfarm" {
name = "webfarm-lb"
location = azurerm_resource_group.webfarm.location
resource_group_name = azurerm_resource_group.webfarm.name
sku = "Standard"

frontend_ip_configuration {
name = "PublicIPAddress"
public_ip_address_id = azurerm_public_ip.webfarm.id
}
}

# Backend Address Pool
resource "azurerm_lb_backend_address_pool" "webfarm" {
loadbalancer_id = azurerm_lb.webfarm.id
name = "BackEndAddressPool"
}

# Health Probe
resource "azurerm_lb_probe" "webfarm" {
loadbalancer_id = azurerm_lb.webfarm.id
name = "http-probe"
protocol = "Http"
request_path = "/"
port = 80
}

# Load Balancing Rule
resource "azurerm_lb_rule" "webfarm" {
loadbalancer_id = azurerm_lb.webfarm.id
name = "http"
protocol = "Tcp"
frontend_port = 80
backend_port = 80
frontend_ip_configuration_name = "PublicIPAddress"
backend_address_pool_ids = [azurerm_lb_backend_address_pool.webfarm.id]
probe_id = azurerm_lb_probe.webfarm.id
}

# VM Scale Set
resource "azurerm_linux_virtual_machine_scale_set" "webfarm" {
name = "webfarm-vmss"
resource_group_name = azurerm_resource_group.webfarm.name
location = azurerm_resource_group.webfarm.location
sku = "Standard_B1s"
instances = 2
admin_username = "adminuser"

admin_ssh_key {
username = "adminuser"
public_key = file("~/.ssh/id_rsa.pub")
}

source_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "18.04-LTS"
version = "latest"
}

os_disk {
storage_account_type = "Standard_LRS"
caching = "ReadWrite"
}

network_interface {
name = "webfarm-nic"
primary = true

ip_configuration {
name = "internal"
primary = true
subnet_id = azurerm_subnet.webfarm.id
load_balancer_backend_address_pool_ids = [azurerm_lb_backend_address_pool.webfarm.id]
}
}

custom_data = base64encode(<<-EOF
#!/bin/bash
apt-get update
apt-get install -y nginx
echo "Hello from Terraform-created VM $(hostname)" > /var/www/html/index.html
systemctl enable nginx
systemctl start nginx
EOF
)
}


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)