Deploying ARM Templates with Terraform

If the title seems like a bit of an oxymoron, bear with me. For most people deploying to Azure using “Infrastructure as Code” you would use either Terraform or ARM Templates, not both. Terraform does, however, have the ability to run ARM templates directly, let's look at why you might want to do that and how it works.

Don't Do This If You Can Avoid It!

Before we dive into this, a warning, if you can avoid running ARM templates in Terraform then do so. Doing what we discuss in this article should only be a last resort when nothing else will work. The main reason for this is that by running ARM templates in a Terraform configuration, you lose a lot of the benefits of Terraform, especially around the state file.

When you deploy an ARM template with Terraform all Terraform knows about is the template at the top level, it has no idea what is in the template. This limitation means that Terraform cannot manage any of the resources you declare in your template because it doesn't know the state of these resources. If you run “Terraform Destroy” against a deployment that ran an ARM template then all it can do is delete the reference to the deployment in Azure, it cannot delete any of the resources. Similar issues will be seen with “Terraform Plan”.

In addition to loosing the benefits of Terraform, you also end up mixing Terraform and ARM template syntax, especially if you try and add your ARM template inline (I would recommend avoiding this).

Using ARM templates in Terraform is a topic that comes up often when people are trying to migrate to Terraform to ARM. The thought is to take existing templates and use them in Terraform and then gradually migrate them into Terraform proper over time. In my view, I would avoid doing this, keep your ARM templates being deployed by ARM until you are ready to convert them to Terraform.

So Why Use It?

Given all the above, when would you use this feature? Generally, the only reason I would use this is if there are features available in ARM that are not available in Terraform which you need to use now, and the rest of your deployment is in Terraform.

To give an example, I recently needed to deploy some Azure API Management instances that are joined to a virtual network. Terraform offers an APIM resource, but due to issues with the upstream ARM API, it doesn't offer the ability to vNet join them. I wanted to continue to use Terraform as the means to deploy my API components in APIM, but I needed to use ARM to vNet join it. By using the ARM template functionality in Terraform, I can deploy the core APIM object using ARM and then add API's into it using Terraform.

Deploying ARM Templates

To demonstrate how this works, we will take a simple example where we want to deploy a Storage Account using ARM, and then create a container using Terraform. In reality, you would use the Terraform Storage Account object, but this could be substituted for any ARM resource.

ARM Template

First, we are going to create our ARM template, which is no different to a standard ARM template.

{
    "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {
        "StorageAccountName": {
            "type": "string",
            "metadata": {
                "description": "Name of Storage Account"
            }
        },
        "StorageAccountType": {
            "type": "string",
            "allowedValues": [
                "Standard_LRS",
                "Standard_GRS",
                "Standard_ZRS"
            ],
            "metadata": {
                "description": "Type of storage to use"
            }
        }

    },
    "variables": {
    },
    "resources": [
        {
            "name": "[parameters('StorageAccountName')]",
            "type": "Microsoft.Storage/storageAccounts",
            "apiVersion": "2015-06-15",
            "location": "[resourceGroup().location]",
            "tags": {
                "displayName": "[parameters('StorageAccountName')]"
            },
            "properties": {
                "accountType": "[parameters('StorageAccountType')]"
            }
        }
    ],
    "outputs": {
        "storageAccountName": {
            "type": "string",
            "value": "[parameters('StorageAccountName')]"
        }
    },
    "functions": [
    ]
}

We'll save this file in the same location where we plan to create our Terraform files. It is possible to place the ARM code inline in your Terraform template, but I would strongly advise you don't do this as it makes it hard to read. Instead, we will pull this in from the file.

Terraform Template

To deploy our template, we use the “azurerm_template_deployment” resource. This resource expects to be provided with the content of the template, any parameters needed by the template, and the deployment type (Incremental or Complete). As mentioned, we are using the Terraform file() command to read in our ARM template from the file rather than storing it inline.

resource "azurerm_template_deployment" "terraform-arm" {
  name                = "terraform-arm-01"
  resource_group_name = azurerm_resource_group.terraform-arm.name

  template_body = file("template.json")

  parameters = {
    "storageAccountName" = "terraformarm"
    "storageAccountType" = "Standard_LRS"

  }

  deployment_mode = "Incremental"
}



Once we create the storage account, we want to create a container in that account. You can see in our ARM template that we are outputting the name of the storage account; we can use these outputs in our Terraform code. These can be referenced using the lookup function to get specific outputs from the template deployment resource.

resource "azurerm_storage_container" "container" {
  name                  = "logs"
  resource_group_name   = azurerm_resource_group.terraform-arm.name
  storage_account_name  = lookup(azurerm_template_deployment.terraform-arm.outputs, "storageAccountName")
  container_access_type = "private"
}

The full Terraform script can be found on Github here.

Deployment

Now we have our template setup we can deploy it. There is nothing special needed here; we can just run “Terraform Apply”. Once we do that you can see that Terraform is treating the ARM template as one big JSON resource, it doesn't;t understand anything about the resources it contains, and it treats it as a single resource.

Apply

If we run a destroy, we can see that it is doing the same, treating it as a JSON object and jus removing the deployment from the ARM fabric. In this scenario, we do end up with the resources deleted, but this is only because the Terraform part of the code created the resource group, so it can delete that. If the resource group had been created outside of Terraform, then the storage account would have remained, but the container would have been deleted.

Destroy

Summary

Using ARM templates in Terraform is a useful escape hatch if you have no other way to create the resources you need, and you want to use them in your Terraform template. The fact that Terraform cannot manage the resources you create in this ARM template means it should only really be used for this sort of last-resort scenario, and not for day to day operations.