Passing values between ARM template and Terraform

When working with Terraform it might become necessary to include an ARM template deployment for part of the solution. When this happens and the ARM template is creating resources with a managed identity it is necessary to return the managed identity to the Terraform script.

ARM templates can output values as part of their deployment process. These output details can be utilized in Terraform by using the output_content of the azurerm_resource_group_template_deployment.

Let’s build a simple example to see how to use the ARM template output.

Step 1: Building the ARM template

For this example, an ARM template to create a Storage Account with managed identity will be created. To make the ARM template easy to construct and easier to read, Bicep will be used.

  1. param resourceGroupLocation string
  2. param workloadName string
  3.  
  4. resource storageResourceGroup 'Microsoft.Storage/storageAccounts@2020-08-01-preview' = {
  5.   name: 'stor${workloadName}${resourceGroupLocation}'
  6.   location: resourceGroupLocation
  7.   kind: 'StorageV2'
  8.   sku: {
  9.     name: 'Standard_LRS'
  10.     tier: 'Standard'
  11.   }
  12.   properties: {
  13.     accessTier: 'Hot'
  14.     minimumTlsVersion: 'TLS1_2'
  15.   }
  16.   identity: {
  17.     type: 'SystemAssigned'
  18.   }
  19. }
  20.  
  21. output storageObjectId string = storageResourceGroup.identity.principalId

Lines 1 and 2 will create the parameters of the ARM template, so Terraform can populate the values. Line 5 is using the parameters to construct the Storage account name. Finally, line 21 an output parameter of type string is created to hold the Principal ID of the created Storage account.

Once we have the bicep template, the bicep build command is executed to generate the ARM template. Reproduced below.

  1. {
  2.   "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  3.   "contentVersion": "1.0.0.0",
  4.   "parameters": {
  5.     "resourceGroupLocation": {
  6.       "type": "string"
  7.     },
  8.     "workloadName": {
  9.       "type": "string"
  10.     }
  11.   },
  12.   "functions": [],
  13.   "resources": [
  14.     {
  15.       "type": "Microsoft.Storage/storageAccounts",
  16.       "apiVersion": "2020-08-01-preview",
  17.       "name": "[format('stor{0}{1}', parameters('workloadName'), parameters('resourceGroupLocation'))]",
  18.       "location": "[parameters('resourceGroupLocation')]",
  19.       "kind": "StorageV2",
  20.       "sku": {
  21.         "name": "Standard_LRS",
  22.         "tier": "Standard"
  23.       },
  24.       "properties": {
  25.         "accessTier": "Hot",
  26.         "minimumTlsVersion": "TLS1_2"
  27.       },
  28.       "identity": {
  29.         "type": "SystemAssigned"
  30.       }
  31.     }
  32.   ],
  33.   "outputs": {
  34.     "storageObjectId": {
  35.       "type": "string",
  36.       "value": "[reference(resourceId('Microsoft.Storage/storageAccounts', format('stor{0}{1}', parameters('workloadName'), parameters('resourceGroupLocation'))), '2020-08-01-preview', 'full').identity.principalId]"
  37.     }
  38.   }
  39. }

Creating the Terraform script

Using the azurerm_resource_group_template_deployment resource in Terraform, the script it will expand the ARM template created earlier and will deploy it. For simplicity of demonstration the Terraform script will just output the value returned by the ARM template.

  1. resource "azurerm_resource_group_template_deployment" "template_deployment" {
  2.   name                = "stor-template-deployment"
  3.   resource_group_name = azurerm_resource_group.rg_storage.name
  4.   deployment_mode     = "Incremental"
  5.   template_content    = file("arm/storage_account.json")
  6.   parameters_content = jsonencode({
  7.     resourceGroupLocation = { value = azurerm_resource_group.rg_storage.location }
  8.     workloadName          = { value = "kdm" }
  9.   })
  10. }
  11.  
  12. output "StorageID" {
  13.   value = azurerm_resource_group_template_deployment.template_deployment.output_content
  14. }

When the script gets executed it will create the Storage Account and the value will get outputted as a JSON string. Sample below:

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
 
Outputs:
 
StorageID = {"storageObjectId":{"type":"String","value":"<em>&lt;Some GUID&gt;</em>"}}

To access the value it is necessary to convert the string to a JSON object, access the parameter of interest and finally the value.

Updating the example to return the value of the Storage Account Principal ID.

  1. output "StorageID" {
  2.   value = jsondecode(azurerm_resource_group_template_deployment.template_deployment.output_content).storageObjectId.value
  3. }

The output will be more what one would expect.

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
 
Outputs:
 
StorageID = <em>&lt;Some GUID&gt;</em>

Note for consideration

During development or updates, it might be necessary to update the output variables. When this scenario is encountered it is important to consider how Terraform determines the changes. Terraform will read values in the Terraform state to compare it to the desired state. When it comes to access the new output variable, it will throw an error on the property of the JSON object as it is not in the state.

A solution to deal with this scenario is to run the change in two phases. First run the ARM template with the new output parameters. Then update the Terraform script to use the output variable introduced. This will ensure that the new output variable is in output_content property of the template deployment resource.