Multi-region Azure VM deployment with Terraform

There are many cases where you would want to deploy a number of VMs to multiple regions, particularly the region pairs like UK South and West, for a resilient application. In this post I’ll show how this can be achieved with some simple Terraform logic.

The clever bit is using the Terraform count argument with some maths and conditions to create multiple VMs and split them between the two regions. In summary this looks something like this, but read on for the detail.

1module "vm" {
2  count    = 8
3  name     = "MyVM-${count.index}"
4  location = count.index % 2 == 0 ? "uksouth" : "ukwest"

The Brief

Deploy multiple identical Virtual Machines into an Azure environment, split evenly between two Regions, for example “UK South” and “UK West”.

The Solution

The following Terraform will deploy a number of VMs evenly split between two regions. Here’s the full code, and I’ve broken it down later in this post to show how it works.

 1# Define Variables
 2locals {
 3  tenant_id        = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" #Tenant to Deploy to
 4  subscription_id  = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" #Subscription to deploy to
 5  rg_name          = "rg-VMDemo"                            #Resource Group to Create and fill with VMs
 6  primary_region   = "uksouth"                              #Primary Region
 7  secondary_region = "ukwest"                               #Secondary Region
 8  vm_count         = 4                                      #Number of VMs to Deploy
 9  vm_name_root     = "vm-VMDemo-"                           #Start of VM Name, will be suffixed with a number
10}
11
12# Setup Terraform
13provider "azurerm" {
14  tenant_id       = local.tenant_id
15  subscription_id = local.subscription_id
16  features {}
17}
18
19terraform {
20  required_providers {
21    azurerm = {
22      source  = "hashicorp/azurerm"
23      version = "=3.116.0"
24    }
25  }
26}
27
28# Resource group 
29resource "azurerm_resource_group" "rg" {
30  name     = local.rg_name
31  location = local.primary_region
32}
33
34# Virtual Machines
35module "vm" {
36  count               = local.vm_count #Deploy this module this many times
37  source              = "Azure/avm-res-compute-virtualmachine/azurerm"
38  version             = "0.18.0"
39  name                = "${local.vm_name_root}${count.index}" #Use the counter to identify VM
40  resource_group_name = azurerm_resource_group.rg.name
41  #Even Numbered VMs (0,2,....) get Primary Region, odd numbered VMs (1,3,....) get Secondary
42  location = count.index % 2 == 0 ? local.primary_region : local.secondary_region
43  zone     = null
44  network_interfaces = {
45    network_interface_1 = {
46      name = "${local.vm_name_root}${count.index}-nic"
47      ip_configurations = {
48        ip_configuration_1 = {
49          name = "ipconfig1"
50          #Use the VNET in the same region -vnet[0] is in primary, [1] in Secondary
51          private_ip_subnet_resource_id = module.vnet[count.index % 2].subnets["subnet1"].resource_id
52        }
53      }
54    }
55  }
56  sku_size = "Standard_B1ls" # Just to keep things small for this demo 
57}
58
59# Create virtual networks - One per region
60module "vnet" {
61  count               = 2 # One VNET for each Region
62  source              = "Azure/avm-res-network-virtualnetwork/azurerm"
63  version             = "0.7.1"
64  name                = "vnet-${count.index}"
65  resource_group_name = azurerm_resource_group.rg.name
66  #Use the same logic as VMs to determine Location
67  location      = count.index % 2 == 0 ? local.primary_region : local.secondary_region
68  address_space = ["10.0.0.0/24"]
69  subnets = {
70    subnet1 = {
71      name             = "subnet"
72      address_prefixes = ["10.0.0.0/24"]
73    }
74  }
75}

How it works

First up in the code we have some variables to define where to deploy and what this environment should look like.

Name Description
tenant_id The ID of the Tenant to Deploy to
subscription_id The ID of the Subscription to deploy to
rg_name The name of the Resource Group to Create and fill with VMs
primary_region The name of the Primary Azure Region
secondary_region The name of the Secondary Azure Region
vm_count The number of VMs to Deploy- this can be any positive number.
vm_name_root Start of VM Name, will be suffixed with a number to distinguish the VMs

The next section (provider, terraform, resource group) has all the bits to make Terraform work, and create a resource group to drop our VMs in. This is standard stuff, so I won’t describe it further here.

Now we get to the good bit- deploying the Virtual Machines! I’ve used Azure Verified Modules here for simplicity, but you could use regular AzureRM or AzureAPI calls in the same way.

1module "vm" {
2  count               = local.vm_count #Deploy this module this many times
3  source              = "Azure/avm-res-compute-virtualmachine/azurerm"
4  name                = "${local.vm_name_root}${count.index}" #Use the counter to identify VM
5  location = count.index % 2 == 0 ? local.primary_region : local.secondary_region

The count instruction tells Terraform to deploy that many copies of this module. We use the value of that count to give unique names to the Virtual Machines (VMDemo-0, VMDemo-1 and so on). Then in the location property we decide which region to place that VM in. Even Numbered VMs (VMDemo-0,VMDemo-2 etc) get put in the Primary Region (UK South in this example), but odd numbered VMs (VMDemo-1,VMDemo-3 etc) get placed in the Secondary Region (UK West).

To explain the logic: The value of count.index % 2 -the Modulus of the count value and 2- gives us a 0 for even numbers (divide an even number by 2 and the remainder is zero) and a 1 for odd numbers (divide an odd number by 2 and the remainder is 1). We then compare that value to zero to get a Boolean- True for even numbers, False for odd. That boolean condition then determines we get the Primary Region if True and the Secondary if false.

Note that the count.index value is zero-indexed, so if you specify the count as 4 you will get VMDemo-0 to VM-Demo3 created.

Finally, we need to deploy two VNETs- one for each Region - so the VMs have somewhere to put there Network Interfaces. We use the same location logic here as for the Virtual Machines, but only deploying 2 VNETs.

1module "vnet" {
2  count               = 2 # One VNET for each Region
3  source              = "Azure/avm-res-network-virtualnetwork/azurerm"
4  name                = "vnet-${count.index}"
5  location            = count.index % 2 == 0 ? local.primary_region : local.secondary_region

Hopefully this is useful to someone, and a useful starting point for a multi-region deployment.