Learning Terraform – Part 2: Variables, Expressions and Functions

This post is a continuation of my series of blog posts on learning Terraform.  In my previous post, I covered the fundamentals of Terraform.  In this post, I want to dive a little deeper and discuss using variables, count, conditional expressions and functions in Terraform.  Let’s get started with variables.

Variables

Variables allow you to define reusable values for a Terraform configuration.  They are typically saved separately from any other configuration files which makes them easy to read and edit.

Terraform supports both input and output variable formats. Input variables are used to define values that help configure infrastructure. These values can be used repeatedly throughout the Terraform configuration. Output variables are used to get information about the infrastructure after deployment (such as server IP addresses).

Input Variables 

Input variables are usually defined within a variable block.

variable "variable_name" {

}

To reference a variable in a Terraform configuration, use var.variable_name as shown below.

resource "azurerm_resource_group" "rg" {
     name = "rg-example"
     location = var.location
}

Type Constraints

The variable declaration can include a type constraint which limits the type of value accepted by the variable.

variable "instance_count" {     
     type = number
}

The type constraint is optional, but it is a best practice to define it.  This not only provides a reminder of the variable data type but also allows Terraform to return an error message if the wrong data type is used.

$ terraform plan
var.instance_count
  Enter a value: string

Error: Invalid value for input variable

The value entered for variable "instance_count" is not valid: a number is required.

Supported type constraints in Terraform are:

  • String
  • Number
  • Boolean

Default Values

Default values can also be included in the variable declaration.  If a default value is defined, it will be the value applied if a variable value is not defined when either referencing the variable or during a terraform plan or terraform apply. If a type constraint and default variable are both defined, the default value must correspond to the specified type.

variable "instance_count" {
     type = number
     default = 1
}

Variable Types

Input variables support a few different types including string,  list, map, and boolean.  Examples of each are shown below.

String 

String values are simple and typically used to store names.

variable "location" {
type = string
default = "westus2"
}

List

A Terraform list is a zero based, comma delimited list encapsulated in square brackets [].  

variable "vm_size" {
     type = list
     default = ["Standard_A1_v2", "Standard_A8_v2", "Standard_A8m_v2"]
}

To access an item from the list, just include the index of the value when referencing the variable as shown in the example below.

resource "azurerm_linux_virtual_machine" "linux_vm" {
     name = "linux_vm"
     resource_group_name = var.rg
     location = var.location
     size = var.vm_size[0]
}

Map

A Terraform map is key/value collection.  Maps can be used to select specific values based on a user defined key.

variable "vm_type" {
     type = map
     default = {
       compute = "Standard_F8s_v2"
       memory = "Standard_D12_v2"
       storage = "Standard_L32s_v2"
     }
}

To reference an item from a mapped list, use the key to reference the correct item as shown below.

resource "azurerm_linux_virtual_machine" "linux_vm" {
     name = "linux_vm"
     resource_group_name = var.rg
     location = var.location
     size = var.vm_type["compute"]
}

Boolean

A boolean provides the option of a true or false value for a variable.

variable "password_auth" {
     default = "true"
}

To reference a Boolean variable in a configuration file, use the var syntax.

disable_pasword_authentication = var.password_auth

It is currently recommended to specify boolean values as strings.  A future version of Terraform will provide first class support for booleans but using string values should result in backward compatibility.

Assigning Values to Variables

Values can be assigned to variables using a couple different methods:

At the command line using the -var option.

$ terraform plan -var="location=westus2" 

Using environmental variables.

$ export TF_VAR_region=westus2 
$ terraform plan

If multiple variables need to be set, this can be done using a variable definition file. Terraform automatically loads variable definition files if they are named as follows:

terraform.tfvars
terraform.tfvars.json

Terraform also supports filenames ending in .auto.tfvars or .auto.tfvars.json.

<name>.auto.tfvars
<name>.auto.tfvars.json

Variable definition files use the same syntax as Terraform files, but only contain variable values.

location = "westus2"

Variable definition files ending in .json are are parsed as json objects.

{
     "location": "westus2"
}

For more information in using variables in Terraform, browse to the Terraform documentation.

Output Values

Terraform output values are similar to a return value.  They can be used to output certain values to the CLI during a terraform apply or to provide inputs to other resources created by Terraform.

Output values are declared using an output block.  The label after the output keyword is the name of the output.

output "vm_password" {
value = azurerm_linux_virtual_machinev.linux_vm.admin_password
}

Output values are available by using the following expression module_name.output_name.  If the example output block above resided in a module called ‘dev_server’, the output would be accessed as follows:  module.dev_server.vm_password.

Optional arguments are also supported for output values.  More details on these can be found in the Terraform documentation.

File Structure

Initially it may seem convenient to put resource blocks, variable blocks, variables and outputs in the same file, but as your infrastructure grows this practice isn’t sustainable. In order to maintain readability and reduce errors it’s recommended to break these into logical files.

/terraform
     /environment (dev, staging, prod)
          main.tf
          variables.tf
          terraform.tfvars
          output.tf

Count

The count parameter lets you scale the number of resources deployed by defining a count value in the resource block. 

resource "azurerm_resource_group" "rg" {
     name = "rg-example"
     location = var.location
     count = 2
}

When deploying multiple resources, each resource may need a different attribute such as name, IP addresses, etc.  Using count.index, the attribute can be updated with each iteration in the loop.  In the example below, three Azure resource groups are getting created:

resource "azurerm_resource_group" "rg" {
     name = "rg-example"
     location = var.location
     count = 3
}


Rather than each resource group being called rg-example, count.index can appended to the name as shown in the example below.

resource "azurerm_resource_group" "rg" {
     name = "rg-example.${count.index}"
     location = var.location
     count = 3
}

The resulting output would be:

rg-example.0
rg-example.1
rg-example.2

There are times when adding the index to the attribute doesn’t provide enough information or detail about the resource.  In this situation, variables can be used.  In the following example, each resource group name is declared in a list variable:

variable "rg_name" {
type = list
default = ["rg-dev", "rg-staging", "rg-prod"]
}

After defining the list variable, the resource block is updated to use the variable instead of the hard coded value.

resource "azurerm_resource_group" "rg" {
     name = var.rg_name[count.index]
     location = var.location
     count = 3
}

The resulting output would be:

rg-dev
rg-staging
rg-prod

Conditional Expressions

Conditional expressions use a boolean expression to select one of two values.

condition ? true_value : false_value

A great example of this is using a conditional expression to determine whether to configure a development or production environment.  In this example there are two Azure Linux virtual machines, one for development and one for production.  The count parameter and a conditional expression will determine which resource block to configure. 

First create a isproduction variable in the variables.tf file.

variable "isproduction" {}

Assign a value in the terraform.tfvars file.

isproduction = true

Add the count parameter and conditional logic to the development virtual machine resource block

resource "azurerm_linux_virtual_machine" "dev" {
     name = "vm-development"
     resource_group_name = var.rg
     location = var.location
     size = var.vm_size[0]
     admin_username = "admin_user"
     network_interface_ids = [
       azurerm_network_interface.development.id
     ]
     # If isproduction is false create VM
     count = var.isproduction == false ? 1 : 0
}

Add the count parameter and conditional logic to the production virtual machine resource block

resource "azurerm_linux_virtual_machine" "prod" {
     name = "vm-production"
     resource_group_name = var.rg
     location = var.location
     size = var.vm_size[2]
     admin_username = "admin_user"
     network_interface_ids = [
       azurerm_network_interface.production.id
     ]
     # If isproduction is true create VM
     count = var.isproduction == true ? 1 : 0
}

The value of the isproduction variable set in the terraform.tfvars file will determine which virtual machine is created. In this example prod virtual machine would be created.

For more details on Terraform’s conditional expression functionality, browse to the Terraform documentation.

Functions

Terraform includes several built-in functions that can be called from within expressions to transform and combine values.  The typical syntax for a function is the function name followed by comma separated arguments in parentheses. 

max(10, 20, 30)

Terraform does not support user defined functions, so only the built-in language functions are available.  Terraform functions can also be run from the Terraform console by using the Terraform console command.

$ terraform console
> max(10,20,30)
30

For more details on Terraform’s built-in functions, browse to the Terraform documentation.

Summary

In this blog post I discussed Terraform variables, count, conditional expressions and functions. These Terraform features provide a significant amount of flexibility and expressiveness. In the next part of this Learning Terraform series I’ll dive into some more features and functionality including provisioners and modules.

Thank you for reading!