Skip to main content

Resources

·2460 words·12 mins
Terraform - This article is part of a series.
Part 8: This Article

A resource in Terraform could be a virtual machine in Azure, or a file in your local file system, or a security camera mounted at the entrance of a supermarket.

Each Terraform provider exposes a number of resources. The AWS provider, for example, exposes all the different kinds of AWS cloud resources available. This could be EC2 instances, Lambda functions, DynamoDB tables, application load balancers, VPCs, EKS clusters, etc.

Resources are why we even bother with tools such as Terraform to begin with. Our goal is to create resources. Everything else around resources (variables, outputs, etc) are secondary and not very useful unless we also create some resources.

An illustration of the relationship between a resource in Terraform and the manifestations of these resources outside of the Terraform configuration is shown in the following figure:

RRTeeessroRorueuarsrfcocoeuerrmceConfigurationAmaLzaomnbdWaebFuSnecrtviiocnesMicArpopsoSfetrvAizcuereGooCglloeudClFouundctPiloantform

In this lesson I continue to go through part 8 of the Certified Terraform Associate exam curriculum. This part of the curriculum is outlined below:

PartContent
8Read, generate, and modify configuration
(a)Demonstrate use of variables and outputs
(b)Describe secure secret injection best practice
(c)Understand the use of collection and structural types
(d)Create and differentiate resource and data configuration
(e)Use resource addressing and resource parameters to connect resources together
(f)Use HCL and Terraform functions to write configuration
(g)Describe built-in dependency management (order of execution based)

To be specific: I will cover parts of 8 (d) and (e) in this lesson. I will also briefly touch on the subjects in 8 (g).

Declaring a resource in HCL
#

The general format of the resource block in Terraform looks like this:

resource "resource_type" "local_name" {
    argument1 = <expression>
    argument2 = <expression>
    argument3 = <expression>
    ...
}

The resource block has two labels:

  • The first label is the resource type. If we take the Azure provider (named azurerm) as an example, a resource type could be azurerm_storage_account. The provider specifies which resource types are available.
  • The second label of the resource block is the local name of the resource. This name is used to refer to the resource in other parts of your Terraform configuration. Note that this name is something that you decide yourself as long as it starts with a letter or underscore and only contains letters, digits, underscores, or dashes!

The combination of the two labels, resource type and local name, uniquely identifies this resource in your Terraform configuration. You can’t have two resources of the same type with the same local name.

In general each resource has a certain number of required arguments (and sometimes there could be required nested blocks). How many and what the arguments are will vary depending on the provider and the resource type. Most likely you will need to use the documentation to find out what the required arguments are. Let’s look at two examples!

A storage account in Azure
#

The Azure provider is named azurerm1. A storage account in Azure is a resource where you can store data in various storage media, for instance as blobs in blob storage. Every resource in Azure must be placed in a resource group2, so in this example we will create both a resource group and a storage account:

resource "azurerm_resource_group" "my_resource_group" {
  name     = "my-resource-group"
  location = "northeurope"
}

resource "azurerm_storage_account" "my_storage_account" {
  name                     = "mystorageaccount"
  resource_group_name      = azurerm_resource_group.my_resource_group.name
  location                 = azurerm_resource_group.my_resource_group.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

Let’s first go through the resource group resource:

  • The first label is the resource type: azurerm_resource_group.
  • The second label is the resource local name: my_resource_group.
  • Two arguments are specified in the resource body: name and location. The values are strings and they must follow rules set by Azure (e.g. the location value must be a valid data center location for Azure).

Now let’s go through the storage account resource:

  • The first label is the resource type: azurerm_storage_account.
  • The second label is the resource local name: my_storage_account.
  • Five arguments are provided:
    • The name argument is a simple string specifying the name of the storage account.
    • The resource_group_name argument specifies which Azure resource group this storage account should be placed in, and the value is a reference to the resource group we created earlier. More about references below.
    • The location argument specifies in which Azure data center this storage account should be placed, and we use another reference to the resource group in order to use the same location.
    • The account_tier and account_replication_type arguments are both strings with values that must be valid values for a storage account, details of which can be found in the documentation.

One observation we can make at this point is that in order to create resources in Terraform we must be familiar with the underlying platform we are working with, in this case Azure. Otherwise we won’t get the details correct.

An S3 bucket in AWS
#

The AWS provider is named aws. An S3 bucket is an instance of the blob storage service in AWS. An S3 bucket resource looks like this:

resource "aws_s3_bucket" "my_s3_bucket" {
  bucket = "my-s3-bucket"
}

There are a few differences between an S3 bucket in AWS and a storage account in Azure, even if they are used for similar things. There is no need to place an S3 bucket in a resource group, because resource groups are not mandatory in AWS. We do not configure a specific region for the S3 bucket, because the AWS provider is configured with a region that will be used for all resources we create using it. In fact, the only required argument is the name of the bucket, this we specify in the bucket argument.

This illustrates how different providers and different resources will behave very different from each other.

References
#

It is often necessary to reference one resource in another resource. Or you want to extract some output value from one resource, so you must reference it in an output block. You make a reference to a resource using:

<resource type>.<resource local name>.<property name>

We saw an example above, where I created an Azure storage account and made two references to a resource group:

resource "azurerm_storage_account" "my_storage_account" {
  name                     = "mystorageaccount"
  resource_group_name      = azurerm_resource_group.my_resource_group.name
  location                 = azurerm_resource_group.my_resource_group.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

The first references extracts the name property of the resource group, and the second reference extracts the location property. How do you know what properties are available? This also depends on the specific provider and resource type that you are using, so you need to look at the documentation for that resource.

An example of what an output block with a reference to an S3-bucket resource might look like is this:

output "my_output {
  value = aws_s3_bucket.my_s3_bucket.arn
}

In this output I extract the ARN (Amazon Resource Name) of the S3-bucket. This is like a resource ID in Azure.

Resource references provide a way to tell Terraform that there are dependencies between your resources. In the example of the storage account and resource group in Azure I know that I need the resource group to exist before the storage account can be created. Does Terraform know this? Not necessarily. But by making an explicit reference to my resource group in the resource block of the storage account I make an implicit dependency between them. Terraform will create the resource group first in order to be able to extract the name and location properties of the resource group and fulfill the references that I specified. If I did not have these references in place in this example then Terraform would try to create both the resource group and the storage account at the same time, and it would most likely fail to create the storage account because the resource group would not exist (until a few seconds later).

Meta arguments
#

Apart from resource specific arguments that varies from resource to resource and from provider to provider, there are additional meta arguments that are available on all resources. Full documentation for all meta arguments are available in the official documentation, below I will go through the ones I have met during the certification exam as well as in real-life.

depends_on
#

I mentioned implicit dependencies above, which are dependencies defined by making a reference to a resource. It is also possible to make an explicit dependency with the depends_on meta argument. This meta argument accepts a list of references to other resources that must be created before a given resource is created. Let me rewrite the example with a resource group and a storage account using explicit dependencies:

resource "azurerm_resource_group" "my_resource_group" {
  name     = "my-resource-group"
  location = "northeurope"
}

resource "azurerm_storage_account" "my_storage_account" {
  name                     = "mystorageaccount"
  resource_group_name      = "my-resource-group"
  location                 = "northeurope"
  account_tier             = "Standard"
  account_replication_type = "LRS"

  depends_on = [
    azurerm_resource_group.my_resource_group
  ]
}

Here I added the depends_on meta argument with a list containing a single reference to the resource group. Terraform will create the resource group first, and then continue by creating the storage account.

for_each and count
#

Both the count and the for_each meta arguments are used to create multiple copies of a resource. You can think of them like loops in other programming languages.

The count meta argument is the simplest to get started with. An example of how this is used is this:

resource "azurerm_resource_group" "resource_groups" {
  count = 3

  name     = "my-resource-group-${count.index}"
  location = "northeurope"
}

This resource block creates three (count = 3) azurerm_resource_group resources. In order for Azure to accept these resource groups I need to use the count index in the resource group name (my-resource-group-${count.index}). My resource groups will be named my-resource-group-0, my-resource-group-1, and my-resource-group-2 (the index starts at 0).

Moving on to the for_each meta argument! This meta argument is similar to the count meta argument, but it creates one copy for each element in a map or a set of strings.

An example of for_each with a set of strings looks like this:

resource "azurerm_resource_group" "resource_groups" {
  for_each = toset( ["rg1", "rg2", "rg3", "rg4"] )

  name     = each.key
  location = "northeurope"
}

I create four storage accounts where the names come from the set provided in the for_each meta argument. I get a handle for the name in the each.key expression.

An example of using for_each with an object instead of a set looks like this:

resource "azurerm_resource_group" "resource_groups" {
  for_each = {
    rg1 = "northeurope"
    rg2 = "westeurope"
    rg3 = "westus2"
    rg4 = "eastus"
  }

  name     = each.key
  location = each.value
}

Here I also create four resource groups, each in a different location. I can access the different properties in my object with each.key and each.value.

provider
#

When we went through the concept of providers we glossed over one detail. Can we add two instances of the same provider? It turns out we can!

First of all: why would we want to have two instances of the same provider? Let us take the aws provider as an example. When you configure the aws provider you must specify a region that the provider will use to create resources in. If you must create resources in several regions through the same Terraform configuration you could configure several aws providers, one for each region.

Once you have configured several instances of a provider you must explicitly select the provider instance you want to use, and this is done using the provider meta argument.

An example Terraform configuration where I configure two instances of the aws provider and then create two S3-buckets in different regions looks like this:

terraform {
  // specify aws as a required provider, this is only done once
  required_providers {
    aws = {
      source = "hashicorp/aws"
    }
  }
}

// configure the default aws provider
provider "aws" {
  region = "eu-north-1"
}

// configure an alternative aws provider with an alias of "us"
provider "aws" {
  alias  = "us"
  region = "us-east-1"
}

resource "aws_s3_bucket" "europe_bucket" {
  bucket = "my-bucket-eu-north-1"
}

resource "aws_s3_bucket" "us_bucket" {
  provider = aws.us
  bucket   = "my-bucket-us-east-1"
}

There are a few important pieces in this configuration:

  • We only need to specify the aws provider as a required provider once, even if you intend to create several instances of it (i.e. think if it as an import-statement in a normal programming language).
  • We create one provider block for each instance of the provider we want to configure.
  • The default provider instance does not specify an alias argument, but each additional instance does. So in this configuration I have a default provider in the eu-north-1 region and an aliased instance in the us-east-1 region.
  • When we create a resource we can either explicitly specify what provider we want to use by referring to the provider through <provider>.<alias>, e.g. aws.us. If we do not specify a provider it will instead use the default provider.

Summary
#

This was a long lesson! However, it is an important one because resources are the most important pieces when it comes to working with Terraform. In this lesson we covered:

  • What resources are.
  • How we create a resource in HCL using the resource block.
  • How we can create a reference to a resource and extract property values from it.
  • What a few important meta arguments are and what they do:
    • depends_on creates explicit dependencies between resources.
    • for_each and count works like loops, allowing you to create multiple copies of a resource using a single resource block.
    • provider allows us to select which provider to use for a given resource, in case we have configured multiple instances of the same provider.

  1. The rm part of azurerm is short for resource manager, it comes from the name of the Azure service responsible for control plane operations known as Azure Resource Manager. ↩︎

  2. Note that resource in resource group is not the exact same concept as a resource in Terraform. A resource group is an Azure concept that exists independently from Terraform. However, an Azure resource group could also be a Terraform resource. It is easy to get confused here! ↩︎

Mattias Fjellström
Author
Mattias Fjellström
Cloud architect · Author · HashiCorp Ambassador
Terraform - This article is part of a series.
Part 8: This Article