Toan Le

How to Deploy Azure VM with Terraform

2023-03-12

In this tutorial, we will guide you on how to deploy an Azure Virtual Machine (VM) with Terraform. Terraform is an Infrastructure as Code (IaC) tool that allows you to define and provision infrastructure in a declarative manner. It is an open-source tool that enables you to define and manage your cloud infrastructure easily.

Prerequisites

Before we start, ensure you have the following:

  • An Azure account with an active subscription
  • Terraform installed on your local machine
  • Azure CLI installed on your local machine

Steps

1. Create a new directory

Create a new directory on your local machine and navigate to it using the command prompt or terminal.

2. Create a new file

Create a new file with the extension .tf (e.g. main.tf) in your directory. This file will contain the Terraform code to create the VM.

3. Define your provider

In your .tf file, define the Azure provider as follows:

provider "azurerm" {
  features {}
}

4. Define your resource group

Create a resource group for your VM using the following code:

resource "azurerm_resource_group" "mtc-rg" {
  name     = "mtc-rg"
  location = "Southeast Asia"
  tags = {
    environment = "dev"
  }
}

5. Define your virtual network

Create a virtual network using the following code:

resource "azurerm_virtual_network" "mtc-vn" {
  name                = "mtc-network"
  resource_group_name = azurerm_resource_group.mtc-rg.name
  location            = azurerm_resource_group.mtc-rg.location
  address_space       = ["10.0.0.0/16"]
  tags = {
    environment = "dev"
  }
}

6. Define your subnet

Create a subnet for your VM using the following code:

resource "azurerm_subnet" "mtc-subnet" {
  name                 = "mtc-subnet"
  resource_group_name  = azurerm_resource_group.mtc-rg.name
  virtual_network_name = azurerm_virtual_network.mtc-vn.name
  address_prefixes     = ["10.0.0.0/24"]
}

7. Define network security group

resource "azurerm_network_security_group" "mtc-sg" {
  name                = "mtc-sg"
  resource_group_name = azurerm_resource_group.mtc-rg.name
  location            = azurerm_resource_group.mtc-rg.location

  tags = {
    environment = "dev"
  }
}

8. Define network security rule

resource "azurerm_network_security_rule" "mtc-dev-rule" {
  name                        = "mtc-dev-rule"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "*"
  source_port_range           = "*"
  destination_port_range      = "*"
  source_address_prefix       = "*"
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.mtc-rg.name
  network_security_group_name = azurerm_network_security_group.mtc-sg.name
}

9. Define network security rule

resource "azurerm_network_security_rule" "mtc-dev-rule" {
  name                        = "mtc-dev-rule"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "*"
  source_port_range           = "*"
  destination_port_range      = "*"
  source_address_prefix       = "*"
  destination_address_prefix  = "*"
  resource_group_name         = azurerm_resource_group.mtc-rg.name
  network_security_group_name = azurerm_network_security_group.mtc-sg.name
}

10. Define network security group association

resource "azurerm_subnet_network_security_group_association" "mtc-sga" {
  subnet_id                 = azurerm_subnet.mtc-subnet.id
  network_security_group_id = azurerm_network_security_group.mtc-sg.id
}

11. Define your public IP address

Create a public IP address for your VM using the following code:

resource "azurerm_public_ip" "mtc-ip" {
  name                = "mtc-ip"
  resource_group_name = azurerm_resource_group.mtc-rg.name
  location            = azurerm_resource_group.mtc-rg.location
  allocation_method   = "Dynamic"

  tags = {
    environment = "dev"
  }
}

12. Define network interface

Finally, create your VM using the following code:

resource "azurerm_network_interface" "mtc-nic" {
  name                = "mtc-nic"
  location            = azurerm_resource_group.mtc-rg.location
  resource_group_name = azurerm_resource_group.mtc-rg.name

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.mtc-subnet.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.mtc-ip.id
  }

  tags = {
    environment = "dev"
  }
}

13. Define virtual machine

Create customdata.tpl to execute script when VM is created. For example install Docker.

#!/bin/bash
sudo apt-get update -y &&
sudo apt-get install -y \
    ca-certificates \
    curl \
    gnupg \
    lsb-release

sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update -y && 
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y

Generate SSH key for VM

ssh-keygen -t rsa

Create a virtual network using the following code:

resource "azurerm_linux_virtual_machine" "mtc-vm" {
  name                = "mtc-vm"
  resource_group_name = azurerm_resource_group.mtc-rg.name
  location            = azurerm_resource_group.mtc-rg.location
  size                = "Standard_B1s"
  admin_username      = "adminuser"
  network_interface_ids = [
    azurerm_network_interface.mtc-nic.id,
  ]

  custom_data = filebase64("customdata.tpl")

  admin_ssh_key {
    username   = "adminuser"
    public_key = file("~/.ssh/mtcazurekey.pub")
  }

  os_disk {
    caching              = "ReadWrite"
    storage_account_type = "Standard_LRS"
  }

  source_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }
}

14. Define output

data "azurerm_public_ip" "ip-data" {
  name                = azurerm_public_ip.mtc-ip.name
  resource_group_name = azurerm_resource_group.mtc-rg.name
  depends_on          = [azurerm_linux_virtual_machine.mtc-vm]
}

output "public_ip_address" {
  value = "${azurerm_linux_virtual_machine.mtc-vm.name}: ${data.azurerm_public_ip.mtc-ip-data.ip_address}"
}

data "azurerm_subscription" "current" {
}

output "current_subscription_display_name" {
  value = data.azurerm_subscription.current.display_name
}

data "azurerm_client_config" "current" {
}

output "account_id" {
  value = data.azurerm_client_config.current.client_id
}

15. Create Service Principle

Terraform supports authenticating to Azure through a Service Principal or the Azure CLI. We recommend using a Service Principal when running in a shared environment (such as within a CI server/automation) - and authenticating via the Azure CLI when you’re running Terraform locally.

// Powershell
az ad sp create-for-rbac --name terraform --role Contributor --scopes /subscriptions/{your_subscription_id}

$env:ARM_SUBSCRIPTION_ID = "{your_subscription_id}"
$env:ARM_TENANT_ID = "{your_tenant_id}"
$env:ARM_CLIENT_ID = "{your_client_id}"
$env:ARM_CLIENT_SECRET = "{your_client_secret}"

16. Deploy your VM

Save your .tf file and navigate to your directory using the command prompt or terminal. Run the following commands to deploy your VM:

terraform init
terraform plan
terraform apply
terraform destroy

After running the terrafrom plan. Here is the output.

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create
 <= read (data resources)

Terraform will perform the following actions:

  # data.azurerm_public_ip.mtc-ip-data will be read during apply
  # (depends on a resource or a module with changes pending)    
 <= data "azurerm_public_ip" "mtc-ip-data" {
      + allocation_method       = (known after apply)
      + domain_name_label       = (known after apply)
      + fqdn                    = (known after apply)
      + id                      = (known after apply)
      + idle_timeout_in_minutes = (known after apply)
      + ip_address              = (known after apply)
      + ip_tags                 = (known after apply)
      + ip_version              = (known after apply)
      + location                = (known after apply)
      + name                    = "mtc-ip"
      + resource_group_name     = "mtc-rg"
      + reverse_fqdn            = (known after apply)
      + sku                     = (known after apply)
      + tags                    = (known after apply)
      + zones                   = (known after apply)
    }

  # azurerm_linux_virtual_machine.mtc-vm will be created
  + resource "azurerm_linux_virtual_machine" "mtc-vm" {
      + admin_username                  = "adminuser"
      + allow_extension_operations      = true
      + computer_name                   = (known after apply)
      + custom_data                     = (sensitive value)
      + disable_password_authentication = true
      + extensions_time_budget          = "PT1H30M"
      + id                              = (known after apply)
      + location                        = "southeastasia"
      + max_bid_price                   = -1
      + name                            = "mtc-vm"
      + network_interface_ids           = (known after apply)
      + patch_mode                      = "ImageDefault"
      + platform_fault_domain           = -1
      + priority                        = "Regular"
      + private_ip_address              = (known after apply)
      + private_ip_addresses            = (known after apply)
      + provision_vm_agent              = true
      + public_ip_address               = (known after apply)
      + public_ip_addresses             = (known after apply)
      + resource_group_name             = "mtc-rg"
      + size                            = "Standard_B1s"
      + virtual_machine_id              = (known after apply)

      + admin_ssh_key {
          + public_key = <<-EOT
                ssh-rsa *******************************      
            EOT
          + username   = "adminuser"
        }

      + os_disk {
          + caching                   = "ReadWrite"
          + disk_size_gb              = (known after apply)
          + name                      = (known after apply)
          + storage_account_type      = "Standard_LRS"
          + write_accelerator_enabled = false
        }

      + source_image_reference {
          + offer     = "UbuntuServer"
          + publisher = "Canonical"
          + sku       = "18.04-LTS"
          + version   = "latest"
        }
    }

  # azurerm_network_interface.mtc-nic will be created
  + resource "azurerm_network_interface" "mtc-nic" {
      + applied_dns_servers           = (known after apply)
      + dns_servers                   = (known after apply)
      + enable_accelerated_networking = false
      + enable_ip_forwarding          = false
      + id                            = (known after apply)
      + internal_dns_name_label       = (known after apply)
      + internal_domain_name_suffix   = (known after apply)
      + location                      = "southeastasia"
      + mac_address                   = (known after apply)
      + name                          = "mtc-nic"
      + private_ip_address            = (known after apply)
      + private_ip_addresses          = (known after apply)
      + resource_group_name           = "mtc-rg"
      + tags                          = {
          + "environment" = "dev"
        }
      + virtual_machine_id            = (known after apply)

      + ip_configuration {
          + gateway_load_balancer_frontend_ip_configuration_id = (known after apply)
          + name                                               = "internal"
          + primary                                            = (known after apply)
          + private_ip_address                                 = (known after apply)
          + private_ip_address_allocation                      = "Dynamic"
          + private_ip_address_version                         = "IPv4"
          + public_ip_address_id                               = (known after apply)
          + subnet_id                                          = (known after apply)
        }
    }

  # azurerm_network_security_group.mtc-sg will be created
  + resource "azurerm_network_security_group" "mtc-sg" {
      + id                  = (known after apply)
      + location            = "southeastasia"
      + name                = "mtc-sg"
      + resource_group_name = "mtc-rg"
      + security_rule       = (known after apply)
      + tags                = {
          + "environment" = "dev"
        }
    }

  # azurerm_network_security_rule.mtc-dev-rule will be created
  + resource "azurerm_network_security_rule" "mtc-dev-rule" {
      + access                      = "Allow"
      + destination_address_prefix  = "*"
      + destination_port_range      = "*"
      + direction                   = "Inbound"
      + id                          = (known after apply)
      + name                        = "mtc-dev-rule"
      + network_security_group_name = "mtc-sg"
      + priority                    = 100
      + protocol                    = "*"
      + resource_group_name         = "mtc-rg"
      + source_address_prefix       = "*"
      + source_port_range           = "*"
    }

  # azurerm_public_ip.mtc-ip will be created
  + resource "azurerm_public_ip" "mtc-ip" {
      + allocation_method       = "Dynamic"
      + fqdn                    = (known after apply)
      + id                      = (known after apply)
      + idle_timeout_in_minutes = 4
      + ip_address              = (known after apply)
      + ip_version              = "IPv4"
      + location                = "southeastasia"
      + name                    = "mtc-ip"
      + resource_group_name     = "mtc-rg"
      + sku                     = "Basic"
      + sku_tier                = "Regional"
      + tags                    = {
          + "environment" = "dev"
        }
    }

  # azurerm_resource_group.mtc-rg will be created
  + resource "azurerm_resource_group" "mtc-rg" {
      + id       = (known after apply)
      + location = "southeastasia"
      + name     = "mtc-rg"
      + tags     = {
          + "environment" = "dev"
        }
    }

  # azurerm_subnet.mtc-subnet will be created
  + resource "azurerm_subnet" "mtc-subnet" {
      + address_prefixes                               = [
          + "10.0.0.0/24",
        ]
      + enforce_private_link_endpoint_network_policies = false
      + enforce_private_link_service_network_policies  = false
      + id                                             = (known after apply)
      + name                                           = "mtc-subnet"
      + resource_group_name                            = "mtc-rg"
      + virtual_network_name                           = "mtc-network"
    }

  # azurerm_subnet_network_security_group_association.mtc-sga will be created
  + resource "azurerm_subnet_network_security_group_association" "mtc-sga" {
      + id                        = (known after apply)
      + network_security_group_id = (known after apply)
      + subnet_id                 = (known after apply)
    }

  # azurerm_virtual_network.mtc-vn will be created
  + resource "azurerm_virtual_network" "mtc-vn" {
      + address_space       = [
          + "10.0.0.0/16",
        ]
      + dns_servers         = (known after apply)
      + guid                = (known after apply)
      + id                  = (known after apply)
      + location            = "southeastasia"
      + name                = "mtc-network"
      + resource_group_name = "mtc-rg"
      + subnet              = (known after apply)
      + tags                = {
          + "environment" = "dev"
        }
    }

Plan: 9 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + account_id                        = "*******************************"
  + current_subscription_display_name = "dev-sub"
  + public_ip_address                 = (known after apply)

The terraform apply command will prompt you to confirm the deployment. Type yes and press enter to proceed.
Run terraform destroy to destroy the environment.

Congratulations! You have successfully deployed an Azure VM using Terraform.

Conclusion

In this tutorial, we have shown you how to deploy an Azure VM using Terraform. Terraform simplifies the process of deploying infrastructure by allowing you to define and manage your resources in a declarative manner. With Terraform, you can easily create, update, and delete resources as needed. We hope you found this tutorial helpful. If you have any questions or comments, feel free to leave them below.

Copyright (c) 2024