Terraform Certification (Part 8): Resources
Tags: hashicorp, terraform
Published: 2023-04-05 10:05:00

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:

![terraform resources](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/03wiax6cp7uaza0uzqb9.png)

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:

| Part  | Content                                                                       |
| :---: | ----------------------------------------------------------------------------- |
| **8** | **Read, 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:

```hcl
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 `azurerm`[^rm]. 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 group[^rg], so in this example we will create both a resource group and a storage account:

```hcl
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:

```hcl
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:

```hcl
<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:

```hcl
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:

```hcl
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](https://developer.hashicorp.com/terraform/language/resources/syntax#meta-arguments), 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:

```hcl
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:

```hcl
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:

```hcl
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:

```hcl
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:

```hcl
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.

[^rg]: 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!
[^rm]: 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.