Understanding Terraform functions, expressions, and meta-arguments - AWS Prescriptive Guidance

Understanding Terraform functions, expressions, and meta-arguments

One criticism of IaC tools that use declarative configuration files rather than common programming languages is that they make it more difficult to implement custom programmatic logic. In Terraform configurations, this issue is addressed by using functions, expressions, and meta-arguments.

Functions

One of the great advantages to using code to provision your infrastructure is the ability to store common workflows  and reuse them again and again, often passing different arguments each time. Terraform functions are similar to AWS CloudFormation intrinsic functions, although their syntax is more similar to how functions are called in programmatic languages. You might have already noticed some Terraform functions, such as like substr, concat,length, and base64decode, in the examples in this guide. Like CloudFormation with intrinsic functions, Terraform has a series of built-in functions that are available for use in your configurations. For example, if a particular resource attribute takes a very large JSON object that would be inefficient to paste directly into the file, you could put the object in a .json file and use Terraform functions to access it. In the following example, the file function returns the contents of the file in string form, and then the jsondecode function converts it into an object type.

resource "example_resource" "example_resource_name" { json_object = jsondecode(file("/path/to/file.json")) }

Expressions

Terraform also allows for conditional expressions, which are similar to CloudFormation condition functions except that they use the more traditional ternary operator syntax. In the following example, the two expressions return the exact same result. The second example is what Terraform calls a splat expression. The asterisk causes Terraform to loop through the list and create a new list by using just the id property of each item.

resource "example_resource" "example_resource_name" { boolean_value = var.value ? true : false numeric_value = var.value > 0 ? 1 : 0 string_value = var.value == "change_me" ? "New value" : var.value string_value_2 = var.value != "change_me" ? var.value : "New value" } There are two ways to express for loops in a Terraform configuration: resource "example_resource" "example_resource_name" { list_value = [for object in var.ids : object.id] list_value_2 = var.ids[*].id }

Meta-arguments

In the previous code example, list_value and list_value_2 are referred to as arguments. You might be familiar with some of these meta-arguments already. Terraform also has a few meta-arguments, which act just like arguments but with some extra functionality:

Other meta-arguments allow for function and expression functionality to be added directly to a resource. For example, the count meta-argument is a useful mechanism to create multiple similar resources at the same time. The following example demonstrates how to create two Amazon Elastic Container Service (Amazon EKS) clusters without using the count meta-argument.

resource "aws_eks_cluster" "example_0" { name = "example_0" role_arn = aws_iam_role.cluster_role.arn vpc_config { endpoint_private_access = true endpoint_public_access = true subnet_ids = var.subnet_ids[0] } } resource "aws_eks_cluster" "example_1" { name = "example_1" role_arn = aws_iam_role.cluster_role.arn vpc_config { endpoint_private_access = true endpoint_public_access = true subnet_ids = var.subnet_ids[1] } }

The following example demonstrates how to use the count meta-argument to create two Amazon EKS clusters.

resource "aws_eks_cluster" "clusters" { count = 2 name = "cluster_${count.index}" role_arn = aws_iam_role.cluster_role.arn vpc_config { endpoint_private_access = true endpoint_public_access = true subnet_ids = var.subnet_ids[count.index] } }

To give each a unit name, you can access the list index within the resource block at count.index. But what if you want to create multiple similar resources that are a little more complex? That’s where the for_each meta-argument comes in. The for_each meta-argument is very similar to count, except that you pass in a list or an object instead of a number. Terraform creates a new resource for each member of the list or object. It is similar to if you set count = length(list), except you can access the contents of the list rather than the loop index.

This works for both a list of items or a single object. The following example would create two resources that have id-0 and id-1 as their IDs.

variable "ids" { default = [ { id = "id-0" }, { id = "id-1" }, ] } resource "example_resource" "example_resource_name" { # If your list fails, you might have to call "toset" on it to convert it to a set for_each = toset(var.ids) id = each.value }

The following example would create two resources as well, one for Sparky, the poodle, and one for Fluffy, the chihuahua.

variable "dogs" { default = { poodle = "Sparky" chihuahua = "Fluffy" } } resource "example_resource" "example_resource_name" { for_each = var.dogs breed = each.key name = each.value }

Just like you can access the loop index in count by using count.index, you can access the key and the value of each item in a for_each loop by using the each object. Because for_each iterates over both lists and objects, the each key and value can get a little confusing to keep track of. The following table shows the different ways that you can use the for_each meta-argument and how you can reference the values upon each iteration.

Example for_each type First iteration Second iteration
A
[“poodle”, “chihuahua”]
each.key = "poodle" each.value = null
each.key = "chihuahua" each.value = null
B
[ { type = "poodle", name = "Sparky" }, { type = "chihuahua", name = "Fluffy" } ]
each.key = { type = “poodle”, name = “Sparky” } each.value = null
each.key = { type = “chihuahua”, name = “Fluffy” } each.value = null
C
{ poodle = “Sparky”, chihuahua = “Fluffy” }
each.key = “poodle” each.value = “Sparky”
each.key = “chihuahua” each.value = “Fluffy”
D
{ dogs = { poodle = “Sparky”, chihuahua = “Fluffy” }, cats = { persian = “Felix”, burmese = “Morris” } }
each.key = “dogs” each.value = { poodle = “Sparky”, chihuahua = “Fluffy” }
each.key = “cats” each.value = { persian = “Felix”, burmese = “Morris” }
E
{ dogs = [ { type = “poodle”, name = “Sparky” }, { type = “chihuahua”, name = “Fluffy” } ], cats = [ { type = “persian”, name = “Felix” }, { type = “burmese”, name = “Morris” } ] }
each.key = “dogs” each.value = [ { type = “poodle”, name = “Sparky” }, { type = “chihuahua”, name = “Fluffy” } ]
each.key = “cats” each.value = [ { type = “persian”, name = “Felix” }, { type = “burmese”, name = “Morris” } ]

 

So if var.animals was equal to row E, then you could create one resource per animal by using the following code.

resource "example_resource" "example_resource_name" { for_each = var.animals type = each.key breeds = each.value[*].type names = each.value[*].name }

Alternatively, you could create two resources per animal by using the following code.

resource "example_resource" "example_resource_name" { for_each = var.animals.dogs type = "dogs" breeds = each.value.type names = each.value.name } resource "example_resource" "example_resource_name" { for_each = var.animals.cats type = "cats" breeds = each.value.type names = each.value.name }