Terraform to Provision Multiple Azure Virtual Machines

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

Terraform installation

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. 

15 comments

  1. Ann Merry Reply

    I have seen multiple articles to provision single Azure resources. But this article helped to provision multiple resources easily. Great article.

  2. Josin Reply

    Great article… Could you please share complete network.tf file..
    thanks

  3. someone Reply

    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

    • admin Post authorReply

      A description is to describe the variable. Could you change the code as given below

      variable “address_space” {
      default = [“10.0.0.0/16”]

  4. charles Reply

    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!

    • admin Post authorReply

      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

  5. ram gholap Reply

    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.

    • ram gholap Reply

      It’s there my apology. I missed that terranic part. Now updated that but facing different issue.. Thanks for you article

Leave a Reply

Your email address will not be published. Required fields are marked *