Here is some information about this architecture.
Here are the steps you can follow to build this solution on your own.
As we’ve learned, resource blocks are the heart of Terraform. Resource blocks can represent one or more infrastructure resources. Resource blocks themselves are defined by the Terraform provider, such as AWS or Azure. Each resource block has a type name, a label, and one or more arguments.
In addition to arguments, resources also have meta-arguments. These special arguments can be used with every resource type. These meta-arguments are as follows:
depends_on
- handle resource dependencies that Terraform doesn’t automatically infer.
count
- sets the number of resources to create e.g. 2 EC2 instances instead of the default 1.
for_each
- loops through a map to create a resource per item in the map.
provider
- sets the provider to be used.
lifecycle
- a way to customize the resource lifecycle behavior.
Let’s learn about each of these now.
depends_on
Meta-ArgumentCloud services, and specific architectures, often have resource dependencies. For example, you can create the subnet for a VPC without the VPC being created first.
Terraform can usually figure this stuff out with its Resource Graph. The resource graph generates a dependency graph when generating a plan or refreshing state.
However, sometimes the dependency graph does not get configured properly. This shouldn’t happen often, but when it does, you can use the depends_on
meta-argument.
When used, the depends_on meta-argument will make Terraform complete all actions on the dependency resource first, before performing actions on the resource that it was set on.
Setting the depends_on
meta-argument is done as follows.
resource "aws_instance" "web_server" {
# other attributes
depends_on = [
aws_iam_role_policy.example
]
}
By default, a resource block configures just one infrastructure object. Sometimes you will want to use one resource block to configure several of the same resource without having to write multiple blocks.
A simple example of this are EC2 instances. Sometimes one just isn’t enough! You can use count
to set how many instances should be created.
You can set count
with a number, like 2, or calculate based on an expression. You could count the number of items in a list, or use a conditional expression, amongst many possibilities.
resource "aws_instance" "web_server" {
count = 2
}
When you use count to create multiple instances, you’ll need a way to refer to those instances in other parts of your code, like outputs. Terraform will create an index for each resource object created. These resources are referenced like so: <TYPE>.<NAME>[<INDEX>]
.
Let’s see how you’d reference instances with and without count.
# when only one instance is created (no count)
output "web_server {
value = aws_instance.web_server
}
# when count is set; reference all web servers created using splat
output "web_server {
value = aws_instance.web_server[*]
}
# when count is set; reference a specific index
output "web_server {
value = aws_instance.web_server[1]
}
for_each
Meta-ArgumentThe for_each
meta-argument is used for a similar purpose as count. It can create more than one resource object within a single resource block.
So when should you use for_each over count? Here is the rule of thumb:
If the resources to be created are identical, or close to identical, use count
If the resource attributes have meaningful differences, use for_each
The for_each
meta-argument is more powerful.
When you use for_each
, the each
object becomes available in the resource block. This object has the following values (per the documentation):
each.key
— The map key (or set member) corresponding to this instance.
each.value
— The map value corresponding to this instance. (If a set was provided, this is the same as each.key
.)
Here’s how to use the for_each
meta-argument:
resource "aws_iam_user" "boss_accounts" {
for_each = toset( ["Todd", "James", "Alice", "Dottie"] )
name = each.key
}
This will iterate through the set in for_each
, and create a resource object with the set item name using each.key
.
provider
Meta-ArgumentWhen you create a project, you will set a default provider using the provider block. However, there are times you may need to use two different providers. For example, if you want to create a multi-region deployment on AWS.
When you create provider blocks, the first one is used as the default. All resources will be created on this provider unless you configure otherwise.
Here’s an example of creating more than one provider.
# this will be the default provider; no alias argument is set
provider "aws" {
region = "us-west-2"
}
# this is an additional provider
provider "aws" {
alias = "failover"
region = "us-east-2"
}
Then, you an specify what provider is to be used for a given resource as follows:
resource "aws_instance" "web" {
# rest of arguments
provider = aws.failover
}
It’s that simple!
lifecycle
Meta-ArgumentResources created with Terraform have a lifecycle. The lifecycle is the process of creating, updating, and destroying resources. This is documented in the Resource Behavior doc.
The lifecycle
meta-argument lets you control some aspects of the resource lifecycle. For example, you may want to protect a resource from being deleted. This meta-argument can be applied to any resource type.
Here are the lifecycle actions you can hook in to:
create_before_destroy
- Some cloud resources cannot be updated, or changed, in place. Rather, they must be deleted and created again. This is a limitation set by the cloud resource, not Terraform. The normal behavior is for Terraform to destroy the resource first, and then create it again. If you don’t like this default behavior, you can use create_before_destroy
to have the resource created first. There are some things to consider when you do this, such as the name of the remote resource.
prevent_destroy
- This can be used as a safety measure to prevent from accidental removal of resources. All you have to do is set this value to true, and Terraform will report an error if you try to destroy it.
ignore_changes
- There are cases where you can create a resource with Terraform that references other resources that may change. If you set ignore_changes
to true, Terraform will ignore any changes that occur to those referenced resources.
replace_triggered_by
- You can use this to force a replacement of the resource if any referenced values change. For example, if you have two instances, you can force one to be replaced if the other changes it’s private IP address. There are a lot of possibilities here.
Here is an example of how to use the lifecycle meta-argument. Please check out the full docs to see more examples.
# will create a new instance before destroying the old one
resource "aws_instance" "web {
# ...
lifecycle {
create_before_destroy = true
}
}
It’s time to get some experience working with these concepts.
If you're using the Skillmix Labs feature, open the lab settings (the beaker icon) on the right side of the code editor. Then, click the Start Lab button to start hte lab environment.
Wait for the credentials to load. Then run this in the terminal.
Be sure to enter in your own access key and secret key and name your profile 'smx-lab'.
$ aws configure --profile smx-lab
AWS Access Key ID [None]:
AWS Secret Access Key [None]:
Default region name [None]: us-west-2
Default output format [None]:
Note: If you're using your own AWS account you'll need to ensure that you've created and configured a named AWS CLI profile named smx-lab.
Let’s start by creating a working directory, the main.tf
file, and adding the initial terraform
config. Use the below to create the directory and config.
$ mkdir meta-arg-lab
$ cd meta-arg-lab
$ touch main.tf
Open the main.tf file and add this code:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
Next, we’ll add the provider configs. We will create both a default and secondary provider. Append this code to the main.tf file.
// previous code
# default provider
provider "aws" {
profile = "smx-lab"
region = "us-west-2"
}
# this is an additional provider
provider "aws" {
profile = "skillmix-lab"
alias = "secondary"
region = "us-east-2"
}
Next, we’ll add the variable for the project. Here, we just have one variable that’s a list of names. Append this code to the main.tf
file.
// previous code
# usernames
variable "iam_user_names" {
description = "List containing IAM users names"
type = list(string)
default = ["Hai", "Oladipo", "Cindy"]
}
We need to fetch a few values from the AWS account. We need an AMI ID, the VPC ID, for the EC2 instance resource. Append this code to the main.tf
file.
// previous code
# get the AMI ID for the default provider
data "aws_ami" "ubuntu" {
most_recent = true
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
owners = ["099720109477"] # Canonical
}
# get the VPC subnet and security group
data "aws_vpc" "lab_vpc" {
filter {
name = "tag:Name"
values = ["Skillmix Lab"]
}
}
#
data "aws_subnet" "lab_subnet" {
filter {
name = "tag:Name"
values = ["Skillmix Lab Public Subnet (AZ1)"]
}
}
Code Review
The aws_ami
data source fetches the latest Ubuntu AMI of the jammy release.
The aws_vpc
data source gets the VPC resource. This VPC resource was created on your lab account.
The aws_subnet
resource retrieves one of the subnets in the VPC.
Next, let’s create an IAM User resource for each person in the variables list.
// previous code
# create an IAM user resource for each item in the list
resource "aws_iam_user" "boss_accounts" {
for_each = toset(var.iam_user_names)
name = each.key
}
Code Review
Notice here we are using the for_each
to iterate over the variables list. We are access the name in the list via the each.key
value.
Next, we will create a security group and an EC2 instance resource. The EC2 resource block one will deploy an instance for each person in the list.
We will also create an S3 ACL and bucket resource to demonstrate how to use the secondary provider.
// previous code
# create a security group
resource "aws_security_group" "web_instance_sg" {
name = "web-server-security-group"
description = "Allowing requests to the web servers"
vpc_id = data.aws_vpc.lab_vpc.id
tags = {
Name = "web-server-security-group"
}
}
# create an instance per person
resource "aws_instance" "people_servers" {
for_each = toset(var.iam_user_names)
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
vpc_security_group_ids = [aws_security_group.web_instance_sg.id]
subnet_id = data.aws_subnet.lab_subnet.id
# create these after the IAM users are created
depends_on = [
aws_iam_user.boss_accounts
]
tags = {
Name = "${each.key}"
}
}
# create s3 acl and bucket
resource "aws_s3_bucket_acl" "my_bucket_acl" {
bucket = aws_s3_bucket.my_bucket.id
acl = "private"
provider = aws.secondary
}
resource "aws_s3_bucket" "my_bucket" {
bucket = "<pick a unique name here>"
provider = aws.secondary
}
Code Review
The aws_instance.people_servers
block uses the for_each meta-argument to iterate over the variables list that we create. For each name in the list, it will create an EC2 instance. Here are some other things to note:
The instance Name
tag will be a combination of text and the user name.
The instances will be created after the IAM users as we set in depends_on
. Admittedly, this isn’t functionally needed, but it does demonstrate how the feature is used.
The S3 ACL and bucket will be created in the secondary AWS provider.
Now for the fun part. Let’s apply the changes and see what happens. At your terminal or command prompt, issue the following commands.
Make sure you’re in the right working directory
$ terraform init
...output
$ terraform plan
...output
$ terraform apply
...output