Day 33: Mastering Terraform HCL Feature

Adrian Rubico

|

May 9, 2025

08:36 PM GMT+8

Day 33: Mastering Terraform HCL Feature

Today, we will dive into powerful features of HashiCorp Configuration Language (HCL) that improve the way we write and manage Terraform code. We will explore how to use count, for_each, lifecycle, and dynamic blocks to make your infrastructure more flexible, scalable, and maintainable.

Task

Clone the following repository and navigate to the correct directory named 05-HCL:

bash
git clone https://github.com/git-adrianrubico/learn-terraform
cd learn-terraform/05-HCL

The main.tf file is configured to create a storage account using HCL features:

hcl
resource "azurerm_resource_group" "rg-hcl-example" {
  name     = "rg-hcl-example"
  location = var.azregion
  tags = {
    environment = local.env
  }
}

resource "azurerm_storage_account" "sa-example" {
  name                     = "stgacsample${local.env}01"
  resource_group_name      = azurerm_resource_group.rg-hcl-example.name
  location                 = azurerm_resource_group.rg-hcl-example.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  
  tags = {
    environment = local.env
  }
}

Using count

The count parameter allows you to create multiple instances of a resource using a single block.

Here’s an example where two storage accounts are created with different names by using count = 2:

hcl
resource "azurerm_storage_account" "sa-example" {
  count                    = 2
  name                     = "stgacsample${local.env}${count.index}"
  resource_group_name      = azurerm_resource_group.rg-hcl-example.name
  location                 = azurerm_resource_group.rg-hcl-example.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  
  tags = {
    environment = local.env
  }
}

We also update the output.tf file to access one of the created instances:

hcl
output "sa_name" {
  value = azurerm_storage_account.sa-example[0].name
}

Here, we use [0] to reference the first instance from the list of storage accounts created using count. This is useful when you want to retrieve a specific instance from the list.

Output of terraform count feature

Picture below shows the two storage account created:

Two storage account created

Using for_each

The for_each meta-argument is used when iterating over a map or set of strings. This allows you to deploy resources based on key-value pairs.

In the example below, we use for_each with a map of replication types:

  • each.key is used to construct a unique name for each storage account.
  • each.value sets the appropriate replication type for each resource.
hcl
resource "azurerm_storage_account" "sa-example" {
  for_each = {
    lrs = "LRS"
    grs = "GRS"
  }
  name                     = "stgacsample${local.env}${each.key}"
  resource_group_name      = azurerm_resource_group.rg-hcl-example.name
  location                 = azurerm_resource_group.rg-hcl-example.location
  account_tier             = "Standard"
  account_replication_type = each.value
  
  tags = {
    environment = local.env
  }
}

To output one of the created resources, we can convert the map into a list using the values() function:

hcl
output "sa_name" {
  value = values(azurerm_storage_account.sa-example)[0].name
}

Here, values(...) converts the map of storage accounts into a list, and [0] accesses the first resource from that list. This approach is especially useful when you only need to refer to one resource created with for_each.

Output of terraform for_each feature

Two storage account created for both LRS & GRS:

Two storage account created by using terraform for_each

Terraform Lifecycle

Terraform lifecycle blocks give you control over how resources are treated during the apply or destroy phases.

hcl
resource "azurerm_resource_group" "rg-hcl-example" {
  name     = "rg-hcl-example"
  location = var.azregion
  tags = {
    environment = local.env
  }
  
  lifecycle {
    prevent_destroy = true
  }
}
Terraform Lifecycle prevent destroy

This configuration prevents accidental deletion of the resource group by requiring manual intervention to remove it.

💡Note: There are many lifecycle meta-arguments to explore that can help you regulate how Terraform handles your resources. Check out the full documentation here: Terraform Lifecycle Meta-Arguments

Using dynamic blocks

Terraform's dynamic block is useful when you want to create repeated nested blocks based on a list or map. This is especially handy for resources like Network Security Groups (NSGs) that may contain multiple security_rule blocks.

Below is an example where we define two NSG rules dynamically using a local variable:

main.tf

hcl
resource "azurerm_network_security_group" "nsg-example" {
  name                = "nsg-example"
  location            = azurerm_resource_group.rg-hcl-example.location
  resource_group_name = azurerm_resource_group.rg-hcl-example.name
  dynamic "security_rule" {
    for_each = local.nsgrules
    content {
      name                       = security_rule.value.name
      priority                   = security_rule.value.priority
      direction                  = security_rule.value.direction
      access                     = security_rule.value.access
      protocol                   = security_rule.value.protocol
      source_port_range          = security_rule.value.source_port_range
      destination_port_range     = security_rule.value.destination_port_range
      source_address_prefix      = security_rule.value.source_address_prefix
      destination_address_prefix = security_rule.value.destination_address_prefix
    }
  }
}

locals.tf

hcl
locals {
  env = "dev"
  
  nsgrules = [{
    name                       = "HTTP"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "80"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
    },
    {
      name                       = "HTTPS"
      priority                   = 101
      direction                  = "Inbound"
      access                     = "Allow"
      protocol                   = "Tcp"
      source_port_range          = "*"
      destination_port_range     = "443"
      source_address_prefix      = "*"
      destination_address_prefix = "*"
  }]
}

output.tf

hcl
output "nsg_name" {
  value = azurerm_network_security_group.nsg-example.name
}
  • We define a local variable local.nsgrules that holds a list of security rules.
  • The dynamic "security_rule" block iterates over each rule in local.nsgrules using for_each.
  • Inside the content {} block, we assign values to each required NSG rule attribute using security_rule.value.

This pattern is extremely powerful for managing multiple similar blocks without duplicating code, especially in environments where rules change frequently or are sourced from external data.

Terraform apply dynamic block NSG

Verify Network Security Group rules:

Verify Network Security Group rules

Conclusion

We discussed how HCL features such as for_each, lifecycle, and dynamic blocks simplify and scale Terraform code. In addition to improving the way you define and manage infrastructure, these tools help you follow best practices at the same time.

We will discuss Terraform drift in the next blog, along with how to detect and manage changes made outside of Terraform.

Discussion