Terraform is an open-source Infrastructure as a service (IaaC) tool, mainly used to provision and configure infrastructure in the various cloud platforms. It supports AWS, Microsoft Azure and GCP cloud platform. One of the main advantages of Terraform is, it does remember the previous state of the platform.
In this article, I will demonstrate how to provision multiple Azure cloud virtual machines with Terraform.
Pre-requisites
To install terraform on Mac book or Ubuntu please refer below document
Visual Studio helps to write the codes in Terraform format. To install Visual Studio on Mac-book follow the below documentation.
https://tutorials.visualstudio.com/vs4mac-install/install
Once the Visual Studio installation is completed. Open it to install the Terraform extension. Click extension icon from the left side panel and search for Terraform, it shows all available extensions, choose the Terraform 1.4.0 version and click install button to install it.
Connect to Azure
All the pre-requisites are ready, next, connect to the Azure cloud with the help of the “az login” command. Enter the “az login” command in the terminal and select your account from the browser. Once you added your credentials, you can see all the subscription details in the terminal.
MacBook-Pro:techies-terra $ az login
You have logged in. Now let us find all the subscriptions to which you have access...
[
{
"cloudName": "AzureCloud",
"homeTenantId": "beff-988fe69e74f2",
"id": "9fb8-a9553bcc4930",
"isDefault": true,
"managedByTenants": [],
"name": "Free Trial",
"state": "Enabled",
"tenantId": "beff-988fe69e74f2",
"user": {
"name": "r@outlook.com",
"type": "user"
}
}
]
Start Coding: main.tf
To provision the Azure resource we will create 4 files. The first file is main.tf which we used to create a Resource Group. Open a file in Visual Studio and save it as main.tf file ( I created it in a folder called techies-terra). You may be noticed the file syntax is now changed to Terraform (A terraform icon is present in the file). Open the files and add the below code.
provider "azurerm" {
version = "1.27.0"
}
resource "azurerm_resource_group" "azure_rg" {
name = var.rgname
location = var.location
}
Here the provider (cloud) name is Azure and Terraform code version for this provider is 1.27.0 you can get the latest version form the Terraform documentation.
Next, we are provisioning first resource which Resource Group so resource type is “azurerm_resource_group” and the name for Terraform representation is “azure_rg”. Note that here the name “azure_rg” is used by Terraform for maping and it is not the name of our Resource group.
In the curly braces, I have added the arguments which are name and location as a variable because we will use these arguments multiple times in the entire Terraform code.
Declare variable
Create another file named variable.tf. This file we will use to declare all the variables which we used for this project. Open the file and add the below variables and save it.
variable "rgname" {
description = "resource grouop name"
default = "DevOps_Techies"
}
variable "location" {
description = "location name"
default = "East Us"
}
Here we have declared two variable one is “rgname” and its default value is “DevOps_Techies”. The variable is named location and value is “East Us”. Terraform will provision a Resource Group with the name “DevOps_Techies” in East Us location.
Go to the terminal where you saved the file and execute below command to initiate Terraform. You can execute command directly from the Visual studio but I prefer to do it from the command line.
MacBook-Pro:techies-terra $ ls
main.tf variable.tf
MacBook-Pro:techies-terra $ terraform init
Initializing the backend...
Initializing provider plugins...
- Checking for available provider plugins...
- Downloading plugin for provider "azurerm" (hashicorp/azurerm) 1.27.0...
Terraform has been successfully initialized!
Next, execute the “terraform plan” command, it will let us know what all are the changes that terraform is going to make and if there any error in our code.
MacBook-Pro:techies-terra $ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be persisted to local or remote state storage.
----------------------------------------------------------------------
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# azurerm_resource_group.azure_rg will be created
+ resource "azurerm_resource_group" "azure_rg" {
+ id = (known after apply)
+ location = "eastus"
+ name = "DevOps_Techies"
+ tags = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Terraform will create the mentioned resources under the subscription, to start provision, trigger “terraform apply” command.
MacBook-Pro:techies-terra $ terraform apply
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
+ create
Terraform will perform the following actions:
# azurerm_resource_group.azure_rg will be created
+ resource "azurerm_resource_group" "azure_rg" {
+ id = (known after apply)
+ location = "eastus"
+ name = "DevOps_Techies"
+ tags = (known after apply)
}
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
azurerm_resource_group.azure_rg: Creating...
azurerm_resource_group.azure_rg: Creation complete after 6s [id=/subscriptions/9fb8-a9553bcc4930/resourceGroups/DevOps_Techies]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
To confirm the Resource Group is created, login to the Azure portal and select Resource groups. It may take time to reflect on the portal.
Network for Resource Group
To create a virtual machine, few components like VNET, subnet, NSG, Public IP and NIC are required. To provision all these component I will create another file called network.tf. Open this file and add the below code.
# Virtual Network
resource "azurerm_virtual_network" "vnet" {
name = var.vnet_name
address_space = var.address_space
location = var.location
resource_group_name = var.rgname
}
# Subnet for virtual machine
resource "azurerm_subnet" "vmsubnet" {
name = var.subnet_name
address_prefix = var.address_prefix
virtual_network_name = var.vnet_name
resource_group_name = var.rgname
}
Add hash (#) at the beginning to add a comment for the code. The first code block provision a vnet and second block provision a subnet inside the vnet. Use variable.tf file to declare all the variables that we used here. Open the variable.tf file and append the below codes.
variable "vnet_name" {
description = "name for vnet"
default = "techies_vnet"
}
variable "address_space" {
default = ["10.0.0.0/16"]
}
variable "subnet_name" {
default = "public_subnet"
}
variable "address_prefix" {
default = "10.0.1.0/24"
}
I haven’t added the description parameter with all variables. If required you can do this.
Next, we will append the below code, which will create a public IP address and NIC.
# Add a Public IP address
resource "azurerm_public_ip" "vmip" {
count = var.number
name = "vm-ip-${count.index}"
resource_group_name = var.rgname
allocation_method = "Static"
location = var.location
}
# Add a Network security group
resource "azurerm_network_security_group" "nsgname" {
name = "vm-nsg"
location = var.location
resource_group_name = var.rgname
security_rule {
name = "PORT_SSH"
priority = 101
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "22"
source_address_prefixes = var.external_ip
destination_address_prefix = "*"
}
}
Here, I have added two variable which is “var.number” and “var.external_ip. The var.number is used for how many same resources we need. If we need to provision multiple virtual machines or multiple IP addresses, then the count can declare as “var.numbercount” (note: you can use any name here except count, it is a deserved word).
The “count.index” used for increment, for example, to provision two virtual machines we can mention the variable var.numbercount=2 then terraform need two names this can be achieved by count.index. The above code we have given IP address name as “vm-ip-${count.index}”, so if we set var.number=2 the first name will “vm-ip-0” and second name will be “vm-ip-1”
The “var.external_ip” variable is used to add a source IP address (you can add your router IP address). Append the below code to variable.tf file
variable "external_ip" {
type = list(string)
default = ["your public ip to allow traffic to server"]
}
variable "numbercount" {
type = number
default = 1
}
Here, I have given the variable name as “numbercount” and type = number and value =1, so it will create resources only one time. As we performed execute terraform plan to check any error for our code, if not execute “terraform apply” command to provision the resources.
Once the Terraform has completed the execution, login to the Azure portal and confirm all resources are created successfully.
As I informed earlier the IP address created in the name vm-ip0, zero is the count.index value. Still we haven’t completed the required network for the Virtual machine, for this we need to associate Network Security Group (NSG) with subnet and IP address associate with NIC. Add the below code to complete this task.
#Associate NSG with subnet
resource "azurerm_subnet_network_security_group_association" "nsgsubnet" {
subnet_id = azurerm_subnet.vmsubnet.id
network_security_group_id = azurerm_network_security_group.nsgname.id
}
# NIC with Public IP Address
resource "azurerm_network_interface" "terranic" {
count = var.numbercount
name = "vm-nic-${count.index}"
location = var.location
resource_group_name = var.rgname
ip_configuration {
name = "external"
subnet_id = azurerm_subnet.vmsubnet.id
private_ip_address_allocation = "Dynamic"
public_ip_address_id = element(azurerm_public_ip.vmip.*.id, count.index)
}
}
Here, we haven’t added any new variable so we are good to run terraform “plan” and “apply”.
Once the terraform completed the activity, log in to the portal and make sure that NSG is associated with the subnet.

Create Virtual Machine
The complete network for the Virtual machine is ready, next we will add code for the VM. The VM will be created under the vnet and the subnet. It uses the IP address which we created recently.
To create a VM we need additional parameters like OS disk, Data Disk, OS image, and profile details. All this detail will include in a single file. Create a new file named instance.tf and add the below code.
#Data Disk for Virtual Machine
resource "azurerm_managed_disk" "datadisk" {
count = var.numbercount
name = "datadisk_existing_${count.index}"
location = var.location
resource_group_name = var.rgname
storage_account_type = "Standard_LRS"
create_option = "Empty"
disk_size_gb = "50"
}
#Aure Virtual machine
resource "azurerm_virtual_machine" "terravm" {
name = "vm-stg-${count.index}"
location = var.location
resource_group_name = var.rgname
count = var.numbercount
network_interface_ids = [element(azurerm_network_interface.terranic.*.id, count.index)]
vm_size = "Standard_B1ls"
delete_os_disk_on_termination = true
delete_data_disks_on_termination = true
storage_os_disk {
name = "osdisk-${count.index}"
caching = "ReadWrite"
create_option = "FromImage"
managed_disk_type = "Premium_LRS"
disk_size_gb = "30"
}
storage_data_disk {
name = element(azurerm_managed_disk.datadisk.*.name, count.index)
managed_disk_id = element(azurerm_managed_disk.datadisk.*.id, count.index)
create_option = "Attach"
lun = 1
disk_size_gb = element(azurerm_managed_disk.datadisk.*.disk_size_gb, count.index)
}
storage_image_reference {
publisher = "Canonical"
offer = "UbuntuServer"
sku = "16.04-LTS"
version = "latest"
}
os_profile {
computer_name = "hostname"
admin_username = "techies"
}
os_profile_linux_config {
disable_password_authentication = true
ssh_keys {
path = "/home/techies/.ssh/authorized_keys"
key_data = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCt1Pp6K2zEiK2+9X672/Yx0rFaoVprB2Khs5VowAM1HEVg3axcwXZwEeIsZISyLx/vAOkUDrv8cD6J+1EUwEGWaiKuCC7xjcpwFH2EiMoTUGGrgNGBfUWQNMbClTLcGI7lP0bcJMcKuzcKPLaISTxSyuIiKot+9SHSmSuxj+gNpP01Y0aPzcMWZVfbJ/N6B+zPsM+cpvwh rrabab-external-2020-03-03"
}
}
connection {
host = azurerm_public_ip.admingwip.id
user = "techies"
type = "ssh"
private_key = "${file("./id_rsa")}"
timeout = "1m"
agent = true
}
}
- azurerm_managed_disk: Additional disk for the VM
- azurerm_virtual_machine: VM size, NIC and IP details
- storage_os_disk: Disk with the OS and it’s type and size.
- storage_image_reference: OS image details
- os_profile: Server hostname and login user.
- os_profile_linux_config: Changes inside the Operating system,
- connection: Details to connect the virtual machine
The OS profile part will create a user (techies) inside the VM and paste the public key in the authorized keys file. The corresponding private key file path we have to mention under the “connection” parameter. Terraform will use this key pair to log in to the VM and make the changes.
Execute the “terraform plan” command to make sure that there is no error reported. If everything looks good, trigger the “terraform apply” command. Terraform will create 3 additional resources.
azurerm_virtual_machine.terravm[0]: Still creating... [1m40s elapsed]
azurerm_virtual_machine.terravm[0]: Creation complete after 1m47s [id=/subscriptions/a9553bcc4930/resourceGroups/DevOps_Techies/providers/Microsoft.Compute/virtualMachines/vm-stg-0]
Apply complete! Resources: 3 added, 0 changed, 0 destroyed.
Login to azure and make sure that OS disk, data disk, and VM is created.
Try to login to the VM with the help of a private key and confirm the user is created.
$ ssh -i id_rsa techies@xxx.xx.xx.xxx
The authenticity of host 'xxx.xx.xx.xxx' can't be established.
ECDSA key fingerprint is SHA256:pdor5jQhBLeEyoNHQtsvMzYLlq/epooKtwi7va8MpSo.
Are you sure you want to continue connecting (yes/no)? yes
To run a command as administrator (user "root"), use "sudo <command>".
See "man sudo_root" for details.
techies@hostname:~$
If you are not able to log in the VM make sure that you have added route public IP address in the “external_ip” variable. We have confirmed the VM and additional disk are created successfully.
Multiple resource creation
We have successfully created a single virtual machine and corresponding resources. Next, you imagine that you need to create an additional two VMs then you NO need to write the entire code two times instead we have to change only one variable which is “numbercount” in the variable.tf file. Terraform will take care of the number of resources and naming. Change the variable detail as below.
variable "numbercount" {
type = number
default = 3
}
Save the variable file and execute a “terraform plan” to confirm no error is reported. Once ready trigger the “terraform plan” command. The expected output will be 2 more virtual machines.
After the execution, login to the Azure and make sure that the additional two VM and its data and OS disk are created.
With the help of the Terraform tool, we have created multiple resources by changing only a single variable.Â
Hope this article helps you to understand Terraform’s functioning and its arguments. If you have any doubts or questions, feel free to comment.Â
I have seen multiple articles to provision single Azure resources. But this article helped to provision multiple resources easily. Great article.
Thanks, Ann for the feedback
Great article… Could you please share complete network.tf file..
thanks
Kudos… great article
Hey I’m having a problem. its complaining about address_space not being a string. How can I fix this?
Error: Unsuitable value type
on variable.tf line 14, in variable “address_space”:
14: description = [“10.0.0.0/16”]
Unsuitable value: string required
A description is to describe the variable. Could you change the code as given below
variable “address_space” {
default = [“10.0.0.0/16”]
That did the job. THANKS! helped me a lot 🙂
you are always welcome..
network_interface_ids = [element(azurerm_network_interface.terranic.*.id, count.index)]
is not working for me. I can create the vm but I m not able to connect to it.
Before I was using [“${azurerm_network_interface.terranic.*.id[count.index]}”] and now I can not make it to work.
Do you have any ideas?
Great post btw!
Hi Charles,
I think it is because of the version miss-match. Could you check Terraform version or the provider version?
provider “azurerm” {
version = “1.27.0”
}
Also, please share the error message which you received while using
network_interface_ids = [element(azurerm_network_interface.terranic.*.id, count.index)]
Thanks
Hi.
Thank you this is useful
You are always welcome!!
This is not version issue. Its a code issue you have not defined nic in code anywhere that’s why causing this issue.
Error: Reference to undeclared resource
│
│ on instance.tf line 18, in resource “azurerm_virtual_machine” “terravm”:
│ 18: network_interface_ids = [element(azurerm_network_interface.terranic.*.id, count.index)]
│
│ A managed resource “azurerm_network_interface” “terranic” has not been declared in the root module.
It’s there my apology. I missed that terranic part. Now updated that but facing different issue.. Thanks for you article
May I know what is the new error that you are getting?