Terraform 101

Author by Matt Sidwell

This blog also appears as part of a Terraform classroom series: Terraform 101

 1.1        Terraform Basics

Terraform is a provider-based Infrastructure-as-Code (IaC) solution for idempotent multi-cloud infrastructure management. Infrastructure components are defined as declarative code in the form of resource blocks which are stored within Terraform configuration files. Resource blocks define all aspects of a given resource within the bounds of the associated Terraform provider which handles translation and communication of Terraform code to the cloud provider API.

The declarative syntax of Terraform allows for a wide range of choices when it comes to organization. Any and all *.tf files within the working directory will be loaded by Terraform when it is called. For this reason, it is trivial to break up a set of configuration files into directories with separate variable files. File structure and file distribution can change depending on the complexity and needs of a given deployment.

1.2         Resource Blocks

Resource blocks within Terraform files consist of a resource declaration with all associated configuration data contained within. The following example shows a resource block which defines a subnet in Azure.

resource "azurerm_subnet" "subnet001" {
  name  = "mySubnet"
  virtual_network_name = "myVnet"
  resource_group_name  = "myRG"
  address_prefix  = ""

The two strings after resource are the resource type and resource ID respectively. Resource type is a reference to the Azure resource type for the provider block which will be covered in a later section. The resource ID is a user-defined tag for the resource which can be referred to using Terraform variables. The combination of type and ID must be unique within a given Terraform deployment. Key-value pairs within the resource block define the configuration of the given resource. Required and optional keys are given by the cloud provider specifications.

1.3         Variable Blocks

Variables in Terraform are declared using variable blocks like the example below. They have typedescription, and default parameters. The type parameter defines the variable as a string, list, or map. Descriptions simply act as a comment which gives context to a variable.  The default parameter defines the default value of the variable in the event that it is not explicitly set.

variable "dataDiskCaching" {
  type = "string"
  description = "Caching for data disk (None, ReadOnly, or ReadWrite)"
  default = "ReadOnly"

Variables can be referenced in multiple ways. They can be explicitly defined within a variables file, environment variables, or runtime command line inputs. In addition, they can be implicitly defined using references to resource blocks by using their type and ID. The previous example resource block has been copied below with a few modifications to showcase the utility of variables in Terraform.

resource "azurerm_subnet" "subnet" {
  name  = "${var.prefix}subnet"
  virtual_network_name = "${azurerm_virtual_network.vnet.name}"
  resource_group_name  = "${azurerm_resource_group.tf_azure_guide.name}"
  address_prefix  = "${var.subnet_prefix}"

Strings can be interpolated by using “${}” sets. The entries which start with “var.*” refer to explicit variables, while the ones that start with a resource type and ID refer to other existing configuration values within the Terraform deployment. Implicit resource references also create dependency links which define the order in which resources must be created.

Credentials often must be passed into resource blocks. Secure variable declarations can exist in a master variable file, but actual keys should be stored in a separate values file in secure storage for use during plan and apply steps.

1.4         Provider Blocks

Terraform connects to a myriad of infrastructure solutions using intermediate API translators called providers
For instance, the azurerm provider uses contributor access to an Azure subscription in order to make changes in that environment. Credentials for said service principal can be created from the Azure CLI or the Azure portal. Once created, the credentials can be entered in a local shell through az login or as defined values for automated deployments within Terraform configuration files.

A provider block contains all the information needed to initialize a connection with the infrastructure solution in question. For instance, an azurerm provider block can contain the service principal credentials for accessing a given Azure subscription for automated deployments. In addition to credentials, a version specification can be added to prevent unwanted provider version upgrades at apply time.

provider "azurerm" {
  version = "~> 1.21"
  subscription_id = "${var.azure_sub_id}"
  client_id = "${var.azure_client_id}"
  client_secret = "${var.azure_client_secret}"
  tenant_id = "${var.azure_tenant_id}"

1.5         Modules

Terraform modules simply consist of Terraform code that is used as a repeatable group. Any valid Terraform deployment code can be used as a module. What makes a given set of Terraform code a module is that it gets called by a root module by using a module block. The only required input for module blocks is the source of the module. Modules can be sourced from a variety of locations including local files or GitHub. For example, a local module could be called by the following module block where the source input is set to a local directory called “terraform_naming_module”.

module "sccm_sn" {
  source                    = "./terraform_naming_module"
  resource_type_input       = "subnet"
  business_unit_input       = "${var.business_unit}"
  workload                  = "SCCM"
  environment_input         = "${var.environment}"
  location_descriptor_input = "${var.location}"
  naming_index              = "01"

This block not only declares a module, but also passes variables into the module to use during processing. In addition, modules can use output blocks to pass resource information and calculated values back to the parent deployment after processing. Between module inputs and outputs, their value in repeatability becomes clear. A single module can be used many times to create similar resources with slightly different names, sizes, shapes, etc. without reinventing the wheel every time. This saves time, effort, and helps maintain consistency between deployments within an organization since they can all use the same set of standardized modules in their work.

1.6         Terraform State

Known resource states are stored as JSON data in a state file which Terraform references when running a plan or apply step to decide whether any resources will be created, changed, or destroyed. When running Terraform locally, a state file is automatically created when a plan or apply command is given to Terraform. However, it is best practice to use a remote state file when working in a team since state files need to be locked during deployments. If team members each had their own copy of the state file, resource consistency will be lost.

At plan/apply time, Terraform compares the current state of resources with their expected state in the state file. If a resource deviates from expected state, it will be changed or recreated during an apply step. Also, if a configuration step fails during apply then the resource will be marked as tainted for further remediation. Workspaces are used to separate code level environment state files from one another while still using a common set of Terraform configuration files.

1.7         Terraform Commands

The Terraform CLI has a compact set of commands for managing deployments. The general flow of a Terraform deployment consists of init, plan, and apply steps. There are other more advanced commands, but those are for specific circumstances which fall outside the scope of this introduction.

Terraform init initializes a Terraform deployment directory with the required data and provider specifications for running further Terraform commands. In addition, it will configure a remote backend if the required credentials and addresses are present. This command is always safe to run as it does not touch resources.

Terraform plan performs a state refresh and compares configuration files to the current state of resources. If a delta is discovered, Terraform will mark that resource as requiring modification, destruction, or recreation. It then outputs a full intended configuration set as well as a simplified counter of resources that will be changed, destroyed, or left untouched. This command can be used to output a Terraform plan file which is used during the apply step to perform the exact changes specified by plan. Otherwise, apply will get a plan on its own when it is run without a specified plan file.

Terraform apply performs the actual deployment of resources into a given environment. By default, it requires user input to confirm a deployment but the “-autoapprove” flag skips this step. As mentioned above, it will run a plan step on its own or can be fed a plan file with an expected run set.


Exercise 1: Creating a Resource

Create a resource block for your preferred resource provider. Good starting resources are resources with few required parameters like an AWS S3 bucket or Azure resource group.

Test the structure of your .tf file by running terraform init or terraform validate. Make sure you try actually deploying the resource by using your preferred CLI to login to the resource provider and running terraform apply. For instance, log into the AWS CLI using aws configure or log into the Azure CLI using az login

Your answer should look similar to the example provided in section 1.2

Exercise 2: Using Variables

Using the resource block created in the first exercise, turn all of the hardcoded resource parameters into variable references. This requires that you declare a variable block for each parameter referenced in this manner.

As before, make sure to test your configuration using a combination of terraform initterraform validate, and terraform apply. You will be asked to input values for your variables at plan/apply time.

Your resource block should look similar to the resource example in section 1.3, and it should be accompanied by a number of variable blocks which look like the example declaration in that same section.

Exercise 3: Organizing with .tf Files

Using the file contents from the second exercise, create a second .tf file and move your variable declarations to it. Test and apply your configuration and see that it creates the same resource as your answer form the previous exercise.