Going to AWS Summit London? 🇬🇧🇬🇧

Meet us there →

Terraform

How to Use Terraform Variables (Locals, Input, Output, Environment)

Terraform variables

Variables are fundamental constructs in every programming language because they are inherently useful in building dynamic programs. We use variables to store temporary values so that they can assist programming logic in simple as well as complex programs.

In this post, we discuss how variables are used in Terraform. Terraform uses HCL (Hashicorp Configuration Language) to encode infrastructure. It is declarative in nature, meaning several blocks of code are declared to create a desired set of infrastructure.

Before we dive into various types of variables, it is helpful to think of the complete Terraform configuration as a single function. As far as variables are concerned, we use them with function in the form of arguments, return values, and local variables. These are analogous to input variables, output variables, and local variables in Terraform.

We will cover:

  1. What are Terraform variables?
  2. Local variables
  3. Input variables
  4. Variable substitution using CLI and .tfvars
  5. Environment variables
  6. Variable precedence
  7. Variable validation
  8. Sensitive variables
  9. Output variables
  10. Using variables in for_each loop
  11. Terraform variables best practices

Note: New versions of Terraform will be placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that will expand on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6. OpenTofu retained all the features and functionalities that had made Terraform popular among developers while also introducing improvements and enhancements. OpenTofu is the future of the Terraform ecosystem, and having a truly open-source project to support all your IaC needs is the main priority.

What are Terraform variables?

Terraform variables let you define values that can be reused throughout your Terraform configuration, similar to variables in any programming language. They make your configuration more dynamic and flexible, and they enhance the parametrization of your code. With variables, you can easily accommodate different configurations without altering your code, as you can easily change only the values of these variables to achieve different use cases.

Terraform variable types

Terraform supports various variable types, each designed to handle different kinds of data:

  • String — This fundamental type stores text values. Use strings for data that doesn’t require mathematical operations, such as usernames or tags.
  • Number — This type is used for numeric values that you might need to perform calculations on or use in numeric settings, such as scaling parameters, setting timeouts, and defining a number of instances to deploy.
  • Bool — Short for Boolean, this type is strictly for true or false values. They are essential for logic and conditional statements in configurations, such as enabling or disabling resource provisioning.
  • List — A list is a sequence of values of the same type. This type is ideal for scenarios where you need to manage a collection of similar items, like multiple configuration tags.
  • Map — Maps are collections of key-value pairs, each unique key mapping to a specific value. This type is useful, for example, when associating server names with their roles or configurations.
  • Tuple — This type is similar to lists but can contain a fixed number of elements, each potentially of a different type. Tuples are suitable when you need to group a specific set of values with varied types together, like a coordinate of mixed data types.
  • Object — Objects are used to define a structure with named attributes, each with its own type. They are very flexible, allowing the definition of complex relationships, like a configuration block that includes various attributes of different types.
  • Set — Sets are collections of unique values of the same type. They are useful when you need to ensure no duplicates, such as a list of unique user identifiers or configurations that must remain distinct.

We’ll discuss these in more detail with examples later in the article.

Local variables

Local variables are declared using the locals block. It is a group of key-value pairs that can be used in the configuration. The values can be hard-coded or be a reference to another variable or resource.

Local variables are accessible within the module/configuration where they are declared. Let us take an example of creating a configuration for an EC2 instance using local variables. Add this to a file named main.tf.

locals {
 ami  = "ami-0d26eb3972b7f8c96"
 type = "t2.micro"
 tags = {
   Name = "My Virtual Machine"
   Env  = "Dev"
 }
 subnet = "subnet-76a8163a"
 nic    = aws_network_interface.my_nic.id
}
 
resource "aws_instance" "myvm" {
 ami           = local.ami
 instance_type = local.type
 tags          = local.tags
 
 network_interface {
   network_interface_id = aws_network_interface.my_nic.id
   device_index         = 0
 }
}
 
resource "aws_network_interface" "my_nic" {
 description = "My NIC"
 subnet_id   = var.subnet
 
 tags = {
   Name = "My NIC"
 }
}

In this example, we have declared all the local variables in the locals block. The variables represent the AMI ID (ami), Instance type (type), Subnet Id (subnet), Network Interface (nic) and Tags (tags) to be assigned for the given EC2 instance.

In the aws_instance resource block, we used these variables to provide the appropriate values required for the given attribute. Notice how the local variables are being referenced using a local keyword (without ‘s’).

The usage of local variables is similar to data sources. However, they have a completely different purpose. Data sources fetch valid values from the cloud provider based on the query filters we provide. Whereas we can set our desired values in local variables — they are not dependent on the cloud providers.

It is indeed possible to assign a value from a data source to a local variable. Similar to how we have done it to create the nic local variable, it refers to the id argument in the aws_network_interface resource block.

As a best practice, try to keep the number of local variables to a minimum. Using many local variables can make the code hard to read.

If you want to know more about locals, see: Terraform Locals: What Are They, How to Use Them, Examples

Terraform input variables

Terraform input variables are used to pass certain values from outside of the configuration or module. They are used to assign dynamic values to resource attributes. The difference between local and input variables is that input variables allow you to pass values before the code execution.

Further, the main function of the input variables is to act as inputs to modules. Modules are self-contained pieces of code that perform certain predefined deployment tasks. Input variables declared within modules are used to accept values from the root directory.

Additionally, it is also possible to set certain attributes while declaring input variables, as below:

  • type — to identify the type of the variable being declared.
  • default — default value in case the value is not provided explicitly.
  • description — a description of the variable. This description is also used to generate documentation for the module.
  • validation — to define validation rules.
  • sensitive — a boolean value. If true, Terraform masks the variable’s value anywhere it displays the variable.

Terraform input variable types

Input variables support multiple data types.

They are broadly categorized as simple and complex. String, number, bool are simple data types, whereas list, map, tuple, object, and set are complex data types.

The following snippets provide examples for each of the types we listed.

String type

The string type input variables are used to accept values in the form of UNICODE characters. The value is usually wrapped by double quotes, as shown below.

variable "string_type" {
 description = "This is a variable of type string"
 type        = string
 default     = "Default string value for this variable"
}

The string type input variables also support a heredoc style format where the value being accepted is a longer string separated by new line characters. The start and end of the value is indicated by “EOF” (End Of File) characters. An example of the same is shown below.

variable "string_heredoc_type" {
 description = "This is a variable of type string"
 type        = string
 default     = <<EOF
hello, this is Sumeet.
Do visit my website!
EOF
}

Number type

The number type input variable enables us to define and accept numerical values as inputs for their infrastructure deployments. For example, these numeric values can help define the desired number of instances to be created in an auto-scaling group. The code below defines a number type input variable in any given Terraform config.

variable "number_type" {
 description = "This is a variable of type number"
 type        = number
 default     = 42
}

Boolean type

The boolean type input variable is used to define and accept true/false values as inputs for infrastructure deployments to incorporate logic and conditional statements into the Terraform configurations. Boolean input variables are particularly useful for enabling or disabling certain features or behaviors in infrastructure deployments.

An example of a boolean variable is below.

variable "boolean_type" {
 description = "This is a variable of type bool"
 type        = bool
 default     = true
}

Terraform list variable

Terraform list variables allow us to define and accept a collection of values as inputs for infrastructure deployments. A list is an ordered sequence of elements, and it can contain any data type, such as strings, numbers, or even other complex data structures. However, a single list cannot have multiple data types.

List type input variables are particularly useful in scenarios where we need to provide multiple values of the same type, such as a list of IP addresses, a set of ports, or a collection of resource names.

The example below is for an input variable of a type list that contains strings.

variable "list_type" {
 description = "This is a variable of type list"
 type        = list(string)
 default     = ["string1", "string2", "string3"]
}

Map type

The map type input variable enables us to define and accept a collection of key-value pairs as inputs for our infrastructure deployments. A map is a complex data structure that associates values with unique keys, similar to a dictionary or an object in other programming languages. For example, a map can be used to specify resource tags, environment-specific settings, or configuration parameters for different modules.

The example below shows how a map of string type values is defined in Terraform.

variable "map_type" {
 description = "This is a variable of type map"
 type        = map(string)
 default     = {
   key1 = "value1"
   key2 = "value2"
 }
}

Object type

An object represents a complex data structure that consists of multiple key-value pairs, where each key is associated with a specific data type for its corresponding value. The object type input variable allows us to define and accept a structured set of properties or attributes as inputs for our infrastructure deployments. For example, an object is used to define a set of parameters for a server configuration.

The variable below demonstrates how an object type input variable is defined with multi-typed properties.

variable "object_type" {
 description = "This is a variable of type object"
 type        = object({
   name    = string
   age     = number
   enabled = bool
 })
 default = {
   name    = "John Doe"
   age     = 30
   enabled = true
 }
}

Tuple type

A tuple is a fixed-length collection that can contain values of different data types. The key differences between tuples and lists are:

  1. Tuples have a fixed length, as against lists.
  2. With tuples, it is possible to include values with different primitive types. Meanwhile, lists dictate the type of elements included in them.
  3. Values included in tuples are ordered. Due to their dynamic sizes, it is possible to resize and reorder the values in lists.

An example of a tuple type input variable:

variable "tuple_type" {
 description = "This is a variable of type tuple"
 type        = tuple([string, number, bool])
 default     = ["item1", 42, true]
}

Set type

A set is an unordered collection of distinct values, meaning each element appears only once within the set. As against lists, sets enforce uniqueness – each element can appear only once within the set. Sets support various inbuilt operations such as union, intersection, and difference, which are used to combine or compare sets.

An example of a set type input variable is below.

variable "set_example" {
 description = "This is a variable of type set"
 type        = set(string)
 default     = ["item1", "item2", "item3"]
}

Map of objects

One of the widely used complex input variable types is map(object). It is a data type that represents a map where each key is associated with an object value.

It allows us to create a collection of key-value pairs, where the values are objects with defined attributes and their respective values. When using map(object), we define the structure of the object values by specifying the attributes and their corresponding types within the object type definition. Each object within the map can have its own set of attributes, providing flexibility to represent diverse sets of data.

An example of the same is given below, where the map of objects represents attribute values used for the creation of multiple subnets.

variable "map_of_objects" {
  description = "This is a variable of type Map of objects"
  type = map(object({
    name = string,
    cidr = string
  }))
  default = {
    "subnet_a" = {
    name = "Subnet A",
    cidr = "10.10.1.0/24"
    },
  "subnet_b" = {
    name = "Subnet B",
    cidr = "10.10.2.0/24"
    },
  "subnet_c" = {
    name = "Subnet C",
    cidr = "10.10.3.0/24"
    }
  }
}

List of objects

This type of variable is similar to the Map of objects, except that the objects are not referred to by any “key”. The example used for the Map of objects can also be represented in the form of a list of objects, as shown below.

The list(object) is an ordered list of objects where each object is referred to using the index. On the other hand, map(object) is an unordered set, and each object is referred to using the key value.

variable "list_of_objects" {
  description = "This is a variable of type List of objects"
  type = list(object({
    name = string,
    cidr = string
  }))
  default = [
    {
      name = "Subnet A",
      cidr = "10.10.1.0/24"
    },
    {
      name = "Subnet B",
      cidr = "10.10.2.0/24"
    },
    {
      name = "Subnet C",
      cidr = "10.10.3.0/24"
    }
  ]
}

Terraform input variables example

Let us work through the same example as before. Only this time, we use variables instead of local variables. Create a new file to declare input variables as variables.tf and add the below content to it.

variable "ami" {
 type        = string
 description = "AMI ID for the EC2 instance"
 default     = "ami-0d26eb3972b7f8c96"
 
 validation {
   condition     = length(var.ami) > 4 && substr(var.ami, 0, 4) == "ami-"
   error_message = "Please provide a valid value for variable AMI."
 }
}
 
variable "type" {
 type        = string
 description = "Instance type for the EC2 instance"
 default     = "t2.micro"
 sensitive   = true
}
 
variable "tags" {
 type = object({
   name = string
   env  = string
 })
 description = "Tags for the EC2 instance"
 default = {
   name = "My Virtual Machine"
   env  = "Dev"
 }
}
 
variable "subnet" {
 type        = string
 description = "Subnet ID for network interface"
 default     = "subnet-76a8163a"
}

Here, we have declared 5 variables — ami, nic, subnet and type with the simple data type, and tags with a complex data type object — a collection of key-value pairs with string values. Notice how we have made use of attributes like description and default.

The ami variable also has validation rules defined for them to check the validity of the value provided. We have also marked the type variable as sensitive.

Let us now modify main.tf to use the variables declared above.

resource "aws_instance" "myvm" {
 ami           = var.ami
 instance_type = var.type
 tags          = var.tags
 
 network_interface {
   network_interface_id = aws_network_interface.my_nic.id
   device_index         = 0
 }
}
 
resource "aws_network_interface" "my_nic" {
 description = "My NIC"
 subnet_id   = var.subnet
 
 tags = {
   Name = "My NIC"
 }
}

Within the resource blocks, we have simply used these variables by using var.<variable name> format. When you proceed to plan and apply this configuration, the variable values will automatically be replaced by default values. The following is a sample plan output.

        }
    }

Plan: 2 to add, 0 to change, 0 to destroy.

To check how validation works, modify the default value provided to the ami variable. Make sure to change the ami- part since validation rules are validating the same. Run the plan command, and see the output. You should see the error message thrown on the console as below.

│ Error: Invalid value for variable
│   on variables.tf line 1:
1: variable "ami" {
│ Please provide a valid value for variable AMI.
│ This was checked by the validation rule at variables.tf:6,3-13.

Also, notice how the type value is represented in the plan output. Since we have marked it as sensitive, its value is not shown. Instead, it just displays sensitive.

 + id                                                           = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                                       = (known after apply)
      + instance_type                                        = (sensitive)
      + ipv6_address_count                              = (known after apply)
      + ipv6_addresses                                     = (known after apply)

Variable substitution using CLI and .tfvars

In the previous example, we relied on the default values of the variables. However, variables are generally used to substitute values during runtime. The default values can be overridden in two ways:

  • Passing the values in CLI as -var argument.
  • Using .tfvars file to set variable values explicitly.

If we want to initialize the variables using the CLI argument, we can do so as below. Running this command results in Terraform using these values instead of the defaults.

terraform plan -var "ami=test" -var "type=t2.nano" -var "tags={\"name\":\"My Virtual Machine\",\"env\":\"Dev\"}"

While working with plan or apply commands, -var argument should be used for every variable to be overridden. Note how we have provided the value for complex data type with escaped characters.

Imagine a scenario where many variables are used in the configuration. Passing the values using CLI arguments can become a tedious task. This is where .tfvars files come into play.

Create a file with the .tfvars extension and add the below content to it. I have used the name values.tfvars as the file name. This way we can organize and manage variable values easily.

ami  = "ami-0d26eb3972b7f8c96"
type = "t2.nano"
tags = {
 "name" : "My Virtual Machine"
 "env" : "Dev"
}

This time, we should ask Terraform to use the values.tfvars file by providing its path to -var-file CLI argument. The final plan command should look as such:

terraform plan -var-file values.tfvars

The -var-file argument is great if you have multiple .tfvars files with variations in values. However, if you do not wish to provide the file path every time you run plan or apply, simply name the file as <filename>.auto.tfvars. This file is then automatically chosen to supply input variable values.

Environment variables

Additionally, input variable values can also be set using Terraform environment variables. To do so, simply set the environment variable in the format TF_VAR_<variable name>.

The variable name part of the format is the same as the variables declared in the variables.tf file. For example, to set the ami variable run the below command to set its corresponding value.

export TF_VAR_ami=ami-0d26eb3972b7f8c96

Apart from the above environment variable, it is important to note that Terraform also uses a few other environment variables like TF_LOG, TF_CLI_ARGS, TF_DATA_DIR, etc. These environment variables are used for various purposes like logging, setting default behavior with respect to workspaces, CLI arguments, etc.

Variable precedence

As we have seen till now, there are three ways of providing input values to Terraform configuration using variables. Namely—default values, CLI arguments, and .tfvars file. The precedence is given to values passed via CLI arguments. This is followed by values passed using the .tfvars file and lastly, the default values are considered. 

In the current example, now that we have the values.tfvars file saved, try to run a plan command by passing values via CLI -var arguments. Make sure to provide different values as that of .tfvars and defaults. Terraform ignores the values provided via .tfvars and defaults.

If the values are not provided in the .tfvars file, or as defaults, or as CLI arguments, it falls back on TF_VAR_ environment variables.

Additionally, if we don’t provide the values in any of the forms discussed above, Terraform would ask for the same in interactive mode when plan or apply commands are run.

As a best practice, it is not recommended to store secret and sensitive information in variable files. These values should always be provided via the TF_VAR_ environment variable.

This is where Spacelift shines. It makes use of these Terraform native environment variables to manage secrets as well as other attributes that make the most sense. Managing the environment in the Spacelift console is easy, thanks to a dedicated tab where values can be edited on the go.

Variable validation

In any programming language, you try to catch the errors as soon as possible, and even if Terraform uses a declarative language, this is the same.

Variable validations ensure that constraints are applied to your variables. Before this feature was even introduced, the only way you could do validations was by making use of a hacky method leveraging the file function. Let’s see that in action:

locals {
 vpc_cidr            = "10.0.0.0/16"
 vpc_cidr_validation = split("/", local.vpc_cidr)[1] < 16 || split("/", local.vpc_cidr)[1] > 30 ? file(format("\n\nERROR: The VPC Cidr %s is not between /16 and /30", local.vpc_cidr)) : null
}

Basically, in the above example, I’m checking if the network mask is lower than 16 and greater than 30, and if it is, my “validation” will try to open a file that doesn’t exist and will actually print out the error message that I want. Otherwise, it won’t print anything:

terraform apply

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

Apply complete! Resources: 0 added, 0 changed, 0 destroyed

Now, let’s change the cidr to receive an error:

locals {
 vpc_cidr            = "10.0.0.0/8"
 vpc_cidr_validation = split("/", local.vpc_cidr)[1] < 16 && split("/", local.vpc_cidr)[1] > 30 ? file(format("\n\nERROR: The VPC Cidr %s is not between /16 and /30", local.vpc_cidr)) : null
}
terraform apply
│ Error: Invalid function argument
│   on main.tf line 3, in locals:
3:   vpc_cidr_validation = split("/", local.vpc_cidr)[1] < 16 && split("/", local.vpc_cidr)[1] > 30 ? file(format("\n\nERROR: The VPC Cidr %s is not between /16 and /30", local.vpc_cidr)) : null
│     ├────────────────
│     │ while calling file(path)
│     │ local.vpc_cidr is "10.0.0.0/8"
│ Invalid value for "path" parameter: no file exists at "\n\nERROR: The VPC Cidr 10.0.0.0/8 is not between /16 and /30"; this function works only with files that are distributed as part of the configuration source code, so if this file will be created by a resource in this configuration you must instead obtain this result from an attribute of that resource.

It does the job, but as you can see, the error message can be misleading.

With variable validations, however, things are improved a lot. Variable validations are defined in the variable block, and they receive two parameters:

  • condition – what you want to check inside your variable
  • error_message – what error message you would like your users to get if the condition is not fulfilled

Let’s recreate the above example with a variable validation:

variable "cidr_block" {
 type    = string
 default = "10.0.0.0/8"
 validation {
   condition     = split("/", var.cidr_block)[1] > 16 && split("/", var.cidr_block)[1] < 30
   error_message = "Your vpc cidr is not between 16 and 30"
 }
}

This will result in an error because the cidr_block is not between /16 and /30:

terraform apply
│ Error: Invalid value for variable
│   on main.tf line 1:
1: variable "cidr_block" {
│     ├────────────────
│     │ var.cidr_block is "10.0.0.0/8"
│ Your vpc cidr is not between 16 and 30
│ This was checked by the validation rule at main.tf:4,3-13

We could take this example even up a notch and verify if the string we are passing is in a cidr format:

validation {
   condition     = strcontains(var.cidr_block, "/") && length(split(var.cidr_block, ".")) == 4
   error_message = "Your vpc cidr doesn't respect the cidr format"
 }

This validation checks if our cidr has a “/” and if it has 3 “dots”.

Sensitive variables

Terraform has also the ability to mark variables as sensitive and will not display their value when you are running terraform plan and apply, but they will be readable from within the Terraform state.

Let’s take a look at an example of a sensitive variable:

variable "my_super_secret_password" {
 type      = string
 default   = "super-secret"
 sensitive = true
}

output "my_super_secret_password" {
 value = var.my_super_secret_password
}
terraform apply
│ Error: Output refers to sensitive values
│   on main.tf line 20:
20: output "my_super_secret_password" {
│ To reduce the risk of accidentally exporting sensitive data that was intended to be only internal, Terraform requires that any root module output containing sensitive data
│ be explicitly marked as sensitive, to confirm your intent.
│ If you do intend to export this data, annotate the output value as sensitive by adding the following argument:
sensitive = true

Now, if we want terraform not to error out and at least show the output, we should add the sensitive = true to that output:

output "my_super_secret_password" {
 value     = var.my_super_secret_password
 sensitive = true
}
terraform apply

Changes to Outputs:
  + my_super_secret_password = (sensitive value)

You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

my_super_secret_password = <sensitive>

If you want to see the sensitive value in the output too, Terraform has a mechanism in place for that if you are leveraging the nonsensitive function:

variable "my_super_secret_password" {
 type      = string
 default   = "super-secret"
 sensitive = true
}

output "my_super_secret_password" {
 value = nonsensitive(var.my_super_secret_password)
}
terraform apply

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

Apply complete! Resources: 0 added, 0 changed, 0 destroyed.

Outputs:

my_super_secret_password = "super-secret"

Output variables

For situations where you deploy a large web application infrastructure using Terraform, you often need certain endpoints, IP addresses, database user credentials, and so forth. This information is most useful for passing the values to modules along with other scenarios.

This information is also available in Terraform state files. But state files are large, and normally we would have to perform an intricate search for this kind of information.

Output variables in Terraform are used to display the required information in the console output after a successful application of configuration for the root module. To declare an output variable, write the following configuration block into the Terraform configuration files.

output "instance_id" {
 value       = aws_instance.myvm.id
 description = "AWS EC2 instance ID"
 sensitive   = false
}

Continuing with the same example, we would like to display the instance ID of the EC2 instance that is created. So, declare an output variable named instance_id — this could be any name of our choosing.

Within this output block, we have used some attributes to associate this output variable’s value. We have used resource reference for aws_instance.myvm configuration and specified to use its id attribute.

Optionally, we can use the description and sensitive flags. We have discussed the purpose of these attributes in previous sections. When a plan command is run, the plan output acknowledges the output variable being declared as below.

Changes to Outputs:
  + instance_id = (sensitive value)

Similarly, when we run the apply command, upon successful creation of EC2 instance, we would know the instance ID of the same. Once the deployment is successful, output variables can also be accessed using the output command:

terraform output

Output:

instance_id = “i-xxxxxxxx”

Output variables are used by child modules to expose certain values to the root module. The root module does not have access to any other component being created by the child module. So, if some information needs to be made available to the root module, output variables should be declared for the corresponding attributes within the child module.

Using variables in for_each loop

This section goes through an example where we want to create multiple subnets for a given VPC. Without variables, we would write the configurations for the number of subnets specified. The other way is to use the Terraform for loop and create separate subnets based on the index value of the iteration.

The for loop thus implemented is not very useful. Consider a case where subnets need to be in selected availability zones, and each of them has a different CIDR range specified. For situations like these, we can make use of complex variable type map(object()) along with for loop to iterate over this variable.

Below is the input variable declaration we need to define attributes of multiple subnets. This block specifies the type of variable my_subnets to be map(object()). It means we want to define a variable that has a map of objects, with a couple of attributes in each object. We have used cidr and az attributes for each object with string type.

variable "my_subnets" {
 type = map(object({
   cidr = string
   az   = string
 }))
 description = "Subnets for My VPC"
}

Let us move to the .tfvars file to initialize the value for this input variable. The .tfvars file should contain the following lines of code.

my_subnets = {
 "a" = {
   cidr = "10.0.1.0/26"
   az   = "eu-central-1a"
 },
 "b" = {
   cidr = "10.0.2.0/26"
   az   = "eu-central-1a"
 },
 "c" = {
   cidr = "10.0.3.0/26"
   az   = "eu-central-1b"
 },
 "d" = {
   cidr = "10.0.4.0/26"
   az   = "eu-central-1c"
 },
 "e" = {
   cidr = "10.0.5.0/26"
   az   = "eu-central-1b"
 }
}

If you compare the input variable declaration and initialization, you will see we have aligned the attributes. Also, the initialized value consists of 6 objects mapped by a string key. Each object has a unique CIDR and Availability Zone (az) value.

Lastly, let’s define the configuration for the subnets themselves. Note: The following code assumes that we have defined aws_vpc.my_vpc elsewhere in the configuration.

resource "aws_subnet" "my_subnets" {
 for_each          = var.my_subnets
 vpc_id            = aws_vpc.my_vpc.id
 cidr_block        = each.value.cidr
 availability_zone = each.value.az
 
 tags = {
   Name = "Subnet - ${each.value.az}"
 }
}

In this resource block, we have used the my_subnets variable to iterate over in a for_each loop. for_each loop comes along with a keyword each, which helps us identify the value to be assigned in each iteration. This single block of code is capable of creating 6 unique subnets.

Read more about using Terraform for_each meta-argument.

Terraform variables best practices

When working with Terraform variables, you have to consider the following:

  • Use descriptive variable names – this is a best practice in any programming language, as using descriptive names, makes the code easier to understand and maintain
  • Organize your variables
  • Specify variable types – when declaring variables, specify their types (number, pool, string, list, etc.)
  • Implement variable validations – use validations to receive errors before plans
  • Secure sensitive variables – take advantage of the sensitive attribute to prevent their values from being displayed in the command output
  • Use default values – default values make configurations easier to use and reduce the need for user input
  • Use optional object type attributes – when dealing with objects, to reduce user input, you should take advantage of the optional type for some of the keys
  • Write documentation – it is very important to write documentation for your code, and explaining some of the complex variables can be very helpful
  • Leverage environment variables when using CI/CD
  • Use dynamic credentials wherever possible and omit using static variables for this

Key points

It is important to note that Terraform does not allow the usage of variables in provider configuration blocks. This is mainly to adhere to best practices of using Terraform.

Variables usually make the developer’s life easier by improving the maintainability of your code. Especially in larger Terraform codebases, variables should be used. Larger codebases are often worked upon by a team of developers. Variables make it very easy to read and modify certain aspects, like infrastructure changes via IaC.

Working in a team of Terraform developers can be challenging, especially when it comes to managing state files. Spacelift is built to provide a great CI/CD experience concerning IaC where it is used for version control of the code as well as state management. It is a perfect place to set up a CI/CD pipeline for Terraform code. If you are struggling with Terraform automation and management, check out Spacelift. You can sign up for a free evaluation right away. 

I hope this blog post was helpful to you in understanding and exploring possibilities with Terraform variables. Let me know your thoughts in the comments section.

Terraform Management Made Easy

Spacelift effectively manages Terraform state, more complex workflows, supports policy as code, programmatic configuration, context sharing, drift detection, resource visualization and includes many more features.

Start free trial
Terraform CLI Commands Cheatsheet

Initialize/ plan/ apply your IaC, manage modules, state, and more.

Share your data and download the cheatsheet