Major version 4.x of the AzureRM provider was just released! What’s new, what’s different, and what do you need to know? That’s what I’m going to cover in this post.
The AzureRM provider has been on major version 3 for two and a half years! Since that release, a ton of new resources and data sources have been added to the provider and they’ve kept up the pace of releasing a new minor version of the provider roughly every two weeks. As such, the most recent minor version for major version 3 is 116. As in one-hundred and sixteen minor release 🤯. A minor release does not have breaking changes, meaning all significant changes to the provider have been waiting until now to be implemented!
So what are the big, breaking changes? They fall into three categories:
There’s also some new functionality added to the provider which doesn’t break existing code, but enhances the use of the provider. In particular, there are two changes:
Why don’t we start with the new functionality and then get into breaking changes?
Provider-defined functions were introduced to Terraform in version 1.8. Of the providers adopting this feature, azurerm
was notably absent at launch. That’s not because HashiCorp didn’t want to add provider-defined functions to AzureRM, but rather because the versions of the provider SDK being used made it difficult. Major version 4 adopts the Terraform plugin framework for some portions of the provider, which brings with it support for provider-defined functions.
There are two new provider-defined functions: normalise_resource_id
and parse_resource_id
. As you probably already know, Azure resource IDs are really long strings that uniquely identify a resource. They include the subscription, resource group, resource provider type, parent resource, etc. While you could try and manipulate with these strings using the built-in functions of Terraform, the two new provider-defined functions make it a bit easier.
The normalise_resource_id
function takes a resource ID and attempts to normalize the capitalization of its elements to match what the Azure API is expecting. The Azure API is case sensitive (sometimes) and if you’re constructing a resource ID by hand or receiving it from another source- like an input variable- you may want to normalize it before using it.
variable "vnet_resource_id" {
type = string
description = "Resource ID of VNet"
}
locals {
vnet_resource_id = provider::azurerm:normalise_resource_id(var.vnet_resource_id)
}
The above code would create a normalized version of a VNet resource ID for use in the rest of your code. This is also super helpful for import blocks, where you have to supply a resource ID to successfully import your resource:
import {
to = azurerm_container_app.example
id = provider::azurerm::normalise_resource_id(var.container_app_id)
}
The parse_resource_id
function breaks a resource ID into its component parts. The output is a map with the following keys:
full_resource_type
parent_resources
resource_group_name
resource_name
resource_provider
resource_scope
resource_type
subscription_id
Depending on the resource type, some of the values may be set to an empty value. For instance, an azurerm_virtual_network
would have the following fields set:
parsed_id_vnet = {
"full_resource_type" = "Microsoft.Network/virtualNetworks"
"parent_resources" = tomap({})
"resource_group_name" = "function-example-rg"
"resource_name" = "function-example-vnet"
"resource_provider" = "Microsoft.Network"
"resource_scope" = ""
"resource_type" = "virtualNetworks"
"subscription_id" = "XXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXX"
}
Since a VNet doesn’t have parent resources, the parent_resources
field is empty. This function is very similar in nature to the arn_parse
function in the aws
provider.
The Azure Resource Manager control plane is built on the idea of Resource Providers (RP) which are responsible for the lifecycle of resources under a certain category. For instance, the VNet shown above is of resource type Microsoft.Network/virtualNetworks
, which means it uses the Microsoft.Network
Resource Provider.
When you create a subscription in Azure, a subset of Resource Providers are registered for use by default. You can find the full list here, just look for any namespace that has “registered by default” appended to it. Most of the default services are core to the platform, like microsoft.support
or Microsoft.Billing
. Bonus points for inconsistent capitalization! Good thing there’s a function to help with that 😂.
All other Resource Providers need to be registered before you can create or manage resources that leverage the RP. When you deploy a resource using the Portal or an ARM template, Azure checks to see if the necessary RP is registered. If it isn’t, Azure Resource Manager registers the RP for you (assuming you have permissions to do so.)
To avoid potential deployment errors, the default behavior of the azurerm
provider is to register every single RP that isn’t already registered. That’s… unnecessary and actually goes against Microsoft’s guidance:
“Register a resource provider only when you’re ready to use it. This registration step helps maintain least privileges within your subscription. A malicious user can’t use unregistered resource providers.”
Not only that, but if the account you’re using doesn’t have permissions to register RPs, you’ll get an error back from Terraform. The workaround for previous versions of the azurerm
provider was the argument skip_provider_registration
. When set to true
, Terraform wouldn’t try to register any RPs, which could result in a deployment error.
Clearly this was a very blunt solution, and version 4 of the azurerm
provider takes a slightly more nuanced approach. There are two new arguments for the provider: resource_provider_registrations
and resource_providers_to_register
.
The resource_provider_registrations
argument can be set to a pre-defined group of RPs to automatically register. Currently the allowed values are core
, extended
, all
, none
, and legacy
. I would imagine more may be added over time. The current list of RPs included in each group can be found on GitHub.
The resource_providers_to_register
argument takes a list of Resource Providers to register. This can be used in tandem with resource_provider_registrations
to register additional RPs that are not part of the grouping. If you want to be REALLY specific, you could set resource_provider_registrations
to none
and only list the RPs your want registered with resource_providers_to_register
. Is it worth doing that? That’s for you to decide.
With the introduction of the two new arguments, the skip_provider_registration
argument has been removed. If you want the same functionality, simply set resource_provider_registrations
to none
.
This is a breaking change if you were using the skip_provider_registration
before. Speaking of breaking changes…
Possibly the biggest breaking change is making Subscription ID a required argument in the provider. For anyone who uses Azure CLI based authentication for their configurations, you’ll now get an error when you attempt to run terraform plan
or terraform apply
:
Planning failed. Terraform encountered an error while generating this plan.
╷
│ Error: `subscription_id` is a required provider property when performing a plan/apply operation
When using the Azure CLI for authentication, the default behavior was to use the credentials and currently selected subscription from the CLI for Terraform deployments. If you wanted to use a different subscription, you could run az account set -s SUBSCRIPTION_NAME
and Terraform would use that one.
Did this lead to accidental deployments in the wrong subscription? Almost certainly. The implicit relationship between the Azure CLI and the provider made it all too easy to run terraform apply
on the wrong subscription and inadvertently take down Prod when you though the Dev subscription was selected.
Version 4 makes the relationship explicit by requiring that you set the target subscription either through the subscription_id
argument in the provider block or through the ARM_SUBSCRIPTION_ID
environment variable. This only impacts Azure CLI based authentication, as every other authentication type requires you to set the subscription ID explicitly.
I can hear the clatter of a thousand Terraform tutorials all being updated at once, and I probably have to go back and make sure none of my example code is broken by this change. This is why its so important to pin the version of your provider to a at least a major version if not more specific.
I’m not going to list out every single resources or data source that has been removed or altered in a breaking way. The 4.0 Upgrade Guide does an excellent job of that. But I do want to point out some significant changes that I think are worth mentioning.
There’s two big things to note with AKS. First, the azurerm_kubernetes_cluster
resource will only use the stable version of the AKS API. There are two versions of the API: stable and preview. Previously, the AKS cluster resource used a mix of the two APIs to allow cutting-edge and experimental features to be available to the resource. The AKS team politely asked Microsoft and HashiCorp to knock it off and only use the stable API. You know, because it’s the stable one. This means that certain arguments will no longer be available in version 4.0 of the provider.
If you wish to keep using the preview AKS API, you can deploy your AKS cluster using the azapi
provider. That gives you full access to any Azure APIs exposed to the public, including experimental and preview versions.
The other big change is a bunch of removed and changed arguments in the azurerm_kubernetes_cluster
resources. There are 23 changes in total including removals, renaming, and accepted values. If you’re managing an AKS cluster with Terraform, you’re going to want to pay special attention to the provider changes!
There are a ton of database related resources that are being replaced. Basically, all resources that start with azurerm_sql
are being replaced with resources that start with azurerm_mssql
, and all the resources starting with azurerm_mariadb
or azurerm_mysql
are being replaced with azurerm_mysql_flexible
resources. If you happen to be using these older resources, some have been superseded and others are being retired entirely.
The azurerm_subnet
and azurerm_virtual_network
resources have arguments that are changing the accepted value type from a list
to a set
. As a quick reminder, a set
in Terraform is an unordered collection of unique elements. Since it is unordered, you cannot refer to an individual element using an index, e.g. local.my_list[0]
. Instead, you’ll be using a for
expression to loop through the contents of the set. Also, when passing a list to these arguments, you’ll need to use the toset()
function to convert it to a set.
These changes may require rewriting some of your existing code to handle the difference between a set and list.
It has been two and a half years since the last major version of azurerm
! 30 MONTHS! I’m pretty sure in that time the aws
provider had two major version releases. Clearly there was a lot of work and thought put into this newer version of the provider, with an eye towards managing change going forward.
I’m really excited to see the AzureRM provider use the newer plugin framework, and I think it will simplify the maintenance of the provider in the future. With any luck that means quicker additions of new resources and properties when they’re released by Microsoft.
Resourcely Guardrails and Blueprints
November 15, 2024
Deploying Azure Landing Zones with Terraform
November 12, 2024
October 18, 2024
What's New in the AzureRM Provider Version 4?
August 27, 2024