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:
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:
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 beazurerm_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
1. 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
andlocation
. 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
andaccount_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.
- The
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 theeu-north-1
region and an aliased instance in theus-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
andcount
works like loops, allowing you to create multiple copies of a resource using a singleresource
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.
-
-
The
rm
part ofazurerm
is short for resource manager, it comes from the name of the Azure service responsible for control plane operations known as Azure Resource Manager. ↩ -
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 Terraformresource
. It is easy to get confused here! ↩
Top comments (0)