Cloud providers are constantly increasing their capabilities, which, in turn, drives more complex workloads in the cloud. As workloads become more complex, they become harder to create and maintain. Workloads can be created manually through a web console or a CLI, but this approach is slow, error-prone, and not easily scalable. To combat this issue, you could create scripts that automate CLI commands to build infrastructure in a more repeatable and less error-prone manner; however, this will do little to help maintain resources or track changes. It’s for these reasons and more that Infrastructure as Code (IaC) tools were created. IaC tools are purpose-built tools used to create, maintain, track, and eventually destroy groups/stacks of infrastructure. As a result, IaC tools can significantly reduce the burden of managing your infrastructure in the cloud.
Why Terraform
Terraform is one of the most popular IaC tools out today. It is a command-line tool created by HashiCorp and is used globally by organizations big and small. Terraform is popular for several reasons, but here are some highlights:
- Multi-Provider support: Terraform can be used with all three major cloud platforms and can also be used with other providers such as GitHub and Kubernetes for expanded functionality.
- Infrastructure Management: Terraform is excellent at managing infrastructure stacks. At its core, Terraform uses the concept of state files to track resources and their state. These state files allow Terraform to be aware of changes made to your infrastructure for whatever resources it manages, even if they were made outside of the Terraform scripts.
- Collaboration: Terraform allows users to collaborate on the same infrastructure stacks without causing conflicts. This is done by storing Terraform scripts in source control such as GitHub and sharing the state file by publishing it to a remote location.
- Enhanced Functionality: Terraform provides users with enhanced functionality through Expressions. Expressions offer users a wealth of functionality by allowing them to make variable references, use operations, evaluate conditional statements, loop through collections, and make function calls to perform tasks.
Getting Started
Getting started with Terraform is relatively easy. There are a few editions of Terraform: Open Source, Terraform Cloud, and Enterprise. For the scope of this article, we will use the free, open-source edition.
Installing Terraform
The Terraform CLI can be downloaded by going to the Terraform downloads page and following the instructions for your OS.
Using Terraform CLI
Once Terraform has been installed, you can validate the installation by checking its version. To use the Terraform CLI, open a terminal/command prompt on your machine and run the command:
1
terraform version
If Terraform is installed correctly, you should see an output stating the Terraform version and your current machine’s OS. Terraform CLI has a bunch of commands that can be run. A comprehensive list of commands can be found by running the following command.
1
terraform -help
The commands are split up into two sections: main commands and other commands.
- The main commands are the primary commands required to use Terraform.
- The other commands provide additionality functionality to Terraform but are not required for basic use.
To keep things simple, we will focus on the main commands. Additional information can be found in the Terraform developer docs if you would like further details on CLI features and Terraform commands.
Basic Commands
init
1
terraform init
The init command initializes Terraform in the working directory by creating any required files and importing required components based on the configuration inferred from the Terraform files in the directory.
- Must be the first command run for new or downloaded Terraform configurations
- The init command only needs to be run once, unless a configuration change or additional options need to be applied to the configuration.
While there is advanced functionality available for the init command, it is not required. You can find more information about the advanced functionality by running the -help
flag on the init command.
1
terraform init -help
validate
1
terraform validate
The validate command checks that the local Terraform configuration is valid.
- Checks all of the Terraform files in the working directory to ensure it’s syntactically correct.
- If validation passes, a success message will be printed to the terminal.
- If an error is detected, it will be displayed in the terminal along with a debugging message.
- The default method of displaying results for the validate command is in the terminal; however, the results can be written to a JSON file using the
-json
flag.
1
terraform validate -json
plan
1
terraform plan
The plan command generates a plan of what Terraform expects the results of a deployment would be.
- The plan command will not apply any planned changes and can be used to check that a Terraform
apply
will work as expected. - The
plan
command cannot predict runtime errors, so in some circumstances, anapply
operation can still fail even if theplan
reports no errors.
Additional flags are not required when using the plan command; however, there are three advanced flags to be aware of -out
, -var
, and var-file
.
-out
The -out
flag is important because it writes the results of a plan
to a file. This file can then be used with an apply
command to run only the changes found during that specific plan
command. Without using this option when running apply
, if changes were made since the results of the plan
were generated, then additional changes could be deployed that you were unaware of. The command allows you to specify the path to write the plan file.
The out
file is a binary file, typically named tfplan.out
1
terraform plan -out=tfplan.out
-var
Use the -var
flag to provide values to variables at runtime. This is useful for values such as passwords or other sensitive data that you might want to inject at runtime.
- Multiple
-var
flags can be added to the plan command to provide multiple variable values - The format of the
-var
flag is the flag followed by the variable name equals variable valueThere must be no spaces to either side of the equals sign, or Terraform will throw an error
1
terraform plan -var name='example'
-var-file
The -var-file
flag can be added to the plan command to specify one or several different variable files. These variable files can be located in different directories than the working directory. This is especially useful if you are creating multiple environments from the same set of Terraform configurations and want to be able to reference the variable values for those environments individually.
- Multiple
-var-file
flags can be added to the plan command to provide multiple variable values - The format of the
-var-file
flag is the flag with an equals sign then the path to the desired variables file.
1
terraform plan -var-file='<path to tfvars file>'
There is additional advanced functionality that can be used with the plan command however is not required. You can find more information about the advanced functionality by running the -help
flag on the plan
command.
1
terraform plan -help
apply
1
terraform apply
The apply command is used to deploy, update, and destroy infrastructure depending on the configuration in the Terraform files in the working directory.
- When running the apply command, a Terraform
plan
will be created, then you will be promoted if you want to continue with the deployment. You can approve the deployment by typing ‘yes’. - The
output
file from a previous Terraformplan
command can be used to apply only those changes recorded in it by specifying the file name without a flag as part of the Terraformapply
:
1
terraform apply tfplan.out
Additional flags are not required when using the plan command; however, there are three advanced flags to be aware of -auto-approve
, -var
, and var-file
.
-auto-approve
The -auto-approve
flag is a convenient way to run a Terraform apply
without being prompted to confirm the deployment. This is very useful when running Terraform with automation, such as a script or CI/CD pipeline.
1
terraform apply -auto-approve
-var
The -var flag can provide the values to variables at runtime. This could be important for values such as passwords or other sensitive data that you might want to inject at runtime.
- Multiple -var flags can be added to a single plan command to provide multiple variable values
- The format of the -var flag is the flag followed by the variable name equals variable value
There must be no spaces to either side of the equals sign, or Terraform will throw an error
1
terraform apply -var name='example'
-var-file
The -var-file
flag can be added to the apply command to specify one or several different variable files. These variable files can be located in directories other than the working directory. This is especially useful if you are creating multiple environments from the same set of Terraform configurations and want to be able to reference the variable values for those environments individually.
- Multiple -var-file flags can be added to the apply command to provide multiple variable values
- The format of the -var-file flag is the flag with an equals sign and then the path to the desired variables file.
1
terraform apply -var name='example'
While there is additional advanced functionality that can be used with the apply command, it is not required. You can find more information about the advanced functionality by running the -help
flag on the plan command.
1
terraform apply -help
destroy
1
terraform destroy
The destroy
command is used to destroy the infrastructure managed by Terraform in the working directory.
- Unlike the
apply
command, which can destroy individual resources if they are removed from the configuration. Thedestroy
command destroys all resources managed by that stack. - Additional flags are not required when using the
plan
command; however, the-auto-approve
tag can be used to run the Terraformdestroy
without being prompted to confirm.
1
terraform destroy -auto-approve
Basic Concepts
Language & File Structure
Terraform has its own language for defining configurations. Configurations are stored in files using the .tf
extension. There is a JSON variation of the Terraform language; however, it is less commonly used. To use the JSON variation name Terraform configuration files using the .tf.json
extension.
Terraform CLI operates within a single top-level directory. Nested and sibling directories to the working directory are ignored. This means that in the Terraform working directory, you can have any number of Terraform configuration files. When the Terraform CLI is run, all of the files will be used and treated as a single configuration. This is helpful because it allows resources to be split out for easier readability. When working on Terraform projects, I like to create files based on their purpose.
The file structure below is what I typically use for my Terraform configurations.
1
2
3
4
5
6
7
8
9
├── environments**
| ├── <named-environment(s).tfvars>**
├── backend.tf
├── locals.tf
├── outputs.tf
├── providers.tf
├── terraform.tfvars
├── variables.tf
└── <otherfile(s)>.tf**
We’ll go more in-depth into the concepts behind some of these files in the sections below, but here is a basic breakdown of the files and their purposes.
- environments (directory)
- The
environments
directory stores variable files unique to individual environments. For example, while a single Terraform configuration may be used to create both a development and production environment, each environment requires a unique configuration of variable values. Even though, the Terraform CLI only runs in the current working directory, ignoring nested directories, the environments approach will work if the-var-file
flag is specified for bothterraform plan
andterraform apply
. - The
environments
directory is only necessary if you use a single Terraform configuration for multiple environments; otherwise, it can be excluded. - There is an alternate method of handling multiple environments called Terraform Workspaces that will be discussed later.
- The
- backend.tf
The backend.tf
file stores the Terraform configurations for remote state and external remote state imports using data blocks.
- locals.tf
- The
locals.tf
file is a dedicated place to define local values for a Terraform configuration.
- The
- outputs.tf
- The
outputs.tf
file provides a centralized location for defining all output values. This is primarily done to make the Terraform configuration easier to read.
- The
- providers.tf
- The
providers.tf
file specifis any information about providers used in the Terraform configuration. A single Terraform configuration can have multiple providers and aliases for a single provider. Providers will be covered more in-depth later, but it’s important to understand that this file is where provider configurations are stored.
- The
- terraform.tfvars & variables.tf
terraform.tfvars
andvariables.tf
work together.variables.tf
is the central place where you define variable configurationsterraform.tfvars
is where you store the actual values for thevariables.tf
definitions
terraform.tfvars
is not required in the working directory if the specific environment variables are configured as described in the environments (directory) section. However, you might still want aterraform.tfvars
file with in your working directory. For example, you could handle environment-specific values in the environments directory and use the working directory’sterraform.tfvars
to assign common value that are shared across all environments.
- Other files
- Typically, I opt to create other Terraform configuration files based on the resources they contain. Usually, I have several additional files separated for readability.
- A common convention is to create a “starting point” file named
main.tf
. This gives users a starting point to walk through the other configuration files, if needed.
Providers
While Terraform is a powerful tool and provides a lot of functionality to users, the primary purpose of Terraform is its integration with other tools and platforms. Terraform integrates with numerous providers, including major cloud providers (such as AWS, Azure, and GCP), virtualization platforms, and 3rd party tools. Providers in Terraform are the integrations required to allow the Terraform CLI to connect to and interact with these external sources.
There are two types of providers, Hashicorp-managed providers and partner providers. Hashicorp providers are created and maintained by Hashicorp. Some of the most notable providers are AWS, Azure, GCP, Kubernetes, Alibaba Cloud, and Oracle Cloud. Hashicorp has also created some utility providers that provide additional functionality to Terraform. One such utility is the Random provider . Among its other features, Random enables Terraform to create random passwords, uuids, and strings.
Partner Providers have been created by third-party vendors to provide integrations to Terraform. Some notable partner providers include Github, ElasticSearch, and Splunk. There are thousands of partner providers, from smaller cloud providers to third-party tools. Information about both Hashicorp-managed providers and partner providers can be found in the Terraform registry.
To use Terraform providers, you add references to them in your Terraform configuration. This is done through the use of the Terraform code block.
1
terraform {}
The Terraform code block provides the Terraform configuration with the required settings that it will need to run. A few settings that can be specified in the Terraform code block are the required_version, required_providers, and backend configurations. You can find more information about the Terraform code block in the documentation.
The required_version setting allows you to specify the version requirements for Terraform. Versions specified in the Terraform code block use the Version Constraint Syntax.
1
2
3
4
5
6
terraform {
required_providers {
<PROVIDER BLOCK>
}
required_version = "~> 1.3"
}
The required_providers setting is where you specify provider configurations. Use the following code block:
1
2
3
4
5
6
7
8
terraform {
required_providers {
<PROVIDER NAME> = {
source = "<NAMESPACE>/<PROVIDER NAME>"
version = "<DESIRED VERSION>"
}
}
}
The documentation for each provider provides instructions for how to add their specific configuration to the Terraform block. It’s also important to point out that providers are being constantly updated, with new versions being continuously published. When creating your Terraform Configurations, it’s important to specify specific provider versions to ensure you don’t get unintended issues from unexpected provider updates down the road.
Once the required_providers setting has been added, typically in the providers.tf
file, then the provider needs to be specified using the provider code block.
1
2
3
provider "<PROVIDER NAME>" {
# Configuration options
}
The provider block is where settings specific to the individual providers can be specified. For some providers, this could be attributes such as credentials information or location settings. To know what options are available for each provider, please see the documentation for that provider. After the required_providers and providers code blocks have been configured, the terraform init
command will use that information to download the required files to use those providers.
Example - Configuring the Random Provider
1
2
3
4
5
6
7
8
9
10
11
12
terraform {
required_providers {
random = {
source = "hashicorp/random"
version = "3.4.3"
}
}
}
provider "random" {
# Configuration options
}
The backend block is used to point Terraform to a remote state location. This block is covered in more detail in the State Management section below.
Resource Blocks
Resources are the building blocks of Terraform configurations. A resource could be a virtual machine, a database, or any number of other possibilities. Resources are the configurations for specific desired components in your infrastructure. Resources are defined using the following code block:
1
2
3
resource "<RESOURCE TYPE>" "<RESOURCE NAME>" {
# Argument Reference Values
}
Resource types are defined by the providers, and how Terraform knows what is being created. The resource name is the name you want to give that particular resource within your Terraform configuration. The combination of the resource type and resource name needs to be unique within the Terraform configuration. Resource configurations can have a combination of required and optional configuration values called argument references. Resources can also share important values, such as IDs, that can be referenced by other resources or exported as outputs. These shared values are called attribute references. NOTE: Attribute references will not be available until after a resource has been deployed using the apply
command. You can get a specific attribute reference using the following syntax:
1
<property name> = <RESOURCE TYPE>.<RESOURCE NAME>.<ATTRIBUTE REFERENCE NAME>
Example - Creating a random password using the Random Provider
1
2
3
4
5
6
resource "random_password" "password" {
length = 12
min_lower = 2
min_upper = 2
min_special = 2
}
This example creates a resource that will generate a random password that is 12 characters long and requires at least two lowercase characters, two uppercase characters, and two special characters.
Example - Referencing the generated value of the randomly created password
1
secret = random_password.password.result
The value of the password will be used to provide the value to the secret variable. NOTE: In this particular case, the result of the password is considered to be a sensitive value, so Terraform will not show the value in the console to keep it safe.
Data Sources
Sometimes resources created outside of your Terraform configuration need to be referenced as part of your configuration. Instead of recreating those resources or attempting to import them into your configuration (yes, you can import resources, but that’s outside the scope of this post), you can get information about them by using data sources. Data sources allow for the configuration of supported resources to be looked up by a value or set of values. Once imported, a data source and its attribute references can be referenced by other resources or exported using outputs. Not all providers support data sources. Data sources are defined using the following code block:
1
2
3
data "<RESOURCE TYPE>" "<RESOURCE NAME>" {
# Argument Reference Values
}
State Management
State management is a crucical component of Terraform. Terraform tracks and manages resources through the use of state files. State files track resources by recording their configuration and metadata and storing it in a file that is then used during operations such as the plan
and apply
commands to check for errors and to notify users of the expected changes that will be made during a run. There are two types of state files, the local state file, and remote state files.
The default is the local.
When the terraform apply command is run for the first time, the Terraform CLI will automatically create the state file on the local machine and name it terraform.tfstate
. If you look in the state file, you will see information about the current state of the configuration, such as the version, outputs, and resources. One thing to be aware of is that the state file stores all values, including sensitive data, in plain text. This means any sensitive data such as passwords or credentials created by or referenced using a data source will be visible in plain text for anyone with access to the state file.
Remote state files are the same as a local state file but are stored in a remote location. A remote state is set up using the backend code block in the terraform code block, similar to how required providers are defined. Backends/Remote state files differ depending on what providers are being used, and therefore vary in their configuration. For example, AWS and Azure both have their own individual ways of creating and using remote backends. Since backend configurations differ depending on what providers you are using, we won’t be covering their configuration in this article. However, if you’d like more information on setting up backends with your providers, please see the documentation for that particular provider.
One thing to consider when using a remote backend is that not all backends allow for the encryption of the state file. It is best practice to encrypt and secure the remote state files whenever possible and to never check them into source control.
Variables
Terraform allows the creation and use of variables in configurations. To use variables, they must first be defined in your configuration by using the input variables block:
1
2
3
variable "<VARIABLE NAME>" {
# Configuration
}
Variables have the following optional arguments that can be used to define them:
- default - The default value if not explicitly provided
- type - Data type of the variable; supported data types: - string - number - bool - list - map - null
- description - Description of the variable. Used to help with clarity and documentation purposes.
- validation (object) - An object of defined validation rules
- sensitive - If set to true, the value of the variable will not be shown in the UI
- nullable - specifies if the value can be null
Values to variables can be provider is a couple of different ways. The first and most common way is to provide values using a tfvars file. The tfvars file, which is often named terraform.tfvars
, is a file that stores the values of variables in a simple format:
1
<VARIABLE NAME>=<VARIABLE VALUE>
Additionally, when running the terraform plan
and terraform apply
, values can be provided using the -var flag or through environment variables on the local machine.
If a defined variable’s value is not provided and the variable definition does not have a default value, the user will be prompted for a value at runtime when running the
terraform plan
orterraform apply
commands.
In order to reference a variable value in a resource, use the following syntax:
1
example = var.example
Terraform loads variable values in a specific order, and where there is overlap, values that are loaded later will override older values. When specifying variable values, some of the following options are used less often and are therefore out of the scope of this article. Still, it’s good to know the options that are available.
Terraform loads variable values in the following order:
- Environment variables
terraform.tfvars
terraform.tfvars.json
(the JSON version of the tfvars file)- Any files named
*.auto.tfvars
or*.auto.tfvars.json
. (loaded in lexical order based on file name) - -var and -var-file command line flags
Functions
Terraform offers many built-in functions that can be used within your configuration to perform a specific action. Functions greatly enhance what can be done in configurations and unlock many possibilities for users and their workloads. Each function performs different actions and requires different information, so the best way to get started with them is to read the documentation about the specific function or functions you would like to use.
Locals
Terraform locals allow for commonly used expressions to be given a name and referenced. Locals can be used for single values or entire expressions, such as the concat or join functions. Locals are defined using the locals code block:
1
2
3
4
locals {
example = "example"
hello_world = join(" ",["hello","world"])
}
To reference a local value in your configuration, reference it using the local keyword (not to be confused with the locals keyword, which is used to define the entire block of local values):
1
example = local.example
Outputs
Terraform outputs allow users to expose specific information from within a configuration. Outputs can be defined using the following code block:
1
2
output "<OUTPUT NAME>" {
value = <OUTPUT VALUE>
Outputs can be helpful because they allow for values to be viewed from the console using the Terraform CLI and share values with other Terraform configurations using the Terraform remote state data source. The remote state data source works together with remote state backend files to share output values. Since the remote state data source is a more advanced topic, we won’t be covering it in this article, but it’s a very powerful feature of Terraform. It can be very useful if you are running multiple Terraform configurations that need to share values.
Tutorial
Now that we’ve discussed the basics of Terraform and the Terraform CLI, we’ll create a simple Terraform stack that uses the Local and Random providers to create and generate a random UUID, then save that value into a file on the local machine.
Steps
- Create the working directory on your local machine, then, in your terminal, change to that working directory
- Open the working directory in your code editor of choice (I recommend Visual Studio Code)
- Create the following files in the working directory
main.tf
outputs.tf
providers.tf
terraform.tfvars
variables.tf
Open
providers.tf
and create the following configuration in the terraform block for both the random and local providers. Additionally, create empty provider blocks for both providers.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
terraform { required_providers { random = { source = "hashicorp/random" version = "3.4.3" } local = { source = "hashicorp/local" version = "2.2.3" } } } provider "random" {} provider "local" {}
Open the
variables.tf
and create a variable definition for a file name that will be used to create the file on the local machine1 2 3 4 5
variable "file_name" { type = string description = "The desired name of the uuid file" nullable = false }
Open
terraform.tfvars
and specify the value for the filename variable1
file_name="uuid.txt"
Open
main.tf
and create the resources for the random UUID and the local file. Make sure to use the UUID created by the random UUID resource as the content in the local file resource. Also, set the filename in the local file resource to use the value from the file_name variable.1 2 3 4 5 6
resource "random_uuid" "uuid" {} resource "local_file" "uuid_file" { content = random_uuid.uuid.result filename = var.file_name }
Open
output.tf
and create an output for the file name of the UUID file stored in the file_name variable.1 2 3
output "file_name" { value = var.file_name }
Run the Terraform
init
command to initialize the Terraform configuration1
terraform init
Run the Terraform
plan
command to display a plan of the expected results1
terraform plan
Run the Terraform
apply
command, and, when prompted, type ‘yes’ to approve the deployment1
terraform apply
- Confirm that
uuid.txt
was created in the working directory and that it contains a random UUID. - Open
terraform.tfstate
to view the resources that are now being tracked by Terraform and to see what information it contains. Run the Terraform
output
command to see the value of the output1
terraform output
Once you’ve confirmed that Terraform ran as expected, destroy the Terraform resources using the Terraform
destroy
command. When prompted, type ‘yes’ to approve the destruction of the Terraform resources1
terraform destroy
- Check the working directory to confirm that
uuid.txt
has been deleted, then checkterraform.tfstate
and verify that those resources were removed from the state file.
Terraform is an incredibly powerful IaC tool. What we covered in this post are just the basics. There is a lot more functionality and features that have not been discussed. Also, the real power of Terraform is when it’s integrated with providers. We will cover more advanced features of Terraform and providers in future articles. For the time being, however, you should now have the information necessary to get started using Terraform.