Terraform Strings: The Complete Guide

Master Terraform strings: learn advanced templating, heredocs, and built-in functions for cleaner, reusable HCL code in minutes.

String Basics: Defining and Quoting

In Terraform, strings represent text sequences used throughout your configurations. You'll use strings constantly: naming resources, setting tags, writing user data scripts, and defining outputs. The HashiCorp Configuration Language (HCL) provides flexible ways to define and manipulate these strings.

Quoted String Literals

The most straightforward way to define strings is using double quotes (").

variable "instance_name_prefix" {
  type        = string
  default     = "my-app-server"
  description = "The prefix for server names."
}

output "greeting_message" {
  value = "Hello, Terraform World!"
}

Quoted strings work well for single-line text and simple values. However, they become unwieldy when you need multiline content or complex text blocks.

Heredoc String Literals

When you need multiline strings like shell scripts, JSON policies, or YAML configurations, heredocs provide a cleaner syntax inspired by Unix shells.

Syntax:

<<DELIMITER
content here
DELIMITER

Indented Heredocs (<<-DELIMITER)

This is the preferred form for cleaner HCL code. The indented heredoc strips common leading whitespace from all lines, keeping your code readable without affecting the string content.

resource "aws_instance" "web_server" {
  user_data = <<-EOF
    #!/bin/bash
    apt-get update
    apt-get install -y nginx
    systemctl start nginx
    echo "Nginx installed" > /tmp/status.txt
  EOF
}

The leading indentation in the HCL code isn't included in the actual script—a huge win for readability.

Standard Heredocs (<<DELIMITER)

Standard heredocs preserve all leading whitespace. Use these only when you specifically need indentation in the output.

resource "local_file" "standard_heredoc" {
  filename = "${path.module}/standard.txt"
  content  = <<EOF
    This line is indented in the HCL.
      And this line will have its indentation preserved.
EOF
}

Choosing Delimiters

While EOF is conventional, you can use any valid identifier. This is helpful if your content contains the word "EOF":

content = <<-ENDSCRIPT
  # Script content containing "EOF"
ENDSCRIPT

Escape Sequences and Special Characters

When your string needs to include special characters, use escape sequences starting with a backslash ().

Common Escape Sequences

Sequence Meaning
\n Newline
\r Carriage return
\t Tab
\" Literal double quote
\\ Literal backslash

Example: Complex Messages

locals {
  complex_message = "He said, \"Terraform is awesome!\"\nThis is on a new line."
}

# Renders as:
# He said, "Terraform is awesome!"
# This is on a new line.

Escaping Interpolation

To include literal ${ in your string (often needed for shell variables or templates), use $${:

resource "local_file" "shell_script" {
  filename = "${path.module}/env_script.sh"
  content  = <<-EOF
    #!/bin/bash
    # This is interpolated by Terraform:
    echo "AWS Region: ${var.aws_region}"

    # This becomes a literal shell variable:
    echo "Current user: $${USER}"
    echo "Home: $${HOME}/data"
  EOF
}

In the resulting script, $${USER} becomes ${USER}, which the shell interprets at runtime.


String Interpolation and Directives

String interpolation makes your strings dynamic by embedding Terraform expressions directly within them.

Basic Interpolation Syntax

The syntax is straightforward: ${expression}. Terraform evaluates the expression and inserts the result into the string.

variable "environment" {
  type    = string
  default = "dev"
}

output "instance_name" {
  value = "app-server-${var.environment}-01"
}
# Output: "app-server-dev-01"

What You Can Interpolate

Input Variables:

variable "aws_region" {
  type    = string
  default = "us-east-1"
}

output "selected_region" {
  value = "Deploying to the ${var.aws_region} region."
}

Resource Attributes:

resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"
}

output "instance_id_info" {
  value = "The new instance ID is: ${aws_instance.example.id}"
}

List/Map Elements:

variable "availability_zones" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

variable "server_config" {
  type = map(string)
  default = {
    "cpu" = "2"
    "ram" = "4GB"
  }
}

output "primary_az" {
  value = "Primary AZ: ${var.availability_zones[0]}"
}

output "server_ram" {
  value = "Server RAM: ${var.server_config["ram"]}"
}

Loop Variables (count.index, each.key, each.value):

variable "user_vms" {
  type = map(string)
  default = {
    "alice" = "t2.small"
    "bob"   = "t2.medium"
  }
}

resource "aws_instance" "user_specific_vms" {
  for_each      = var.user_vms
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = each.value

  tags = {
    Name  = "vm-for-${each.key}"
    Owner = each.key
  }
}

Function Results:

output "module_path" {
  value = "This module is located at: ${path.module}"
}

Basic Arithmetic:

variable "base_app_port" {
  type    = number
  default = 8080
}

output "app_ports" {
  value = "App 1: ${var.base_app_port}, App 2: ${var.base_app_port + 1}"
}

Heredocs and Multiline Strings

Heredocs shine when managing complex multiline content. Combined with string interpolation and template directives, they're powerful for generating configuration files and scripts.

Indented Heredoc Best Practices

resource "aws_instance" "web_server" {
  # Clean indentation in HCL source code
  user_data = <<-EOF
    #!/bin/bash
    set -e

    # Update system
    apt-get update
    apt-get upgrade -y

    # Install dependencies
    apt-get install -y \
      nginx \
      curl \
      wget

    # Start services
    systemctl enable nginx
    systemctl start nginx

    echo "Initialization complete at $(date)" >> /var/log/user-data.log
  EOF
}

The <<- variant is almost always preferable to << because it maintains code readability without affecting the output.

Combining Heredocs with Interpolation

variable "environment" {
  type    = string
  default = "staging"
}

variable "app_port" {
  type    = number
  default = 8080
}

resource "aws_instance" "app_server" {
  user_data = <<-EOF
    #!/bin/bash
    ENVIRONMENT="${var.environment}"
    PORT="${var.app_port}"

    echo "Deploying to $${ENVIRONMENT} on port $${PORT}"
    # Application startup logic here
  EOF
}

String Functions

Terraform provides a rich set of built-in functions for string manipulation. These work within interpolations, local values, outputs, and function calls.

String Transformation Functions

format(spec, values...)

Creates a formatted string using printf-style syntax:

> format("Server: %s, IP: %s, Cores: %d", "web01", "10.0.1.5", 4)
"Server: web01, IP: 10.0.1.5, Cores: 4"

locals {
  instance_info = format(
    "Instance %s (%s) in %s",
    var.instance_name,
    var.instance_type,
    var.aws_region
  )
}

join(separator, list) and split(separator, string)

Join combines list elements with a separator; split does the inverse:

> join("-", ["app", "prod", "web", "01"])
"app-prod-web-01"

> split(".", "www.example.com")
["www", "example", "com"]

locals {
  resource_name = join("-", [var.project, var.environment, var.resource_type])
  name_parts    = split("-", local.resource_name)
}

replace(string, search, replacement)

Replaces occurrences of a substring (supports regex):

> replace("my_app_v1.0", "_", "-")
"my-app-v1.0"

> replace("user_id_123", "/(\\d+)$/", "-num-$1")
"user-num-123"

locals {
  sanitized_name = replace(var.user_input, "/[^a-z0-9-]/", "")
}

lower(string) and upper(string)

Convert case—essential for resources with case-sensitive naming (e.g., S3 buckets):

> lower("MyAwesomeBucket")
"myawesomebucket"

> upper("dev-instance")
"DEV-INSTANCE"

locals {
  bucket_name = lower("${var.company}-${var.project}-${data.aws_caller_identity.current.account_id}")
}

substr(string, offset, length)

Extract substrings (0-indexed):

> substr("abcdefgh", 2, 3)
"cde"

> substr("HelloTerraform", 0, 5)
"Hello"

locals {
  env_short = lower(substr(var.environment, 0, 3))  # "production" -> "pro"
}

Whitespace Trimming Functions

chomp(string)

Removes trailing newlines (\n, \r\n):

> chomp("some text\n")
"some text"

locals {
  clean_content = chomp(file("${path.module}/config.txt"))
}

trimsuffix(string, suffix) and trimprefix(string, prefix)

Remove specific suffixes or prefixes:

> trimsuffix("backup_final.zip", ".zip")
"backup_final"

> trimprefix("project-app-server", "project-")
"app-server"

locals {
  domain_without_extension = trimsuffix(var.domain, ".com")
  service_name            = trimprefix(var.resource_name, "svc-")
}

trimspace(string)

Removes leading and trailing Unicode whitespace:

> trimspace("   hello world   \n")
"hello world"

locals {
  clean_input = trimspace(var.user_provided_value)
}

String Matching Functions

These return booleans and are useful in conditionals:

> startswith("http://example.com", "http://")
true

> endswith("main.tfvars", ".tfvars")
true

> strcontains("production-database-primary", "database")
true

locals {
  is_https = startswith(var.api_url, "https://")
  is_prod  = strcontains(var.environment, "prod")
}

Regular Expression Functions

Terraform uses RE2 syntax (no PCRE features like backreferences):

regex(pattern, string)

Returns the first match or capture group; errors if no match:

> regex("v([0-9]+\\.[0-9]+)", "product-version v1.23-beta")
["1.23"]

locals {
  version = regex("v([0-9.]+)", var.release_tag)[0]
}

regexall(pattern, string)

Returns all non-overlapping matches:

> regexall("\\b[a-z]{3}\\b", "cat sat on the mat")
["cat", "sat", "mat"]

locals {
  all_ips = regexall("[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+", var.log_content)
}

Unicode Considerations

Terraform handles strings as Unicode characters, not bytes. This matters for length and substring operations:

> length("Hello")
5

> length("你好")  # Chinese characters
2          # Not 6 bytes

# When length matters, always consider your character set

Template Directives: Conditionals and Loops

Template directives extend heredocs with control flow, enabling conditional logic and iteration directly within strings.

Conditional Logic: %{if condition}

The if directive includes parts of a string based on a boolean condition:

%{if <CONDITION>}
  content if true
%{else}
  content if false (optional)
%{endif}

The %{else} block is optional. If the condition is false and no else exists, nothing is rendered for that directive.

Example: Environment-Specific Deployment Message

variable "environment" {
  type    = string
  default = "staging"
}

resource "local_file" "deployment_notes" {
  filename = "${path.module}/deployment_notes.txt"
  content  = <<-EOT
    Deployment Details:
    Target Environment: ${var.environment}

    %{if var.environment == "production"}
    IMPORTANT: This is a PRODUCTION deployment.
    - Verify all pre-flight checks are complete
    - Monitor dashboards post-deployment
    - Have a rollback plan ready
    %{else if var.environment == "staging"}
    This is a STAGING deployment.
    Ideal for final testing before production.
    %{else}
    This is a DEVELOPMENT or QA deployment.
    Feel free to experiment. Data may be wiped.
    %{endif}

    Deployment initiated at: ${timestamp()}
  EOT
}

Iteration and Loops: %{for item in collection}

The for directive generates repeated template content for each element in a collection:

For Lists:

%{for ITEM in COLLECTION}
  content using ${ITEM}
%{endfor}

%{for INDEX, ITEM in COLLECTION}
  content using ${INDEX} and ${ITEM}
%{endfor}

For Maps:

%{for KEY, VALUE in MAP}
  content using ${KEY} and ${VALUE}
%{endfor}

Example: Generating Access Rules

variable "readonly_users" {
  type    = list(string)
  default = ["auditor", "guest_viewer", "support_tier1"]
}

output "user_access_policy" {
  value = <<-POLICY
    # Read-Only User Access
    %{for user_name in var.readonly_users}
    define_access {
      user   = "${user_name}"
      role   = "read_only"
      status = "active"
    }
    %{endfor}
  POLICY
}

Example: Generating Firewall Rules from a Map

variable "service_ports" {
  type = map(number)
  default = {
    "http"  = 80
    "https" = 443
    "ssh"   = 22
  }
}

output "firewall_config" {
  value = <<-RULES
    # Service Port Configuration
    %{for service, port in var.service_ports}
    firewall_rule {
      name        = "allow_${service}"
      port        = ${port}
      protocol    = "tcp"
      action      = "accept"
      description = "Allow incoming ${upper(service)} traffic"
    }
    %{endfor}
  RULES
}

Managing Whitespace with the Tilde Modifier

A common challenge with directives in heredocs is unwanted whitespace. The tilde (~) modifier controls this:

  • %{for item in list ~} removes whitespace following the directive on that line
  • ~%{endfor} removes whitespace preceding the directive on that line

Before: Unstripped Output

variable "features_enabled" {
  type    = list(string)
  default = ["feature_x", "feature_y", "feature_z"]
}

output "feature_list_unstripped" {
  value = <<-LIST
    Enabled Features:
    %{ for feature in var.features_enabled }
    - ${feature}
    %{ endfor }
  LIST
}

# Output has extra newlines:
# Enabled Features:
#
# - feature_x
#
# - feature_y
#
# - feature_z

After: With Tilde Modifier

output "feature_list_stripped" {
  value = <<-LIST
    Enabled Features:
    %{ for feature in var.features_enabled ~}
    - ${feature}
    %{ endfor ~}
  LIST
}

# Clean output:
# Enabled Features:
# - feature_x
# - feature_y
# - feature_z

The tilde is powerful but requires careful testing—always verify your output!


The templatefile Function

The templatefile function separates template content from Terraform code, improving maintainability for complex string generation.

The Problem: Mixing Logic and Configuration

Large heredocs with extensive interpolation and directives can clutter your main configuration:

resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  user_data = <<-EOT
    #!/bin/bash
    echo "Hello, ${var.environment}!"
    %{for port in var.open_ports}
    firewall-cmd --permanent --add-port=${port}/tcp
    %{endfor}
    firewall-cmd --reload
  EOT
}

The Solution: Separate Template Files

Create a .tftpl template file (e.g., templates/user_data.tftpl):

#!/bin/bash
echo "Hello, ${environment}!"
%{for port in open_ports}
firewall-cmd --permanent --add-port=${port}/tcp
%{endfor}
firewall-cmd --reload

Then reference it in your configuration:

resource "aws_instance" "example" {
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  user_data = templatefile("${path.module}/templates/user_data.tftpl", {
    environment = var.environment
    open_ports  = var.open_ports
  })
}

Benefits

  • Cleaner Configuration: Your main .tf files remain focused on resource definitions
  • Template Reusability: Share the same template across multiple resources
  • Easier Maintenance: Edit templates without touching Terraform code
  • Better IDE Support: Some editors provide syntax highlighting for .tftpl files

Template Variable Mapping

The second argument to templatefile() is a map of template variables. Template variables use simple names (no var. prefix):

# Template file: templates/config.tftpl
database_host = "${db_host}"
database_port = ${db_port}
database_name = "${db_name}"

# Terraform code:
local_file.config = {
  content = templatefile("${path.module}/templates/config.tftpl", {
    db_host = aws_db_instance.main.endpoint
    db_port = aws_db_instance.main.port
    db_name = aws_db_instance.main.name
  })
}

Advanced Templating Patterns

Using jsonencode and yamlencode for Configuration Data

CRITICAL: Always use jsonencode() and yamlencode() for generating JSON or YAML instead of manually constructing them with heredocs and interpolation. Manual construction is error-prone due to strict syntax requirements (quotes, commas, braces, YAML indentation).

Incorrect Approach (Error-Prone):

resource "aws_iam_role_policy" "example" {
  name = "example-policy"
  role = aws_iam_role.example.id

  # Manual JSON - risky!
  policy = <<-EOF
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["s3:GetObject"],
          "Resource": ["arn:aws:s3:::${var.bucket_name}/*"]
        }
      ]
    }
  EOF
}

Correct Approach:

resource "aws_iam_role_policy" "example" {
  name = "example-policy"
  role = aws_iam_role.example.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect   = "Allow"
        Action   = ["s3:GetObject"]
        Resource = ["arn:aws:s3:::${var.bucket_name}/*"]
      }
    ]
  })
}

The jsonencode() function handles all escaping and formatting automatically, ensuring valid output.

Combining Multiple Features

A real-world example combining heredocs, interpolation, directives, and functions:

variable "services" {
  type = map(object({
    port    = number
    enabled = bool
    tags    = list(string)
  }))
  default = {
    web = {
      port    = 80
      enabled = true
      tags    = ["frontend", "public"]
    }
    api = {
      port    = 8080
      enabled = true
      tags    = ["backend"]
    }
    admin = {
      port    = 9000
      enabled = false
      tags    = ["admin", "internal"]
    }
  }
}

resource "local_file" "docker_compose" {
  filename = "${path.module}/docker-compose.yml"
  content  = yamlencode({
    version = "3.8"
    services = {
      for name, config in var.services :
      name => {
        image   = "myapp:${name}"
        port    = ["${config.port}:${config.port}"]
        enabled = config.enabled
        labels = {
          for tag in config.tags :
          "service.tag" => tag
        }
      } if config.enabled
    }
  })
}

Using Locals for Intermediate Steps

Breaking complex string construction into readable pieces:

locals {
  project_code_upper = upper(var.project_code)
  env_short          = lower(substr(var.environment, 0, 3))
  random_suffix      = random_id.server_suffix.hex

  # Now the final string is clear
  server_name = "${local.project_code_upper}-${local.env_short}-web-${local.random_suffix}"
}

resource "random_id" "server_suffix" {
  byte_length = 4
}

resource "aws_instance" "web" {
  tags = {
    Name = local.server_name
  }
}

Common Pitfalls and Solutions

A. Whitespace and Indentation Issues

Problem: Unwanted leading/trailing whitespace from heredocs or directives breaking generated scripts or YAML.

Solutions: - Always use indented heredocs (<<-EOF) for multiline strings - Master the tilde (~) modifier for directives - Ensure closing delimiters are on their own line

B. Misunderstanding Function Arguments and Returns

Problem: Passing wrong argument types (e.g., string to join() instead of a list) or misinterpreting what a function returns.

Solutions: - Consult the official Terraform documentation for each function - Use terraform console to experiment with functions and inputs - Test locally before deploying

C. Complex Regular Expressions

Problem: Regex patterns that are hard to read, debug, and maintain. Remember: Terraform uses RE2 syntax (no backreferences).

Solutions: - If a simpler string function works, use it - Keep regex patterns concise and well-commented - Test RE2 patterns specifically—don't assume PCRE compatibility

D. Escaping Special Characters

Problem: - Forgetting \" for double quotes in quoted strings - Forgetting $${ for literal ${ or %%{ for literal %{ in heredocs - Regex backslashes: a literal \ in regex needs \\ in HCL strings - Trying to use \ for escapes in heredocs (it's usually literal)

Solutions: - Understand context-specific escaping rules - Test outputs carefully - Use terraform console to verify string escaping

E. Syntax Errors in Multiline Strings

Problem: Typos in directive keywords (%{endfoor}), missing closing directives, or other HCL errors in large string blocks.

Solutions: - Proofread carefully - Use an IDE with good HCL syntax highlighting and linting - Run terraform validate regularly

F. Forgetting Input Validation

Problem: Unvalidated input variables interpolated into strings can create malformed outputs or security issues.

Solutions: - Use validation blocks for input variables - Enforce constraints (allowed values, regex patterns, ranges) - Document expected formats

variable "instance_name" {
  type    = string
  default = "app"

  validation {
    condition     = can(regex("^[a-z0-9-]{1,32}$", var.instance_name))
    error_message = "Instance name must be 1-32 lowercase alphanumeric characters and hyphens."
  }
}

Best Practices for 2026

1. Prioritize Readability and Maintainability

  • Use terraform fmt consistently: Run it regularly to standardize formatting
  • Choose clarity over cleverness: Avoid tangled one-liners
  • Add strategic comments: Especially for complex regex, directives, or non-obvious logic
# Good: Clear intent
locals {
  # Regex extracts version number from semantic versioning string
  version = regex("^v([0-9]+\\.[0-9]+)", var.release_tag)[0]
}

# Not ideal: Cryptic and hard to maintain
locals {
  v = regex("^v([0-9]+\\.[0-9]+)", var.release_tag)[0]
}

2. Keep Expressions and Interpolations Simple

Avoid multiple ternaries in one string:

# Not recommended:
value = var.env == "prod" ? "production" : var.env == "staging" ? "staging" : "development"

# Better: Use if directives or locals
locals {
  env_label = (
    var.env == "prod" ? "production" :
    var.env == "staging" ? "staging" :
    "development"
  )
}

value = local.env_label

Break complex constructions into readable pieces:

locals {
  tags = {
    Environment = var.environment
    Project     = var.project_name
    Owner       = var.owner_email
    CostCenter  = var.cost_center
  }

  name_parts = [
    local.tags["Project"],
    local.tags["Environment"],
    random_id.suffix.hex
  ]

  resource_name = join("-", local.name_parts)
}

3. Choose the Right Tool for the Job

Tool Use When Example
Interpolation ${...} Simple value insertion "name-${var.environment}"
Directives %{...} Conditional logic or loops in heredocs Multi-line config generation
Functions Specific transformations lower(), join(), regex()
format() Need argument reordering/specific formatting Complex formatted output
jsonencode/yamlencode Generating JSON/YAML Policy documents, configuration files
templatefile() Large, reusable templates External script templates

4. Always Use jsonencode/yamlencode for Configuration Data

# YES: Guaranteed valid output
resource "aws_iam_policy" "s3_access" {
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = "s3:*"
      Resource = "*"
    }]
  })
}

# NO: Error-prone manual construction
# resource "aws_iam_policy" "s3_access" {
#   policy = <<-EOF
#     {
#       "Version": "2012-10-17",
#       "Statement": [{
#         "Effect": "Allow",
#         "Action": "s3:*",
#         "Resource": "*"
#       }]
#     }
#   EOF
# }

5. Use Consistent Naming and Explicit Declarations

variable "database_instance_identifier" {
  type        = string
  default     = "production-mysql"
  description = "The identifier for the RDS database instance"

  validation {
    condition     = can(regex("^[a-z0-9-]{1,63}$", var.database_instance_identifier))
    error_message = "Must be lowercase alphanumeric and hyphens, 1-63 characters"
  }
}

# Parameterize, don't hardcode
locals {
  db_name = var.database_instance_identifier
}

6. Validate Early and Often

# During development
terraform validate

# Before committing
terraform plan -out=tfplan

# Before applying
terraform show tfplan

7. Master Whitespace Control in Heredocs

# Use indented heredocs (<<-) and the tilde modifier (~) strategically
output "clean_yaml" {
  value = <<-YAML
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: ${var.config_name}
    data:
      config.json: |
        %{for key, value in var.config_items ~}
        "${key}": "${value}"
        %{endfor ~}
  YAML
}

8. Separate Templates from Code

Keep your main .tf files focused on infrastructure resources. Store template content in .tftpl files:

project/
├── main.tf
├── variables.tf
├── outputs.tf
├── templates/
│   ├── user_data.tftpl
│   ├── cloud_init.yaml
│   └── policy_document.json
└── locals.tf

9. Test String Outputs in terraform console

terraform console

> var.environment
"production"

> format("Instance: %s-%d", "web", 1)
"Instance: web-1"

> join("-", ["app", var.environment, "server"])
"app-production-server"

> regex("[0-9]+", "version-2024-12-01")
["2024"]

# Exit with 'exit'

10. Use Data Sources for Dynamic Values

Instead of hardcoding or manually constructing complex values, use data sources:

data "aws_availability_zones" "available" {
  state = "available"
}

locals {
  # Dynamically build AZ list from real AWS data
  azs = data.aws_availability_zones.available.names

  # Use in string interpolation
  primary_az = local.azs[0]
}

Conclusion

Mastering Terraform strings is a critical step toward becoming a proficient infrastructure engineer. From simple quoted strings to complex heredocs with template directives and built-in functions, you now have a complete toolkit for dynamic, maintainable configurations.

Key Takeaways

  1. Prioritize Clarity: Write string logic that's easy to understand
  2. Choose Wisely: Select the right feature (interpolation, directives, functions) for the task
  3. Control Whitespace: Master <<-EOF and the tilde (~) modifier
  4. Automate and Validate: Use terraform fmt and terraform validate consistently
  5. Test Outputs: Especially for complex generated strings
  6. Always Use jsonencode/yamlencode: For JSON and YAML, never manually construct
  7. Separate Concerns: Use .tftpl files for large templates
  8. Validate Inputs: Ensure variables meet expected formats

With these practices, you can build elegant, robust, and highly reusable Terraform configurations that scale with your infrastructure.

Happy Terraforming!