Distributing WebAssembly components using OCI registries
As the cloud-native space keeps evolving at a rapid pace, WebAssembly is…
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.
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.
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.
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.
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 element
function.
# 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 } }
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] } } }
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.
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.
After applying the configuration, Terraform Cloud reflects the state of our Azure resources and highlights the differences.
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.
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.