6 min read

Migrating to HashiCorp Terraform 0.12 on Microsoft Azure

With the release of Terraform 0.12, we can improve the configuration of our infrastructure resources that are using the Azure Terraform Resource Provider. In this post, we will discuss how we can use Terraform 0.12 to organize, configure, and deploy resources to Azure.

What is Terraform 0.12?

Terraform is an open source tool that uses declarative configuration files to create, change, and improve infrastructure. The Terraform 0.12 release, in particular, includes additional improvements and features to the declarative language, called HashiCorp Configuration Language version 2 (HCL2). We use HCL2 to describe infrastructure resources and configuration in configuration files.

HCL2 simplifies the process of writing, inspecting, and applying the configuration to Azure resources. The first version of HCL in versions prior to Terraform 0.12 used string types while HCL2 in Terraform version 0.12 and higher leverages a generalized type system and stricter parsing of configuration. The new type system expands functionality and addresses long-standing community requests such as list comprehension, null or unset values, and expanded deserialization of JSON and other formats.

Upgrading the Azure Terraform Resource Provider

How does Terraform 0.12 affect the Azure Terraform Resource Provider? Stricter type enforcement may result in some errors but improved error messaging in Terraform 0.12 can help identify them. Furthermore, we proactively update the provider to support Terraform 0.12 syntax as we find errors. The Azure provider supports Terraform 0.12 syntax since version 1.27.

Before running terraform 0.12upgrade, we advise that you review the detailed upgrade instructions on upgrading to Terraform 0.12. While the CLI upgrade command can facilitate an initial refactor for simple configurations, run terraform 0.12checklist and evaluate any pre-upgrade steps.

Benefits of Terraform 0.12

Improvements and features for Terraform 0.12 provide additional flexibility and minimize configuration when creating Azure resources. Using an example to deploy Azure App Service instances and an [Azure Application Gateway](https://docs.microsoft.com/en-us/azure/application-gateway/overview), we will demonstrate the benefits of Terraform 0.12’s first class expression syntax, richer type system, and dynamic nested blocks.

Upgrading Azure App Service Configuration

As part of our configuration, we need to create two App Service instances with Windows and Linux configurations. In order to support any arbitrary configuration of App Service instances, we could iterate over a list of maps describing each instance’s kind, tier, and size. In Terraform 0.11, these maps consisted of string labels and values that must be of the same type. For example, a map for an App Service must use a string label and string value.

# Terraform 0.11 Configuration. Some sections omitted for clarity.
locals {
 app_services = [
   {
     kind     = "Linux"
     sku_tier = "Standard"
     sku_size = "S1"
   },
   {
     kind     = "Windows"
     sku_tier = "Basic"
     sku_size = "B1"
   },
 ]
}

To iterate over our list of App Service instances, we use a series of functions and interpolated variables. Terraform versions prior to 0.12 use a string-based type system and could only interpret variables through string interpolation. As a result, accessing values in maps and lists requires the additional use of functions. We use the lookup function to access a value based on a key in a map. To access an element at an index in a list, we use the elementfunction.

# Terraform 0.11 Configuration. Some sections omitted for clarity.
resource "azurerm_app_service" "example" {
 count               = "${length(local.app_services)}"
 name                = "${lower(lookup(local.app_services[count.index], "kind"))}-appservice"
 location            = "${azurerm_resource_group.example.location}"
 resource_group_name = "${azurerm_resource_group.example.name}"
 app_service_plan_id = "${element(azurerm_app_service_plan.example.*.id, count.index)}"

 site_config {
   # omitted for clarity
 }
}

The generalized type system in Terraform 0.12 provides useful functionality to minimize configuration and better access values in richer types. Instead of using maps, we can leverage structural types to group together multiple values of varying types. For example, we can rewrite our App Service instance configuration with a nested map for sku. With this change, our App Service instance configuration becomes an object type with varying attributes.

# Terraform 0.12 Configuration. Some sections omitted for clarity.
locals {
 app_services = [
   {
     kind = "Linux"
     sku = {
       tier = "Standard"
       size = "S1"
     }
   },
   {
     kind = "Windows"
     sku = {
       tier = "Basic"
       size = "B1"
     }
   }
 ]
}

Furthermore, we can remove variable interpolation syntax, the lookup function, and the element function from our configuration. Maps, objects, and lists can be accessed using indices and attributes. For example, our local.appservices list can be accessed using an index syntax such as local.app_services[count.index]. We can also retrieve the kind attribute in the App Service instance configuration with local.app_services[count.index].kind.

# Terraform 0.12 Configuration. Some sections omitted for clarity.
resource "azurerm_app_service" "example" {
 count               = length(local.app_services)
 name                = "${lower(local.app_services[count.index].kind)}-appservice"
 location            = azurerm_resource_group.example.location
 resource_group_name = azurerm_resource_group.example.name
 app_service_plan_id = azurerm_app_service_plan.example[count.index].id

 site_config {
    # omitted for clarity
 }
}

Upgrading Azure Application Gateway Configuration

After upgrading the configuration for App Service instances, we can now tackle the Azure Application Gateway configuration. The azurerm_application_gateway resource requires nested blocks for certain attributes, such as the backend_address_pool. In Terraform 0.11, these attribute blocks contained other content and metadata necessary for the resource. If we need many of the same attributes, we would declare each nested block individually. This often leads to duplicated and hard-coded configuration for each block.

# Terraform 0.11 Configuration. Some sections omitted for clarity.
resource "azurerm_application_gateway" "network" {
 frontend_port {
   name = "${azurerm_app_service.example.0.name}"
   port = 8080
 }

 frontend_port {
   name = "${azurerm_app_service.example.1.name}"
   port = 8081
 }

 # omitted for clarity

 backend_address_pool {
   name  = "${local.backend_address_pool_name}"
   fqdns = ["${azurerm_app_service.example.0.default_site_hostname}"]
 }

 backend_address_pool {
   name  = "${local.backend_address_pool_name}"
   fqdns = ["${azurerm_app_service.example.1.default_site_hostname}"]
 }
}

Upon upgrading to Terraform 0.12, we can rewrite the configuration to use dynamic blocks. Instead of declaring each block with a different configuration, we iterate over a list and create a set of blocks dynamically. We achieve this by prefixing our nested block for an attribute with the dynamic keyword. The for_each iterator allows us to iterate over a list of objects, retrieve their values, and set variables in the nested block’s content.

To set variables for each attribute in the nested block, we need to access the element that the iterator references. Terraform 0.12 defaults the current element’s variable name to the resource’s attribute. For example, in the dynamic "frontend_port" block, we can retrieve each App Service object using the frontend_port variable. To get the value of a specific attribute, such as the name of the instance, we call frontend_port.value.name. We can also use frontend_port.key to retrieve the current iterator index and use it for the port.

# Terraform 0.12 Configuration. Some sections omitted for clarity.

resource "azurerm_application_gateway" "network" {

 dynamic "frontend_port" {
   for_each = azurerm_app_service.example
   content {
     name = "${frontend_port.value.name}-feport"
     port = "808${frontend_port.key}"
   }
 }

# omitted for clarity

 dynamic "backend_address_pool" {
   for_each = azurerm_app_service.example
   content {
     name  = "${backend_address_pool.value.name}-beap"
     fqdns = [backend_address_pool.value.default_site_hostname]
   }
 }
}

Remote State Storage with Terraform Cloud

When deploying our App Service instances and Application Gateway, we use Terraform Cloud’s remote state storage to manage their state. This reduces additional infrastructure configuration to store, version, and manage Terraform state files.

First, we sign up for Terraform Cloud and create an organization.

Snapshot of "Creating an org"

To leverage Terraform Cloud, we create a user token and insert it into the following ~/.terraformrc file. This will allow our CLI to call Terraform Cloud.

credentials "app.terraform.io" {
 token = ""
}

Next, we add the backend “remote” directive to our Terraform declaration.

terraform {
 required_version = "~> 0.12"
 backend "remote" {
   organization = "hashicorp-azurerm"

   workspaces {
     name = "prod"
   }
 }
}

When we run terraform init, Terraform Cloud generates a workspace under our organization.

snapshot of a workspace

After applying the configuration, Terraform Cloud reflects the state of our Azure resources and highlights the differences.

snapshot of the new state

More detailed instructions for Terraform Cloud use can be found on the HashiCorp blog. By leveraging Terraform Cloud, we can maintain the state of our App Service instances and Application Gateway without setting up additional state management.

Conclusion

In this post, we covered some of the new improvements to Terraform 0.12 and how to apply them to resources managed by the Azure Provider. In addition, we used Terraform Cloud to store the state of our Azure resources remotely as we upgrade our configuration.

For more information about Terraform 0.12, refer to HashiCorp’s documentation. Additional resource references for the Terraform Azure Provider can be found in our provider documentation. To use remote state management, go to Terraform Cloud to create an organization.

Other questions or feedback? Please let us know in the comments below.