Learn Terraform and deploy Azure resource via Azure DevOps pipeline (Part 3)

Part 3: Deploy Azure Virtual Machine using Terraform modules

Rohan Islam
8 min readJul 28, 2020

So far we have learned basic terraform in Part 1 and terraform module in Part 2 and we have already created a resource group and a virtual network. Now I am planning to deploy an Azure virtual machine (VM) using terraform modules and see how the output of a module is called from the root module.

Target architecture

Target architecture

Components of a virtual machine

Before we proceed with the coding, let’s first understand the components of a virtual machine that we need to provision on Azure.

We already have a resource group and a virtual network created on our subscription. So we will put our VM within the existing resource group and on the subnet of our existing virtual network.

In order to provision a virtual machine, we at least need to create a network interface card (nic) and a virtual machine resource including OS disk.

Our target is to create modules for each of these components and call them from main or root module to provision the virtual machine.

Create modules

We are going to create two modules — network-interface and virtual-machine. By now we know what are the files we need to create a module. So, we are going to create the following folder structure and create the respective files in there.

rohan@K42N:~/Documents/tfdemo$ tree --dirsfirst
.
└── azuredeploy
├── modules
│ ├── network-interface
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── README.md
│ │ └── variables.tf
│ ├── virtual-machine
│ │ ├── main.tf
│ │ ├── outputs.tf
│ │ ├── README.md
│ │ └── variables.tf

│ └── virtual-network
│ ├── main.tf
│ ├── outputs.tf
│ ├── README.md
│ └── variables.tf
├── main.tf
├── terraform.tfvars
└── variables.tf

Network interface module

The module to deploy network interface card is kept within the folder called network-interface. We will now see how the module files look like here.

# main.tf file of network-interface moduleresource "azurerm_network_interface" "nic" {
name = "${var.vmname}-nic-01"
location = var.location
resource_group_name = var.resource_group_name
ip_configuration {
name = "ipconfiguration1"
subnet_id = var.subnet_id
private_ip_address_allocation = "Dynamic"
}
}

In the main.tf file we have the network interface resource and it’s attributes. Notice that how I am declaring the name of the nic by concatenating the VM name and a string “-nic-01”. For rest of the attributes I am using variables.

# variables.tf file of network-interface modulevariable "vmname" {
type = string
description = "name of the vm"
}
variable "location" {
type = string
description = "Azure location"
}
variable "resource_group_name" {
type = string
description = "name of the resource group"
}
variable "subnet_id" {
type = string
description = "id of the subnet"
}

As we already know, I have declared all the variables in the variables.tf file that I used in the main.tf.

# outputs.tf file of network-interface moduleoutput "nic_id" {
description = "id of the network interface"
value = azurerm_network_interface.nic.id
}

I am capturing the nic id as an output of this module in the outputs.tf file as I am gonna need the nic id to create the VM.

Virtual machine module

The module to deploy a VM resource including OS disk is kept under virtual-machine folder. We will now see how the module files look like here.

# main.tf file of virtual-machine moduleresource "azurerm_windows_virtual_machine" "vm" {
name = var.vmname
resource_group_name = var.resource_group_name
location = var.location
size = var.vm_size
admin_username = var.admin_usename
admin_password = var.admin_password
network_interface_ids = var.network_interface_ids
os_disk {
name = "${var.vmname}-os-disk-01"
caching = "ReadWrite"
storage_account_type = var.os_disk_type
}
source_image_reference {
publisher = var.image_publisher
offer = var.image_offer
sku = var.image_sku
version = "latest"
}
}

The main.tf file contain windows vm resource and it’s attributes. I am concatenating a string “-os-disk-01” with the variable “vmname” to declare the OS disk name in the same way as I did for nic name.

# variables.tf file of virtual-machine modulevariable "vmname" {
type = string
description = "The name of the virtual machine"
}
variable "resource_group_name" {
type = string
description = "The name of resource group"
}
variable "location" {
type = string
description = "Azure location "
}
variable "network_interface_ids" {
type = list(string)
description = "network interface id"
}
variable "vm_size" {
type = string
description = "size of the virtual machine"
}
variable "os_disk_type" {
type = string
description = "type of the os disk. example Standard_LRS"
}
variable "admin_usename" {
type = string
description = "local admin user of the virtual machine"
}
variable "admin_password" {
type = string
description = "password of the local admin user"
}
variable "image_publisher" {
type = string
description = "Azure image publisher"
}
variable "image_offer" {
type = string
description = "Azure image offer"
}
variable "image_sku" {
type = string
description = "Azure image sku"
}

The variables.tf file contains all the variables that I used in the main.tf of the virtual machine module. Notice the type of the variable “network_interface_ids”, this has been declared as an array of strings as the VM resource expect the value of nic id in the form of an array.

# outputs.tf file of virtual-machine moduleoutput "vm_id" {
description = "id of the fileshare"
value = azurerm_windows_virtual_machine.vm.id
}

I am capturing the vm id in the outputs.tf file of this module. Though I am not gonna need the vm id to create a VM but I may need it later.

Preparing main and calling the modules

Now that we have our modules ready, we have to update our main files in order to call these modules to provision a windows vm. We are gonna use the same main files that we used to provision the virtual network. Let’s see how the files look like.

# main.tfterraform {
backend "azurerm" {}
}
provider "azurerm" {
version = "=2.0.0"
features {}
}
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.location
}
module "virtual-network" {
source = "./modules/virtual-network"
virtual_network_name = var.virtual_network_name
resource_group_name = var.resource_group_name
location = var.location
virtual_network_address_space = var.virtual_network_address_space
subnet_name = var.subnet_name
subnet_address_prefix = var.subnet_address_prefix
}
module "network-interface" {
source = "./modules/network-interface"
vmname = var.vmname
location = var.location
resource_group_name = var.resource_group_name
subnet_id = module.virtual-network.subnet_id
}
module "virtual-machine" {
source = "./modules/virtual-machine"
vmname = var.vmname
location = var.location
resource_group_name = var.resource_group_name
network_interface_ids = [module.network-interface.nic_id]
vm_size = var.vm_size
os_disk_type = var.os_disk_type
admin_usename = var.admin_usename
admin_password = var.admin_password
image_publisher = var.image_publisher
image_offer = var.image_offer
image_sku = var.image_sku
}

In the main.tf file we already had the “resource group” resource and we called the “virtual-network” module before when we deployed the vnet. Now, I am calling network-interface and virtual-machine modules from the same main.

Under network-interface module take a close look at the syntax of calling a string type output of a module module.<module name>.<declared output name>. Here I am calling the subnet_id output of virtual-network module and passing the value to network-interface module.

In the virtual-machine module I am calling the output of network-interface module and passing the value to virtual-machine module. Here the output is the nic_id which is of type array.

# variables.tfvariable "resource_group_name" {
type = string
description = "resource group name of the virtual network"
}
variable "location" {
type = string
description = "location of the virtual network"
}
variable "virtual_network_name" {
type = string
description = "name of the virtual network"
}
variable "virtual_network_address_space" {
type = list(string)
description = "address space of the virtual network"
}
variable "subnet_name" {
type = string
description = "name of the subnet"
}
variable "subnet_address_prefix" {
type = string
description = "address prefix of the subnet"
}
variable "vmname" {
type = string
description = "name of the vm"
}
variable "vm_size" {
type = string
description = "size of the virtual machine"
}
variable "os_disk_type" {
type = string
description = "type of the os disk. example Standard_LRS"
}
variable "admin_usename" {
type = string
description = "local admin user of the virtual machine"
}
variable "admin_password" {
type = string
description = "password of the local admin user"
}
variable "image_publisher" {
type = string
description = "Azure image publisher"
default = "MicrosoftWindowsServer"
}
variable "image_offer" {
type = string
description = "Azure image offer"
default = "WindowsServer"
}
variable "image_sku" {
type = string
description = "Azure image sku"
default = "2016-Datacenter"
}

Now I have updated the existing variables.tf file to add all the variables that I used in the main file. Here I have shown how we can specify a default value of a variable. If we specify a default value of a variable in variables.tf file and we do not assign any value to this variable in terraform.tfvars file, the default value will be picked up by terraform while deploying.

resource_group_name = "terrform-demo-rg"
location = "westeurope"
virtual_network_name = "tfdemo-vnet-01"
virtual_network_address_space = ["172.16.0.0/16"]
subnet_name = "subnet-01"
subnet_address_prefix = "172.16.0.0/24"
vmname = "tfdemovm001"
vm_size = "Standard_D2_v3"
os_disk_type = "Standard_LRS"
admin_usename = "**************"
admin_password = "****************"

I have updated the existing terraform.tfvars file to assign values against all other variables that I used in main. However, notice that I have not assigned any value against the variables for which I have assigned default value in variables.tf, so in this case those default values will be picked up. Please use username and password as per your own choice.

Deployment

At this stage we have our code ready to provision a windows virtual machine on Azure. We already know how to do the following steps, so I am not going to explain them in details again.

Code commit

Commit the updated code on the local git repo.

Push code to Azure DevOps repo

Now push the updated code to Azure DevOps repo.

Run Azure DevOps build pipeline

Run the existing build pipeline to drop the updated code in Artifacts.

Run Azure DevOps release pipeline

Once the build completes successfully, create a release to trigger the existing release pipeline.

Once the release completes successfully, we can go to Azure portal and see the VM is created within the existing resource group and it has got an ip address from the existing subnet.

Azure portal

Next steps:

  • Practice deploying more Azure resources using terraform
  • Please read Part 4 to continue with the learning.

Thanks for reading, give it a 👏 if you like it. Please leave a comment and let me know if you have any feedback.

--

--

Rohan Islam

Cloud Architect | Continuous learner | Passionate about technologies