Getting Started with Terraform with Azure

This post guides you though the creation of a Ubuntu Virtual Machine within Auzre, using Terraform. It assume you have setup a Azure account created. You can sign up for a free trail if you do not.

Install terraform, and the Azure ‘az cli’ tool. The command below assumes a .deb based Linux distro.

$ wget "https://releases.hashicorp.com/terraform/0.12.29/terraform_0.12.29_linux_amd64.zip"
$ unzip terraform_0.12.29_linux_amd64.zip
$ curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

Running ‘az login’ will launch web browser, and log you into Azure. The ‘az account show’ will give you the details of your session.

$ az login
$ az account show
{
  "environmentName": "AzureCloud",
  "homeTenantId": "removed",
  "id": "removed",
  "isDefault": true,
  "managedByTenants": [],
  "name": "Free Trial",
  "state": "Enabled",
  "tenantId": "removed",
  "user": {
    "name": "[email protected]",
    "type": "user"
  }
}

This basic .tf file below will serve as a test to ensure that your terraform is working. It will create a single resource group, which serves as a management container for collections of resources.

provider "azurerm" {
  # The "feature" block is required for AzureRM provider 2.x.
  # If you are using version 1.x, the "features" block is not allowed.
  version = "~>2.0"
  features {}
}

resource "azurerm_resource_group" "example" {
  name     = "example"
  location = "West Europe"
}

Having created the .tf file above, now run an apply (you should of course always run a plan before to sanity check what will be created)

$ ./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.example will be created
  + resource "azurerm_resource_group" "example" {
      + id       = (known after apply)
      + location = "westeurope"
      + name     = "example"
    }

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.example: Creating...
azurerm_resource_group.example: Creation complete after 0s [id=/subscriptions/199284ea-bc26-4f86-adc0-30e420d6b709/resourceGroups/example]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

A full example

The terraform below creates all of the resources required to spin up a Ubuntu 18.04 LTS Linux server. You’ll need to modify the below with your own username and password. The output of the TF will show you the IP address. You might need to run ‘terraform refresh && terraform output‘ after your apply as for me the IP address didn’t show the first time.

provider "azurerm" {
  # The "feature" block is required for AzureRM provider 2.x.
  # If you are using version 1.x, the "features" block is not allowed.
  version = "~>2.0"
  features {}
}

resource "azurerm_resource_group" "west_rg1" {
  name     = "West_Europe_RG1"
  location = "West Europe"
}

resource "azurerm_virtual_network" "network1" {
  name                = "vn1"
  address_space       = ["10.0.0.0/16"]
  location            = azurerm_resource_group.west_rg1.location
  resource_group_name = azurerm_resource_group.west_rg1.name

  tags = {
    environment = "Terraform Demo"
  }
}

resource "azurerm_subnet" "internal" {
  name                 = "internal"
  resource_group_name  = azurerm_resource_group.west_rg1.name
  virtual_network_name = azurerm_virtual_network.network1.name
  address_prefixes     = ["10.0.1.0/24"]
}

resource "azurerm_network_security_group" "ssh" {
  name                = "SSH Inbound"
  location            = azurerm_resource_group.west_rg1.location
  resource_group_name = azurerm_resource_group.west_rg1.name

  security_rule {
    name                       = "test123"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "22"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }
}

resource "azurerm_network_interface" "main" {
  name                = "test-nic"
  location            = azurerm_resource_group.west_rg1.location
  resource_group_name = azurerm_resource_group.west_rg1.name

  ip_configuration {
    name                          = "testconfiguration1"
    subnet_id                     = azurerm_subnet.internal.id
    private_ip_address_allocation = "Dynamic"
    public_ip_address_id          = azurerm_public_ip.example.id
  }
}

resource "azurerm_network_interface_security_group_association" "example" {
  network_interface_id      = azurerm_network_interface.main.id
  network_security_group_id = azurerm_network_security_group.ssh.id
}

resource "azurerm_public_ip" "example" {
  name                = "acceptanceTestPublicIp1"
  resource_group_name = azurerm_resource_group.west_rg1.name
  location            = azurerm_resource_group.west_rg1.location
  allocation_method   = "Dynamic"
}

resource "azurerm_virtual_machine" "main" {
  name                  = "test-vm"
  location              = azurerm_resource_group.west_rg1.location
  resource_group_name   = azurerm_resource_group.west_rg1.name
  network_interface_ids = [azurerm_network_interface.main.id]
  vm_size               = "Standard_DS1_v2"

  delete_os_disk_on_termination    = true
  delete_data_disks_on_termination = true

  storage_image_reference {
    publisher = "Canonical"
    offer     = "UbuntuServer"
    sku       = "18.04-LTS"
    version   = "latest"
  }
  storage_os_disk {
    name              = "myosdisk1"
    caching           = "ReadWrite"
    create_option     = "FromImage"
    managed_disk_type = "Standard_LRS"
  }
  os_profile {
    computer_name  = "testvm"
    admin_username = "pookey"
    admin_password = "Password12*"
  }
  os_profile_linux_config {
    disable_password_authentication = false
  }
}

output "instance_ip_addr" {
  value = azurerm_public_ip.example.ip_address
}

Known Issues

When trying to destroy the resources, a few issues cropped up. To resolve the issue, simply wait a few minutes and re-run.

Error: Error deleting Network Security Group "acceptanceTestSecurityGroup1" (Resource Group "West_Europe_RG1"): Code="Canceled" Message="Operation was canceled." Details=[{"code":"CanceledAndSupersededDueToAnotherOperation","message":"Operation DeleteNetworkSecurityGroupOperation (d8d816f9-7a51-4a6e-a7e6-1dd43de5e34c) was canceled and superseded by operation DeleteNetworkSecurityGroupOperation (7a7ff9fa-580b-43ce-bab6-14b941f897b9)."}]

Error: Error deleting Network Security Group "acceptanceTestSecurityGroup1" (Resource Group "West_Europe_RG1"): network.SecurityGroupsClient#Delete: Failure sending request: StatusCode=400 -- Original Error: Code="NetworkSecurityGroupOldReferencesNotCleanedUp" Message="Network security group acceptanceTestSecurityGroup1 cannot be deleted because old references for the following Nics: (\n/subscriptions/199284ea-bc26-4f86-adc0-30e420d6b709/resourceGroups/West_Europe_RG1/providers/Microsoft.Network/networkSecurityGroups/acceptanceTestSecurityGroup1:/subscriptions/199284ea-bc26-4f86-adc0-30e420d6b709/resourceGroups/West_Europe_RG1/providers/Microsoft.Network/networkInterfaces/test-nic) and Subnet: (\n/subscriptions/199284ea-bc26-4f86-adc0-30e420d6b709/resourceGroups/West_Europe_RG1/providers/Microsoft.Network/networkSecurityGroups/acceptanceTestSecurityGroup1:) have not been released yet." Details=[]



Error: Error deleting Network Security Group "acceptanceTestSecurityGroup1" (Resource Group "West_Europe_RG1"): network.SecurityGroupsClient#Delete: Failure sending request: StatusCode=400 -- Original Error: Code="NetworkSecurityGroupOldReferencesNotCleanedUp" Message="Network security group acceptanceTestSecurityGroup1 cannot be deleted because old references for the following Nics: (\n/subscriptions/199284ea-bc26-4f86-adc0-30e420d6b709/resourceGroups/West_Europe_RG1/providers/Microsoft.Network/networkSecurityGroups/acceptanceTestSecurityGroup1:/subscriptions/199284ea-bc26-4f86-adc0-30e420d6b709/resourceGroups/West_Europe_RG1/providers/Microsoft.Network/networkInterfaces/test-nic) and Subnet: (\n/subscriptions/199284ea-bc26-4f86-adc0-30e420d6b709/resourceGroups/West_Europe_RG1/providers/Microsoft.Network/networkSecurityGroups/acceptanceTestSecurityGroup1:) have not been released yet." Details=[]