An Introduction to Terraform Variables

If you're new to the world of Terraform, then it’s a necessity to know about the usage of Terraform variables (tfvars).

NOV 2, 2020 | ABHISHEK HUGAR
undefined

Getting Started with Terraform Variables

If you’ve read our previous blogs, or if you’re a beginner to the world of Terraform, then it’s a necessity to know about the usage of Terraform variables (tfvars). In this blog, we’ll take you through the definition of variables and values, declarations, usage examples, and leave you with some best practices to get started.

Terraform variables are key to mastering the basics of Terraform, but first, it’s important to understand the definition of a variable: known locations of memory where you can write, read, and reuse the values assigned to the variable. There are three different types of tfvars, which include: 1) input variables, 2) output variables, and 3) local values. We’ll walk you through each type, with key examples to guide you along the way.

As an added bonus, Terraform variables and their definitions can be saved in a different file, with the extension “.tfvars” or “.tfvars.json,” making them easily readable and writable.

In this article, we’ll discuss specific components of tfvars, such as:

  • Input Variables
  • How to Assign Values
  • Output Variables
  • Local Values
  • Terraform Variables Best Practices

Input Variables: Definition and Usage

These tfvars act like key pairs, where for each input variable you’re defining a block with a name, type and then value. These blocks can be used either in the root module or in the child modules.

Let’s look at a few examples for declaring blocks:

variable "availability_zone_names" {
  type    = list(string)
  default = ["us-west-la"]
}

variable "docker_ports" {
  type = list(object({
    internal = number
    external = number
    protocol = string
  }))
  default = [
    {
      internal = 8300
      external = 8300
      protocol = "tcp"
    }
  ]
}

Source: Declaring Input Variables

In the above example code, two blocks have been declared.

1. The first block is for a Terraform variable with the name “availability_zone_names”, where the type is a list of strings and a default value of “us-west-1a” has been assigned.

2. The second block has the name “docker_ports”, where the type is a list of objects and these objects come in key value pairs, where there are 3 keys:
a) internal
b) external, which take the value as numbers
c) protocol, which will accept string values. Accordingly, default values have been assigned to “protocol.”

As seen in the above examples, it should now be clear that Terraform variables are always declared after using the keyword “variable”. We then start to define a block by using the open curly brackets “{“. Inside the block, we can define the type, and assign default values. Finally, the block ends with a closing curly bracket “}”.

NOTE: The data types seen in the below snippet are supported:

Source: Types and Values

Other than the above data types, another keyword “any” is used to define that a particular tfvars can be assigned with any value.

We can also have a description tag present between the opening and closing brackets. Description is used mostly for documentation purposes, and the usage of the Terraform variable is added here. Default values and descriptions are not mandatory and can be skipped if not required. The below example is a snippet from one of our configuration files where we have written a description, but skipped default values.

variable "main_cidr" {
  description = "VPC CIDR for main network"
  type = string
}

variable "public_subnets" {
  description = "CIDR blocks for public subnets"
  type = list(string)
}

How to Assign Values

Up until this point, we’ve discussed how to define and give default values. But, what if there is no default assigned? In this case, how can we assign them?

Firstly, it’s necessary to understand that Terraform variables can be defined either in the root module or child module. When they are defined in child modules, we directly pass values to these modules from their parent module or the caller module.

If your tfvars are defined in the root module, things get a bit more interesting. There are actually three different ways in which values could be assigned (with pros and cons to each, of course). We’ll walk you through your options:


How to Assign Values from the Root Module:

1. Through commands on the CLI

2. The “.tfvars” file

3. Using Environment variables


1. Through commands on the CLI

Using the CLI is the most conventional way, but there are disadvantages. When you are using either the “plan” or “apply” commands, you may use the option “-var”. The option may be used any number of times with these commands (even in a single command). However, we would not recommend going through commands on the CLI, particularly if you have many or complex variables in your configuration modules.

Here’s an example of how to use the “apply” command with “-var”:

terraform apply -var="AWS region=us-east-1"
terraform apply -var="image_id=ami-xyz456"

2. The “tfvars” file--a workaround

As we mentioned above, if you have many or complex variables in your configuration modules, using commands on the CLI may not be the best way. However, there is a workaround: using the definition file. When you have many variables that need to be assigned values, consider creating a file with the extension “.tfvars” or “.tfvars.json” which will make the process simpler and more efficient.

It’s easy to do--simply define a file with only the Terraform variables, with their values, and name the file with the extensions mentioned above. Once completed, you may use the “apply” command with the option “-var-file” and the file name. Below is an example of how this command may be used:

terraform apply -var-file="assign.tfvars"


NOTE: If you have any files with the name “terraform.tfvars” or “terraform.tfvars.json”, then these variable definition files are loaded automatically.


3. Using Environment variables

This is a dynamic value which has been set-up on your operating system and it affects how any process on your system works. These are always in pairs. In this case, your system is searched for the environment variables whose name might be in the format as seen below:

Below is an example of how you may assign values in this case:

Output Values: Definition and Usage

As we have already seen input tfvars, which act like function parameters and accept or send values to child modules, it’s now important to learn more about the inverse--output. In the simplest terms, they’re values that are returned from a child to the parent module. These can be printed on the CLI when the “apply” command is used and queried using the command “terraform output”.

Let’s have a look at the output block:

output "instance_ip_addr" {
  value = aws_instance.server.private_ip
}

Source: Declaring an Output Value

In the above block that has been declared, we can see that a block name “instance_ip_addr” has been given after using the keyword “output”. We then begin and end the block using the opening and closing curly brackets. Inside the block, a particular value has been assigned with an expression that fetches the private_ip for the particular aws_instance.

While “value” is a mandatory field for an output block, we can also have “description”, “sensitive” and “depends_on” fields.

Description: Just like input, output variables can have the description field. It serves the same purpose of documentation.

Sensitive: If your output variable is some kind of sensitive information such as a password or token information, you may use this field and mark it true. This will prevent it from being printed. However, this information is still stored on “State File”.

Depends_On: Usually the dependency of one module on the other is clear due to the output values. However, by any unlikely chance that the dependency is missed, we can use the depends_on field to determine the dependency.

Local Values

These are variables that are local to a module. They are defined, assigned, and used in the same module, and defined in the “locals” block. Below is an example snippet on a local block:

locals {
  service_name = "forum"
  owner        = "Community Team"
}

Source: Declaring local value

Local variables can be declared once and used any number of times in the module. These can be accessed as objects by using the format of “local.Variable_Name”.

Terraform Variables Best Practices

While you can learn all about a particular tool or technology, it’s also important to keep in mind best practices and follow them whenever necessary. If you’ve been following the article, you should now be aware that tfvars are a part and parcel of Terraform, and they are used and found extensively throughout configuration. If you’re looking for general best practices for Terraform, we have a number of resources available for you here, including advice for DevOps.

Tfvars best practices include the following:

1. Naming convention and description: We know that this may seem a very trivial best practice, but trust us, it’s actually one of the most important things you should keep in mind. In our previous blog, “Complete Guide to Terraform: News, Updates, Tips,” we’ve specified how documentation is a major advantage offered by Terraform. However, other than the source code, you will also have to concentrate on giving the right names to your Terraform variables and adding the description field to each. This will enable any future engineer who works on updating the configuration to be able to read and understand and not make any unnecessary changes or additions. Another important point to remember is that the name always has to be a valid identifier.

Below are the exceptions specified by HashiCorp that may not be used as identifiers:
-> source
-> providers
-> for_each
-> count
-> version
-> locals
-> lifecycle
-> depends_on

2. “.tfvars” Files: We have already discussed the different ways in which we may assign values to an input variable. Of the three methods that we discussed, we noted that having a definitions file with the extension “.tfvars” was the most efficient way to assign values. This also enables you to pass the values that you don’t wish to include in your source configuration file. We would also suggest that if you have any credentials involved, you should include it in the definitions file rather than in your actual configurations.

3. Best Usage of Local Values: When you have expressions that may change in the future and need to be used for multiple lines in your configuration, we recommend the usage of local values in your module so that you may avoid additional declarations. However, it’s not always the best decision to use them more than required. Local values may end up causing confusion and unexpected results for future configurational changes when done by a different person.

4. Sensitive Data: As a best practice we always encourage that the sensitive flag in output values is set to true. But the data is anyways recorded in the state file. Hence, a combination of a sensitive flag and storing the state remotely on cloud instead of storing the state file locally increases the security. Check out our blog on State File to understand more.

5. Right Data Type is the Key: Using the right data type has been important in every part of coding. It ensures the right results and efficient usage of the configuration code that you have written. At any point in time, if you are not really sure of the data type, use the keyword “any” to define the data type.

6. Custom Validation: With Tf version 0.13.0, a new boon has been offered called the “Custom Validation Rules”. You may nest 0 or more validation blocks inside your variable block. The validation block will basically check if the input value is as per the validation rules and returns “true” or “false” accordingly. If invalid, then you may even print an error message.

We hope that our introduction to Terraform variables has been a helpful guide to get started. If you’re looking for more support or resources, join our InfraCode Slack to gain valuable help from our community of experts and join the discussion about Infrastructure as Code.