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:
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:
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:
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:
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:
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:
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:
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:
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"
}
}
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:
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:
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:
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:
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:
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.
# 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! :)