Mastering Expressions In Terraform

In today’s blog post, we will discuss expressions in terraform. Terraform supports conditional, for, and splat expressions which help you to add conditional logic (if else conditions), loop through data using for expression, retrieve list items using splat expressions.

Conditional Expressions

A conditional expression in Terraform allows you to select one of two values based on the evaluation of a boolean condition. This is similar to the ternary operator found in many programming languages.

Syntax:

condition ? true_val : false_val
  • condition: Any expression that evaluates to a boolean (true or false).
  • true_val: The value returned if the condition is true.
  • false_val: The value returned if the condition is false.

Explanation: If the condition is true, the result of the expression is true_val. If the condition is false, the result is false_val. Terraform attempts automatic type conversions if true_val and false_val are of different types.

Example 1: Defining a Default Value

variable "instance_type" {
  description = "Type of the EC2 instance."
  type        = string
  default     = ""
}

resource "aws_instance" "example" {
  ami           = "ami-0abcdef1234567890"
  instance_type = var.instance_type == "" ? "t2.micro" : var.instance_type
  tags = {
    Name = "MyInstance"
  }
}

Explanation of the code: In this example, if the instance_type variable is an empty string, the aws_instance will be created with t2.micro. Otherwise, it will use the value provided in var.instance_type.

Example 2: Conditionally Setting a Tag

variable "environment" {
  description = "Deployment environment (e.g., 'prod', 'dev')."
  type        = string
}

resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-unique-bucket-${var.environment}"
  tags = {
    Environment = var.environment == "prod" ? "Production" : "Development"
    ManagedBy   = "Terraform"
  }
}

Explanation of the code: Here, the Environment tag for the S3 bucket is set to “Production” if var.environment is “prod”, otherwise it’s set to “Development”.

Common Use Cases:

  • Setting default values for variables or attributes.
  • Conditionally enabling or disabling features or resources (count argument).
  • Selecting resource attributes based on environment (e.g., development vs. production).
  • Dynamically adjusting resource configurations based on input.

For Expressions

For expressions in Terraform allow you to construct complex type values (lists, sets, tuples, or objects/maps) by transforming elements from another complex type value. This is powerful for data manipulation and dynamic configuration generation.

Syntax:

# For list/set/tuple comprehension
[for element in collection : expression]

# For object/map comprehension
{for key, value in collection : new_key => new_value}

List Comprehension

List comprehension creates a new tuple (which behaves like a list) by iterating over each element of a given collection and applying an expression to it.

Example 1: Transforming a List of Strings

variable "instance_names" {
  description = "A list of instance names."
  type        = list(string)
  default     = ["webserver", "appserver", "dbserver"]
}

output "uppercase_instance_names" {
  value = [for name in var.instance_names : upper(name)]
}

Explanation of the code: This for expression iterates through var.instance_names and transforms each name to uppercase, resulting in an output like ["WEBSERVER", "APPSERVER", "DBSERVER"].

Example 2: Filtering Elements from a List

variable "all_users" {
  type    = list(string)
  default = ["alice", "bob", "charlie-admin", "diana-admin"]
}

output "admin_users" {
  value = [for user in var.all_users : user if contains(user, "-admin")]
}

Explanation of the code: This example filters the all_users list, including only those names that contain “-admin”, producing ["charlie-admin", "diana-admin"]. The if clause acts as a filter.

Map Comprehension

Map comprehension creates a new object (which behaves like a map) by iterating over a collection and constructing key-value pairs from each element.

Example 1: Creating a Map from a List

variable "users" {
  description = "A list of user IDs."
  type        = list(string)
  default     = ["user123", "admin456", "guest789"]
}

output "user_roles" {
  value = { for user in var.users : user => "read-only" }
}

Explanation of the code: This for expression transforms the users list into a map where each user ID is a key, and its corresponding value is “read-only”. The output would resemble:

{
  "user123" = "read-only"
  "admin456" = "read-only"
  "guest789" = "read-only"
}

Example 2: Re-keying and Transforming a Map

locals {
  instance_specs = {
    "dev-web"  = { ami = "ami-dev-1", type = "t2.micro" }
    "prod-app" = { ami = "ami-prod-1", type = "t3.medium" }
  }
}

output "instance_ami_map" {
  value = { for name, spec in local.instance_specs : name => spec.ami }
}

Explanation of the code: This for expression iterates over instance_specs and creates a new map where the keys are the instance names and the values are their respective AMIs, resulting in:

{
  "dev-web" = "ami-dev-1"
  "prod-app" = "ami-prod-1"
}

Splat Expressions

Splat expressions provide a way for iterating over a list or set of complex objects and extracting a specific attribute from each of them. They are a shorthand for a common for expression pattern.

Important Note: Splat expressions apply only to lists, sets, and tuples. For maps or objects, you must use for expressions. Also, resources that use the for_each argument will appear as a map of objects, so splat expressions cannot be used directly with them.

List Splat ([*])

The [*] symbol iterates over all elements of the list given to its left and accesses the attribute name given on its right from each element. If a value is non-null, it transforms it into a single-element tuple. If the value is null, it returns an empty tuple.

Syntax:

list_of_objects[*].attribute_name

Example 1: Extracting IDs from a List of Objects

locals {
  ec2_instances = [
    { id = "i-0a1b2c3d4e5f6a7b8", name = "web-01" },
    { id = "i-0c9d8e7f6a5b4c3d2", name = "app-01" },
    { id = "i-0g5h4i3j2k1l0m9n8", name = "db-01" },
  ]
}

output "instance_ids" {
  value = local.ec2_instances[*].id
}

Explanation of the code: This splat expression extracts the id attribute from each object in the ec2_instances list, producing a list of IDs: ["i-0a1b2c3d4e5f6a7b8", "i-0c9d8e7f6a5b4c3d2", "i-0g5h4i3j2k1l0m9n8"].

Example 2: Accessing Nested Attributes

locals {
  vpcs = [
    { name = "main", cidr = "10.0.0.0/16", tags = { env = "prod" } },
    { name = "dev", cidr = "10.1.0.0/16", tags = { env = "dev" } },
  ]
}

output "vpc_envs" {
  value = local.vpcs[*].tags.env
}

Explanation of the code: This demonstrates accessing a nested attribute (env within tags) for each VPC, resulting in ["prod", "dev"].

Example 3: Extracting from Data Source Results

data "aws_security_groups" "web_sgs" {
  tags = {
    Purpose = "WebAccess"
  }
}

output "web_sg_ids" {
  value = data.aws_security_groups.web_sgs.ids[*]
}

Explanation of the code: This example uses a data source to fetch security groups tagged “WebAccess” and then uses a splat expression to easily extract a list of their IDs.

Attribute Splat (Legacy .*)

Earlier versions of Terraform had a slightly different version of splat expressions using the .* sequence. With the attribute-only splat expression, only the attribute lookups apply to each element of the input, and index operations are applied to the result of the iteration. While still supported for backward compatibility, [*] is generally preferred for clarity.

Example (Legacy):

# Equivalent to local.vpcs[*].tags.env in modern splat
output "vpc_envs_legacy" {
  value = local.vpcs.*.tags.env
}

Practical Use Cases

Expressions are fundamental to nearly every advanced Terraform configuration. Here are a few examples of how they can be combined:

Conditionally Create Resources: Use a conditional expression to decide whether to create a resource or module based on a boolean variable, leveraging the count argument.

variable "enable_logging" {
  description = "Whether to enable S3 logging bucket."
  type        = bool
  default     = true
}

resource "aws_s3_bucket" "log_bucket" {
  count  = var.enable_logging ? 1 : 0
  bucket = "my-app-logs-${terraform.workspace}"
  acl    = "log-delivery-write"
}

Explanation: The log_bucket will only be created if var.enable_logging is true.

Dynamically Generate Security Group Rules: Use for expressions to create multiple security group ingress rules from a list of CIDR blocks, often combined with for_each for resource iteration.

variable "allowed_web_cidrs" {
  type    = list(string)
  default = ["192.168.1.0/24", "10.0.0.0/8"]
}

resource "aws_security_group" "web_sg" {
  name        = "web-access-sg"
  description = "Allow web traffic"
  vpc_id      = "vpc-0abcdef1234567890"

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = var.allowed_web_cidrs # This is a direct list assignment
  }

  # Creating individual rules with for_each for more granular control
  dynamic "ingress" {
    for_each = var.allowed_web_cidrs
    content {
      from_port   = 443
      to_port     = 443
      protocol    = "tcp"
      cidr_blocks = [ingress.value]
    }
  }
}

Explanation: The first ingress block directly takes a list of CIDRs. The second dynamic "ingress" block uses a for_each loop with var.allowed_web_cidrs to create separate HTTPS rules for each CIDR, demonstrating how for can build blocks.

Extracting and Transforming Information from Data Sources: Use splat expressions to easily get specific attributes from a list of data source results, then potentially transform them with for expressions.

data "aws_instances" "web_servers" {
  instance_tags = {
    Role = "webserver"
  }
}

output "web_server_private_ips" {
  value = data.aws_instances.web_servers.private_ips[*] # Splat to get list of IPs
}

output "web_server_id_to_ip_map" {
  value = { for i, ip in data.aws_instances.web_servers.private_ips : data.aws_instances.web_servers.ids[i] => ip }
}

Explanation: web_server_private_ips uses splat to get a list of all private IPs. web_server_id_to_ip_map combines for and data source outputs to create a map of instance IDs to their private IPs.

Managing Multiple Environments with Conditionals and Maps: Using conditional logic to select values from a map based on the current workspace or variable.

variable "env_configs" {
  type = map(object({
    instance_count = number
    vpc_cidr       = string
  }))
  default = {
    "dev" = {
      instance_count = 1
      vpc_cidr       = "10.0.0.0/16"
    },
    "prod" = {
      instance_count = 3
      vpc_cidr       = "10.10.0.0/16"
    }
  }
}

resource "aws_vpc" "app_vpc" {
  cidr_block = var.env_configs[terraform.workspace].vpc_cidr
}

resource "aws_instance" "app_server" {
  count         = var.env_configs[terraform.workspace].instance_count
  ami           = "ami-0abcdef1234567890"
  instance_type = terraform.workspace == "prod" ? "m5.large" : "t2.micro"
}

Explanation: This setup dynamically configures vpc_cidr and instance_count based on the current Terraform workspace by looking up values in the env_configs map. It also conditionally sets the instance_type for the app_server based on the workspace.

Best Practices for Mastering Expressions

To write clear, maintainable, and effective Terraform configurations using expressions, consider these best practices:

  • Readability First: While expressions can be concise, prioritize readability. Break down complex expressions into smaller, more manageable parts using local values. Overly long or nested expressions can be difficult to debug.
  • Leverage Locals: For expressions used multiple times or those that are particularly complex, define them as locals. This improves reusability, makes your code DRY (Do not Repeat Yourself), and enhances readability by giving meaningful names to intermediate computations.
  • Type Consistency: Be mindful of the data types involved in your expressions. While Terraform performs some automatic conversions, explicit type conversions (tolist(), tomap(), tostring(), tonumber(), toset()) can prevent unexpected errors and make your intent clearer.
  • Validation Rules: Combine expressions with validation rules in input variables or outputs to ensure that the values provided to your modules are within expected bounds or formats, providing immediate feedback to users.
  • Thorough Testing: Expressions can sometimes lead to subtle bugs, especially when dealing with complex logic or data transformations. Always test your configurations thoroughly, especially when using complex expressions, to ensure they produce the desired outcome across various scenarios. Use terraform console for quick testing of expressions.
  • Start Simple: If you are new to expressions, begin with basic conditional and for expressions before attempting more intricate scenarios or splat expressions. Gradually build complexity as your understanding grows.

Conclusion

Expressions are the backbone of dynamic and powerful Terraform configurations. By mastering conditional expressions for decision-making, for expressions for data transformation, and splat expressions for concise attribute extraction, you unlock the full potential of Terraform. These tools enable you to write more flexible, scalable, and maintainable infrastructure as code, adapting to various environments and requirements with ease.

Related Items

Author

Debjeet Bhowmik

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins. In my free time, I write blogs on ckdbtech.com

Leave a Comment