Terraform Authentication using Azure SPN

To authenticate Terraform with Azure, you can use Azure Active Directory (Azure AD) to generate a service principal (SPN), and then use the workstation CLI to configure the necessary environment variables for Terraform to access and manage Azure resources.

When using Terraform from your workstation where you are running the Terraform commands such as terraform apply yourself, it’s advised to use environment variables to store the credentials. It means the SPN credentials are not saved to a code repository and, at a minimum, only available from the workstation itself. You should never write credentials into your code as it makes it hard to limit exposure.

Secret Management

A best practice is creating an SPN with a limited blast radius by limiting its access to certain working sections in Azure, such as a specific subscription or a resource group. You could grant it contributor rights to all your subscriptions, but this is > not advised. However, this means you need to manage multiple SPN accounts if you have multiple subscriptions. Using your own interactive Azure credential is not recommended, as it can often have additional access permissions and is a powerful tool for hackers.

Create the SPN

You need to create a service principal name (SPN) account in Azure AD and assign contributor rights to the area in Azure where it will manage resources. This could be the management group, subscription, or resource group. An SPN, also known as an Azure AD app registration, is the account Terraform will use when interacting with Azure. Terraform should not use your standard login account.

When you create the SPN, the generated authentication tokens are output to the CLI. These tokens produced in the CLI are appid, password and tenant. You also need the subscription_id, which must be retrieved from Azure directly.

The following uses AZ CLI

Open a terminal and run the following command, when prompted in the browser session logon to Azure.

az login

Now select your working subscription.

az account set --subscription "<SUBSCRIPTION_ID>"

Create the SPN (app registration) in Azure AD by creating the new SPN with a <NAME> that describes the landing zone, for example, terraform-spn-<SUBSCRIPTION-NAME>. Update the <SUBSCRIPTION_ID> with the subscription ID you specified in the previous step. When the command completes, ensure you preserve the output data used to create environment variables Terraform requires.

az ad sp create-for-rbac --name="<NAME>"
                         --role="contributor"
                         --scopes="/subscriptions/<SUBSCRIPTION_ID>"

This is an example of what data will be output to the CLI

  "appId": "xxxxxx-xxx-xxxx-xxxx-xxxxxxxxxx",
  "displayName": "<NAME>",
  "password": "xxxxxx~xxxxxx~xxxxx",
  "tenant": "xxxxx-xxxx-xxxxx-xxxx-xxxxx"

How Terraform uses the SPN

By default, if you load all the SPN credentials into the environment variables, the Terraform configuration files do not need any specific configuration set to use the environment variables. When Terraform executes, it will look for and load the variables automatically so long as they start with ARM_. However, if you need to split your SPN values between environment variables and in-code you can do so, as demonstrated below.

Here are two examples of some simple Terraform code that creates an Azure resource group. The code can be copied into a single main.tf file and executed.

The single subscription code executes with zero edits, it relies on all four SPN values being loaded as environment variables.

The multi subscription code requires you to supply the azure subscription_id in the code but relies on the other three SPN values being loaded as environment variables. Doing it this way is useful if you have a single SPN but it has permission to multiple subscriptions in the same Azure tenant. It’s not ideal but also not too dangerous to have the subscription_id written to your VSC.


// This relies on all SPN values being set in 
// the environment variables

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0.2"
    }
  }

  required_version = ">= 1.1.0"
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "rg" {
  name     = "myTFResourceGroup"
  location = "westus2"
}

// This relies on the SPN having access to all
// your subscriptions and you need to code the
// subscription ID only.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0.2"
    }
  }

  required_version = ">= 1.1.0"
}

provider "azurerm" {
  features {}

  subscription_id = "<your_subscription_id>"
}

resource "azurerm_resource_group" "rg" {
  name     = "myTFResourceGroup"
  location = "westus2"
}

The following image shows the basic layout of using an SPN in Azure when interacting with Azure from your workstation.

image

Set the SPN as environment variables

This method stores the SPN credentials as environment variables on the workstation executing the Terraform code. You load the authentication tokens into your workstation environment variables, and they’ll then will be called by Terraform when running terraform plan or terraform apply

Loading all the values is ideal if you have a single SPN to use. There are workarounds if you have multiple SPN, but it depends how fine grained you are with your access. For instance, if you have a single SPN for multiple subscriptions you could omit loading the subscription value and code that in your Terraform providers file instead.

This step will set the environment variables permanently which means they’ll be written to the system and always exist. Replace {value} with the output of the SPN creation command. Run this on a Terminal session on your Windows workstation.

setx ARM_CLIENT_ID {value}
setx ARM_CLIENT_SECRET {value}
setx ARM_TENANT_ID {value}
setx ARM_SUBSCRIPTION_ID {value}

You can use this command to view the environment variables in the Terminal.

gci env:ARM_*

References

Authenticate Terraform to Azure

az ad sp create-for-rbac

Last modified July 21, 2024: update (e2ae86c)