Home A Beginners Guide to Terraform
Post
Cancel

A Beginners Guide to Terraform

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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, an apply operation can still fail even if the plan 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 value

    There 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 Terraform plan command can be used to apply only those changes recorded in it by specifying the file name without a flag as part of the Terraform apply:
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. The destroy 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 Terraform destroy 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 both  terraform plan and terraform 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.
  • 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.
  • 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.
  • 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.
  • terraform.tfvars & variables.tf
    • terraform.tfvars and variables.tf work together.
      • variables.tf is the central place where you define variable configurations
      • terraform.tfvars is where you store the actual values for the variables.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 a terraform.tfvars file with in your working directory. For example, you could handle environment-specific values in the environments directory and use the working directory’s terraform.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_versionrequired_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 or terraform 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:

  1. Environment variables
  2. terraform.tfvars
  3. terraform.tfvars.json (the JSON version of the tfvars file)
  4. Any files named *.auto.tfvars or *.auto.tfvars.json. (loaded in lexical order based on file name)
  5. -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

  1. Create the working directory on your local machine, then, in your terminal, change to that working directory
  2. Open the working directory in your code editor of choice (I recommend Visual Studio Code)
  3. Create the following files in the working directory
    1. main.tf
    2. outputs.tf
    3. providers.tf
    4. terraform.tfvars
    5. variables.tf
  4. 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" {}
    
  5. Open the variables.tf and create a variable definition for a file name that will be used to create the file on the local machine

    1
    2
    3
    4
    5
    
     variable "file_name" {
       type = string
       description = "The desired name of the uuid file"
       nullable = false
     }
    
  6. Open terraform.tfvars and specify the value for the filename variable

    1
    
     file_name="uuid.txt"
    
  7. 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
     }
    
  8. 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
     }
    
  9. Run the Terraform init command to initialize the Terraform configuration

    1
    
     terraform init
    
  10. Run the Terraform plan command to display a plan of the expected results

    1
    
    terraform plan
    
  11. Run the Terraform apply command, and, when prompted, type ‘yes’ to approve the deployment

    1
    
    terraform apply
    
  12. Confirm that  uuid.txt was created in the working directory and that it contains a random UUID.
  13. Open terraform.tfstate to view the resources that are now being tracked by Terraform and to see what information it contains.
  14. Run the Terraform output command to see the value of the output

    1
    
    terraform output
    
  15. 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 resources

    1
    
    terraform destroy
    
  16. Check the working directory to confirm that uuid.txt has been deleted, then check terraform.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.

This post is licensed under CC BY 4.0 by the author.
Contents