Terraform Basics #3 - Variables & Outputs

Last Edited: 7/12/2025

This blog post introduces variables and outputs in Terraform.

DevOps

In the previous article, we discussed how easily we can set up a cloud infrastructure with Terraform using a simple example. Although we could already observe the usefulness of Terraform as an IaC tool from that example, many parameters were hardcoded, making the code non-reusable, inflexible, and insecure, defying the important purposes of Terraform as an IaC tool. Hence, in this article, we will discuss how we can make use of variables and outputs in HCL to start taking more advantage of Terraform.

Variables

In HCL, we can define variables using a variable block, containing a description, type, and optionally a default value and whether the variable is sensitive (for environment variables), and use them with var.<var_name>. While variables can be defined in the same main.tf file where resources are defined, it's best practice to set up a separate vars.tf file for defining them. The following is an example of variable definitions in vars.tf.

vars.tf
variable "ami" {
  description = "Amazon Machine Image for EC2 instance"
  type        = string
}
 
variable "instance_type" {
  description = "EC2 instance type (default: t4.micro)"
  type        = string
  default     = "t4.micro"
}
 
variable "db_password" {
  description = "SENSITIVE DB password"
  type        = string
  sensitive   = true
}

Instead of hardcoded strings, we can use variables like var.ami and var.instance_type at appropriate places. In Terraform, there are many ways to provide values to variables. When performing plan and apply, Terraform first checks for -var and -var-file arguments and verifies that values are provided for all necessary variables. If no arguments are provided, or if there are insufficient values, it looks for *.auto.tfvars and terraform.tfvars files ( tfvars files should contain <var_name> = <value> on each row). Then, it checks for TF_VAR_<name> environment variables and then checks for default values. If there are still insufficient values, it will prompt us to manually input values on the CLI.

Usually, we define values for non-sensitive variables in .tfvars files (like ami and instance_type) and sensitive variables (like db_password, where the sensitive parameter is set to true to avoid it being displayed on the CLI) in .env files or as -var arguments. By using this setup, we can make the code reusable, flexible, and secure, as we can edit or use different .tfvars files depending on the infrastructure requirements and phases (development, staging, and production), and avoid hardcoded secrets in files that are potentially shared in a repository.

Local Variables & Outputs

There are cases where we want to use variables not for code reusability but for better code organization and to avoid repetition. For example, we might want to avoid hardcoding the port number 8080 for the EC2 instance by creating a local variable ec2_port. In such cases, we can make use of local variables, which only apply within the file they are defined in and can be defined using a locals block as follows.

main.tf
locals {
  ec2_port    = 8080
  lb_port_in  = 80
  lb_port_out = 0
}

All the local variables for a specific file are defined within the same locals block, as shown above. We can then use those variables like local.ec2_port, local.lb_port_in, and so on. Local variables help avoid repetition and make edits easier. There are also cases where we want to obtain the output values of attributes available after the apply command, such as when we use the IP address of EC2 instances for setting up networking (e.g., a Kubernetes cluster). For these cases, we can use outputs defined by an output block, containing a value (optionally a description, type, and sensitive parameter) that specifies which attribute of a resource we want. Outputs are typically defined in a outputs.tf file, similar to how variables are defined in vars.tf.

output "instance_1_ip_addr" {
  type   = string
  values = aws_instance.instance_1.public_ip
}

After using the apply command and resources are ready, non-sensitive outputs are displayed on the CLI. We can also use terraform output -json to see all the parameters of the outputs and terraform output <output_name> to check the value of a specific output, which can be logged or piped for subsequent manual or automatic infrastructure configurations.

Language Features & Meta-Arguments

Beyond variables and outputs, there are many language features that enable us to write better code. For example, we can use template strings ("AMI used: ${var.ami}"), operators (!, ==, etc.), conditionals (<cond> ? true : false), for loops ([for i in var.list: i]), and so on in our resource definitions. There are also useful predefined functions for hashing, type conversion, date and time, and so on, and I highly recommend checking out the official documentation cited at the bottom of the article for more.

We can also use meta-arguments in our resource blocks, which allow us to have more control over our infrastructure provisioning. For example, we can use the depends_on argument, where we can define the resources that need to be created before the resource with this argument is built, in a list. We also have access to the count meta-argument, which allows us to create multiple replicas of the same resources and refer to them individually using count.index, the for_each argument, which allows us to iterate over a list (or a set) and create a resource for each, referring to them with each.key, and the lifecycle argument, which allows us to configure behaviors in the lifecycle (like create_before_destroy for availability and ignore_changes to avoid modifications). There are many other useful meta-arguments in Terraform, so I recommend checking out the official documentation for more details.

Conclusion

In this article, we introduced variables, local variables, outputs, and other useful language features and meta-arguments for taking advantage of Terraform as an IaC tool. I highly recommend using these features to rewrite the Terraform code from the previous article for practice and in other projects as well. For more details and explanations of these features, I highly recommend checking out the resources cited below.

Resources