Tutorials
Tutorials
September 23, 2024

AzureRM Terraform Provider Overview

By
Brendan Thompson

At the core of Terraform and OpenTofu is the provider. The provider allows engineers to interact with any number of services on the internet, allowing them to; create, update, and destroy resources on that service. The diagram below helps to articulate how exactly this flow works; firstly the Service must provide an API, which is then encapsulated in an SDK, a Terraform provider is then created using the Terraform Plugin Framework, which is then able to be consumed by HCL which acts as a configuration file for our infrastructure as code creating both new resources and data sources. In order to utilize a provider within your Infrastructure as Code, it must first be published to the Terraform registry or a registry that implements the registry interface.

Terraform/ OpenTofu Provider Flow

In the rest of this article, we are going to talk about the Terraform AzureRM Provider, this allows the creation of resources with the Microsoft Azure platform using Terraform configuration files. Microsoft actually has two separate types of providers available for engineers to use, the first being the aforementioned AzureRM provider and the second being AzAPI. The latter of the two Terraform provides direct access to the Azure API and _can_ be a good choice if Azure resources are not yet available in the AzureRM provider. However, I would strongly suggest utilizing the AzureRM provider in your day to day configuration as it provides a clear and easy to consume interface. The AzureRM provider also gets very regular releases with a weekly cadence and fantastic technical support from both HashiCorp, Microsoft, and the Community. This regular cadence ensures that security updates and bug fixes are released as quickly as possible meaning less snafus for us engineers, and ensures that we can take advantage of the latest features that both Azure and the provider framework has to offer!

The following section(s) will go through the provider configuration as well as examples of deploying common Azure resources.

Configuration

Firstly, let's look at the most basic provider configuration object we can use for AzureRM:

 provider "azurerm" {
    features {}
    subscription_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

Unfortunately, even with the recent release of Version 4 of the Azure provider, the features block is still required. This block can be utilized to customize the behaviour of specific Azure Provider resources. (However I have never seen this used). The second requirement is the subscription_id argument as this scopes the provider to a specific Azure subscription, in lieu of actually using the argument an environment variable can be used, that variable is ARM_SUBSCRIPTION_ID, I would generally opt for the use of environment variables as it allows for easier customization at runtime. The big missing item here is authentication, we obviously can't augment resources on the platform without first authenticating to it. Azure offers us a few options for authentication:

As can be seen there are a variety of methods for engineers to authenticate their instance of the AzureRM provider to Azure. In the following examples you could use any of the above, however I will demonstrate with the Service Principal with Client Secret, showing a booth with environment variables and arguments.

Provider Arguments

The below shows all the required arguments in order to have a properly authenticated AzureRM provider instance. Whilst for the sake of this example the client_secret has a value this shoud NEVER be done. Either utilize the environment variable or an input variable to pass that argument in at a bare minimum.

provider "azurerm" {
  features {}

  client_id       = "00000000-0000-0000-0000-000000000000"
  client_secret   = "Ek5PiKxsfDerga8UScAgp9KSnM5NkUHPpdbr"
  tenant_id       = "10000000-0000-0000-0000-000000000000"
  subscription_id = "20000000-0000-0000-0000-000000000000"
}

Environment Variables

Firstly, we must configure the environment variables, the example below shows this for both bash and PowerShell as they are both valid options.

export ARM_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export ARM_CLIENT_SECRET="Ek5PiKxsfDerga8UScAgp9KSnM5NkUHPpdbr"
export ARM_TENANT_ID="10000000-0000-0000-0000-000000000000"
export ARM_SUBSCRIPTION_ID="20000000-0000-0000-0000-000000000000"


$env:ARM_CLIENT_ID = "00000000-0000-0000-0000-000000000000"
$env:ARM_CLIENT_SECRET = "Ek5PiKxsfDerga8UScAgp9KSnM5NkUHPpdbr"
$env:ARM_TENANT_ID = "10000000-0000-0000-0000-000000000000"
$env:ARM_SUBSCRIPTION_ID = "20000000-0000-0000-0000-000000000000"

And now we look at the extremely complex provider configuration:

provider "azurerm" {
  features {}
}

It is easy to see the advantage of utilizing environment variables as it doesn't require any argument configuration within the provider and it allows it to be managed by an external system such as an orchestrator or CI/CD pipelining tool.

Provider Versions

The next thing to talk about is the all important ability to constrain version when it comes to Terraform providers. This is done via the terraform block within your Terraform configuration. The following example will utilize the pessimistic versioning constraint.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.x"
    }
  }
}

It is vitally important to ensure that you have these blocks within required_providers as it makes certain you're getting an expected version from an expected source. Or perhaps you have brought in a copy of providers into your organization for security control that then enforces that the provider is sourced from your internal instance rather than the public one. The following are the valid operators that can be used with version constraints:

  •  `=`: Only the explicitly defined version can be used. (This behaviour is the same when no operator is provided)
  •  `!=`: Excludes the exact defined version.
  •  `>`, `>=`, `<`, `<=`: Version comparators against the defined version.
  •  `~>`: Ensures only the rightmost element of the defined version may increment (e.g. `1.0` allows for any `1.x` but will not allow `2.x`)

Nine times out of ten I will choose the last constraint known as the pessimistic version constraint.

Example

Now that we understand how to configure the provider let's look at a single example that creates some foundational Azure resources using the provider. This example will create the following resources:

  • Resource Group — the container/group for resources with Azure a resource cannot exist outside of a Resource Group
  • Virtual Network — the networking construct used with Azure
  • Virtual Machine — an IaaS instance, this can be Windows or Linux and there are resource blocks for both of these individually.

First off we create ourselves a little helper in a locals block this will ensure naming is consistent for all resources we provision:

locals {
  suffix = "dev-aue-app"
}

Normally this would be done with input variables to ensure that it is configurable however in this instance given it's an example I've opted for static values.

The next is our Resource Group creation, this is where all our resources will land. It also helps ensure a consistent location is used for all resources.

resource "azurerm_resource_group" "this" {
  name     = format("rg-%s", local.suffix)
  location = "AustraliaEast"
}

As you can see we are referencing our `local.suffix`, which will yield us the name rg-dev-aue-app.

The next thing for us to do is the network setup, this includes; Virtual Network, Subnet, and Network Interface. Other resources to consider here would be Network Security Groups and Application Security Groups.

resource "azurerm_virtual_network" "this" {
  name                = format("vn-%s", local.suffix)
  location            = azurerm_resource_group.this.location
  resource_group_name = azurerm_resource_group.this.name


  address_space = ["10.0.0.0/24"]
}


resource "azurerm_subnet" "this" {
  name                 = format("sn-%s-iaas", local.suffix)
  resource_group_name  = azurerm_resource_group.this.name
  virtual_network_name = azurerm_virtual_network.this.name


  address_prefixes = ["10.0.0.0/28"]
}


resource "azurerm_network_interface" "this" {
  name                = format("nic-%s", local.suffix)
  location            = azurerm_resource_group.this.location
  resource_group_name = azurerm_resource_group.this.name


  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.this.id
    private_ip_address_allocation = "Dynamic"
  }
}

The Virtual Network is the foundation of Azure networking, this will generally have a number of Subnets associated with it. Some of which are extremely specific such as the AzureFirewallSubnet which must be named exactly that for it to be consumable by the Azure Firewall.

Next let's look at the final resource within our example, the Virtual Machine, for this example I have selected to use a Linux Virtual Machine as it's the simplest to configure and nobody likes Windows anyway 😜.

resource "azurerm_linux_virtual_machine" "this" {
  name                = format("vm-%s", local.suffix)
  location            = azurerm_resource_group.this.location
  resource_group_name = azurerm_resource_group.this.name


  size = "Standard_F2"


  admin_username = "cloudadmin"
  admin_ssh_key {
    username   = "cloudadmin"
    public_key = file("~/.ssh/id_rsa.pub")
  }


  network_interface_ids = [
    azurerm_network_interface.this.id
  ]


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


  source_image_reference {
    publisher = "Canonical"
    offer     = "0001-com-ubuntu-server-jammy"
    sku       = "22_04-lts"
    version   = "latest"
  }
}

The Virtual Machine takes on references for all resources that we have previously created. It will provision a Linux (Ubuntu specifically) instance within our given Resource Group and Virtual Network.

Wrapping Up

In this post we have discussed what a Provider is, the specific implementation of the AzureRM provider including how to configure it and what authentication options are available to us engineers. In addition to this we looked at an example of some common resources being provisioned by the AzureRM provider.

Note: While this blog references Terraform, everything mentioned in here also applies to OpenTofu. New to OpenTofu? It is a fork of Terraform 1.5.7 as a result of the license change from MPL to BUSL by HashiCorp. OpenTofu is an open-source alternative to Terraform that is governed by the Linux Foundation. All features available in Terraform 1.5.7 or earlier are also available in OpenTofu. Find out the history of OpenTofu here.

Start using the OpenTofu & Terraform platform of the future.

A screenshot of the modules page in the Scalr Platform