Terraform is a powerful tool for managing infrastructure, and its clarity and readability are vital to ensuring that code remains maintainable and understandable over time. Proper formatting isn’t just an aesthetic choice; it’s a practice that enhances collaboration and minimizes errors. This guide explores three essential principles of formatting Terraform resources to promote clean, efficient, and well-structured configurations.

1. Maintaining the Scrimmage Line

When writing Terraform resources, a key principle is maintaining a clear and consistent “scrimmage line.” This metaphor emphasizes keeping resource attributes concise and aligned whenever possible. For simple resources with all attributes fitting neatly on a single line, it’s best to keep them compact and readable. For example:

resource "azurerm_resource_group" "main" {

  name     = "rg-${var.application_name}-${var.environment_name}-${random_string.main.result}"
  location = var.primary_region

}

Optionally, adding a new line after the opening curly brace and before the closing curly brace can improve readability, especially in larger files where whitespace helps visually separate sections of code. Maintaining this clean structure ensures that resources are visually approachable, especially for teams working collaboratively.

2. Highlighting Meta-Arguments

When dealing with count or for_each meta-arguments, it’s crucial to give them prominence by placing them on their own line. This approach intentionally breaks the scrimmage line to make these critical arguments stand out. For instance:

resource "github_repository_file" "bulk" {

  count = length(local.files)

  repository          = var.repository
  branch              = var.branch
  file                = local.files[count.index]
  content             = file("${path.module}/files/${local.files[count.index]}")
  commit_message      = "Managed by Terraform"
  commit_author       = var.commit_user.name
  commit_email        = var.commit_user.email
  overwrite_on_create = true

}

Adding a blank line around the meta-argument further enhances its visibility, helping it stand apart from surrounding attributes and blocks. This minor adjustment significantly improves code clarity, especially for collaborators who need to quickly understand the logic behind resource iterations. Properly highlighting meta-arguments ensures that they won’t be overlooked in larger configurations.

3. Separating Complex Blocks

When resources include complex attributes or blocks, it’s essential to format them in a way that preserves the integrity of the scrimmage line. Separating these blocks from the primary attribute list ensures that the core details of the resource remain easy to scan and comprehend. For example:

resource "github_repository_file" "pull_request_plan" {

  count = length(var.environments)

  repository          = var.repository
  branch              = var.branch
  file                = "atat-pull-request-plan-${var.environments[count.index]}.yaml"
  commit_message      = "Managed by Terraform"
  commit_author       = var.commit_user.name
  commit_email        = var.commit_user.email
  overwrite_on_create = true

  content = templatefile(
    "${path.module}/files/atat-pull-request-plan.yaml",
    {
      environment_name = var.environments[count.index]
    }
  )

}

Contrast this with a “corrupted” scrimmage line, where mixing simple attributes with complex blocks disrupts readability:

resource "github_repository_file" "pull_request_plan" {

  count = length(var.environments)

  repository = var.repository
  branch     = var.branch
  file       = "atat-pull-request-plan-${var.environments[count.index]}.yaml"
  content = templatefile(
    "${path.module}/files/atat-pull-request-plan.yaml",
    {
      environment_name = var.environments[count.index]
    }
  )
  commit_message      = "Managed by Terraform"
  commit_author       = var.commit_user.name
  commit_email        = var.commit_user.email
  overwrite_on_create = true

}

In this corrupted example, complex blocks blend with simpler attributes, making it harder to follow the resource’s logic and intent. By isolating complex blocks, you improve both readability and maintainability.

4. Organizing Lifecycle and Dependencies

Terraform offers optional arguments like lifecycle and depends_on, which provide additional configuration for resources. When using these arguments, they should always be placed at the bottom of the resource block, separate from the attribute configuration. This deliberate placement ensures they don’t disrupt the flow of the core attributes while clearly signaling their purpose. For example:

resource "aws_instance" "example" {
  ami           = var.ami_id
  instance_type = var.instance_type

  lifecycle {
    create_before_destroy = true
  }

  depends_on = [
    aws_security_group.example
  ]

}

Keeping these arguments at the bottom not only improves readability but also prevents them from blending with the main attributes. This separation makes it easier to quickly locate lifecycle rules or dependency configurations when reviewing code.

5. Treat Module ‘Source’ with the Reverence it Deserves

Modules are a powerful way to encapsulate reusable infrastructure patterns, but their formatting must also adhere to clear standards. The source attribute of a module is not any ordinary attribute. This special status should be recognized. The source attribute in a module block should always stand alone, much like the count meta-argument in resources. This ensures the source of the module is immediately visible and clear. For example:

module "network" {

  source = "./modules/network"

  vpc_id   = azurerm_virtual_network.example.id
  subnets  = var.subnets
  tags     = local.default_tags

}

When using iterators like count or for_each with a module, the source attribute should come first, followed by the iterator, with a new line in between to make this distinction abundantly clear. For instance:

module "network" {

  source = "./modules/network"

  for_each = var.vnets

  vpc_id   = each.value.id
  subnets  = each.value.subnets
  tags     = local.default_tags

}

This formatting keeps the configuration clean, ensures that the iterator doesn’t blend in with the rest of the module attributes, and helps others quickly identify the module’s source and logic.

6. Avoid Embedding Overly Complex Logic Inline

Complex logic embedded in a single line can make configurations harder to read, debug, and maintain. Instead, break out the logic into a nearby local variable for clarity and reusability. Consider this overly complex example:

resource "azurerm_network_security_rule" "example" {

  name                        = "example-rule"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_address_prefixes     = var.is_production ? ["10.0.0.0/16"] : ["0.0.0.0/0"]
  destination_address_prefix  = "*"
  source_port_range           = "*"
  destination_port_range      = var.is_production ? "443" : "80"

}

This can be refactored to use a local variable, significantly improving readability:

locals {
  ns_rule_config = var.is_production ? {
    port        = "443"
    source_ips  = ["10.0.0.0/16"]
  } : {
    port        = "80"
    source_ips  = ["0.0.0.0/0"]
  }
}

resource "azurerm_network_security_rule" "example" {
  name                        = "example-rule"
  priority                    = 100
  direction                   = "Inbound"
  access                      = "Allow"
  protocol                    = "Tcp"
  source_address_prefixes     = local.ns_rule_config.source_ips
  destination_address_prefix  = "*"
  source_port_range           = "*"
  destination_port_range      = local.ns_rule_config.port
}

By using a local, the logic is centralized, making it easier to understand and modify without sifting through multiple attributes. Make sure that the local is declared nearby. Meaning if it is only used in one resource, declare it directly above that resource so that the relationship is emphasized by its proximity. By extracting the logic into a local variable you achieve the following:

Improved Readability: The resource block now focuses on what it does, not the underlying logic. Reusability: The local variable can be reused in multiple places if needed.

Ease of Maintenance: Updating logic in the local variable affects all resources referencing it, simplifying changes.

Conclusion

By adhering to these principles — maintaining the scrimmage line, highlighting meta-arguments, separating complex blocks, organizing lifecycle and dependency arguments, formatting module blocks, and avoiding overly complex inline logic — Terraform configurations become functional, elegant, and highly maintainable. Thoughtful formatting communicates intent, enhances collaboration, and ensures your codebase is scalable and robust. Properly formatted Terraform code is not just a reflection of good engineering practices; it’s a foundation for long-term success in infrastructure-as-code.