Terraform
Create dynamic expressions
The Terraform configuration language supports complex expressions to allow you to compute or generate values for your infrastructure configuration. Expressions can be simple string or integer values, or more complex values to make your configuration more dynamic.
In this tutorial, you will use expressions to configure and deploy EC2 instances and a load balancer. You will use conditionals to determine whether to provision multiple instances for high-availability and the splat
expression to return multiple private IPs in your network.
Prerequisites
You can complete this tutorial using the same workflow with either Terraform Community Edition or HCP Terraform. HCP Terraform is a platform that you can use to manage and execute your Terraform projects. It includes features like remote state and execution, structured plan output, workspace resource summaries, and more.
Select the HCP Terraform tab to complete this tutorial using HCP Terraform.
This tutorial assumes that you are familiar with the Terraform workflow. If you are new to Terraform, complete the Get Started tutorials first.
In order to complete this tutorial, you will need the following:
- Terraform v1.1+ installed locally.
- An AWS account with local credentials configured for use with Terraform.
Clone example repository
Clone the Learn Terraform Expressions repository, which contains configuration for AWS network components, an EC2 instance, and load balancer. You will modify the configuration of these resources using Terraform expressions.
$ git clone https://github.com/hashicorp/learn-terraform-expressions.git
Navigate to the repository in your terminal.
$ cd learn-terraform-expressions
Use a conditional expression
Conditional expressions select a value based on whether the expression evaluates to true or false.
In this configuration, you will use the locals
block to create a resource name based on a conditional value and capture that name in a map of resource tags.
Open your main.tf
file and paste in the following code snippet.
main.tf
resource "random_id" "id" {
byte_length = 8
}
locals {
name = (var.name != "" ? var.name : random_id.id.hex)
owner = var.team
common_tags = {
Owner = local.owner
Name = local.name
}
}
The syntax of a conditional expression first defines the condition, then the outcomes for true and false evaluations. In this example, if var.name
is not empty (!= ""
), local.name
is set to the var.name
value; otherwise, the name is the random_id
.
Condition | ? | true value | : | false value |
---|---|---|---|---|
If the name variable is NOT empty | then | Assign the var.name value to the local value | else | Assign random_id.id.hex value to the local value |
Next, update the network resources of your configuration and add the local.common_tags
expression to your tags
attribute.
Note
Use local values with caution. While reusing a local value simplifies your configuration, it can add complexity to your resource lifecycle.
main.tf
resource "aws_vpc" "my_vpc" {
cidr_block = var.cidr_vpc
enable_dns_support = true
enable_dns_hostnames = true
tags = local.common_tags
}
resource "aws_internet_gateway" "igw" {
vpc_id = aws_vpc.my_vpc.id
tags = local.common_tags
}
resource "aws_subnet" "subnet_public" {
vpc_id = aws_vpc.my_vpc.id
cidr_block = var.cidr_subnet
tags = local.common_tags
}
resource "aws_route_table" "rtb_public" {
vpc_id = aws_vpc.my_vpc.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.igw.id
}
tags = local.common_tags
}
Update your ELB resource with the tags.
main.tf
## ...
resource "aws_elb" "learn" {
## ...
instances = aws_instance.ubuntu.id
idle_timeout = 400
connection_draining = true
connection_draining_timeout = 400
tags = local.common_tags
}
Finally, update your aws_instance
resource.
main.tf
resource "aws_instance" "ubuntu" {
availability_zone = "us-east-1a"
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
associate_public_ip_address = true
subnet_id = aws_subnet.subnet_public.id
tags = local.common_tags
}
Save your changes.
Create a new file called outputs.tf
and add the values for your instance tags.
outputs.tf
output "tags" {
description = "Instance tags"
value = aws_instance.ubuntu.tags
}
Create infrastructure
Initialize this configuration.
$ terraform init
Initializing the backend...
Initializing provider plugins...
- Reusing previous version of hashicorp/aws from the dependency lock file
- Reusing previous version of hashicorp/random from the dependency lock file
- Installing hashicorp/aws v4.6.0...
- Installed hashicorp/aws v4.6.0 (signed by HashiCorp)
- Installing hashicorp/random v3.1.2...
- Installed hashicorp/random v3.1.2 (signed by HashiCorp)
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
Run terraform apply
. Respond yes
to the prompt to confirm the operation.
$ terraform apply
Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
+ create
Terraform will perform the following actions:
##...
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
##...
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
tags = tomap({
"Name" = "terraform"
"Owner" = "hashicorp"
})
Tip
This tutorial shows the output for Terraform commands run with the Terraform CLI. If you are following the HCP Terraform workflow, the output may differ slightly but the results will be the same.
If you use HCP Terraform to provision your resources, your workspace now displays the list of all of the resources it manages.
The Terraform outputs contain the formatted resource tags.
Create a conditional count criteria
Open variables.tf
and add a new boolean variable for high availability.
variables.tf
variable "high_availability" {
type = bool
description = "If this is a multiple instance deployment, choose `true` to deploy 3 instances"
default = true
}
Save this file.
Next, open main.tf
and update the aws_instance
resource to use the new
high_availability
variable.
First, update the count parameter with a conditional expression based on the
value of var.high_availability
. Then update the associate_public_ip_address
parameter so that only the first instance is assigned a public IP address.
Finally, merge the tags for the new instances.
main.tf
resource "aws_instance" "ubuntu" {
count = (var.high_availability == true ? 3 : 1)
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro"
associate_public_ip_address = (count.index == 0 ? true : false)
subnet_id = aws_subnet.subnet_public.id
tags = merge(local.common_tags)
}
Condition | ? | true value | : | false value |
---|---|---|---|---|
If var.high_availability is set to true | then | Create three aws_instance resources | else | Create one aws_instance resource |
If count.index is 0 | then | Assign public IP | else | Do not assign public IP |
Save your changes.
Use a splat
expression
The aws_instance
resource could now have a count value of 3
. To return the
private IP addresses of all of the instances, you will use a splat *
expression to create an output value.
The splat
expression captures all objects in a list that share an attribute. The special *
symbol iterates over all of the elements of a given list and returns information based on the shared attribute you define.
Without the splat expression, Terraform would not be able to output the entire array of your instances and would only return the first item in the array.
Create a splat expression
Edit the outputs.tf
file to add the new private_addresses
output. This output will return the private DNS of all instances created by the aws_instance.ubuntu
resource.
outputs.tf
output "private_addresses" {
description = "Private DNS for AWS instances"
value = aws_instance.ubuntu[*].private_dns
}
This expression mirrors capturing a specific element in an array. If you only wanted to return the third instance IP in the array of instances, you could do that by replacing the *
with 2
.
The current tags
output will error because there are multiple instances.
Replace the tags
output block with the following first_tags
output, which will return the first instance's tags.
outputs.tf
output "first_tags" {
description = "Instance tags for first instance"
value = aws_instance.ubuntu[0].tags
}
Open main.tf
to add the new instances to the ELB configuration.
main.tf
resource "aws_elb" "learn" {
##...
instances = aws_instance.ubuntu[*].id
idle_timeout = 400
connection_draining = true
connection_draining_timeout = 400
tags = local.common_tags
}
Save your changes.
Apply your changes
Run terraform apply
to provision the new instances and update your load balancer configuration. Respond yes
to the prompt to confirm the operation.
$ terraform apply
random_id.id: Refreshing state... [id=NfgSYKxrorA]
aws_vpc.my_vpc: Refreshing state... [id=vpc-06e45b1a591e2b96f]
aws_internet_gateway.igw: Refreshing state... [id=igw-05972512ea18f8eb0]
aws_subnet.subnet_public: Refreshing state... [id=subnet-0f0710e956d144ac3]
aws_route_table.rtb_public: Refreshing state... [id=rtb-002b2df72a5eee2ba]
aws_instance.ubuntu[0]: Refreshing state... [id=i-08ff118d23a9cddc4]
aws_route_table_association.rta_subnet_public: Refreshing state... [id=rtbassoc-0d692f2cd308ef864]
aws_elb.learn: Refreshing state... [id=Learn-ELB]
Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
+ create
~ update in-place
Terraform will perform the following actions:
##...
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
##...
Apply complete! Resources: 2 added, 1 changed, 0 destroyed.
Outputs:
first_tags = tomap({
"Name" = "terraform"
"Owner" = "hashicorp"
})
private_addresses = [
"ip-172-16-10-133.us-east-2.compute.internal",
"ip-172-16-10-244.us-east-2.compute.internal",
"ip-172-16-10-63.us-east-2.compute.internal",
]
Terraform's output now contains the entire array of private addresses for all three EC2 instances.
Clean up resources
After verifying that the resources were deployed successfully, run terraform destroy
to destroy them. Respond yes
to the confirmation prompt to confirm the action.
$ terraform destroy
random_id.id: Refreshing state... [id=NfgSYKxrorA]
aws_vpc.my_vpc: Refreshing state... [id=vpc-06e45b1a591e2b96f]
aws_internet_gateway.igw: Refreshing state... [id=igw-05972512ea18f8eb0]
aws_subnet.subnet_public: Refreshing state... [id=subnet-0f0710e956d144ac3]
aws_route_table.rtb_public: Refreshing state... [id=rtb-002b2df72a5eee2ba]
aws_instance.ubuntu[0]: Refreshing state... [id=i-08ff118d23a9cddc4]
aws_instance.ubuntu[1]: Refreshing state... [id=i-0f4ef22bcf35a2632]
aws_instance.ubuntu[2]: Refreshing state... [id=i-0566759e2fd7116d7]
aws_route_table_association.rta_subnet_public: Refreshing state... [id=rtbassoc-0d692f2cd308ef864]
aws_elb.learn: Refreshing state... [id=Learn-ELB]
Terraform used the selected providers to generate the following execution plan. Resource actions are
indicated with the following symbols:
- destroy
Terraform will perform the following actions:
##...
Do you really want to destroy all resources?
Terraform will destroy all your managed infrastructure, as shown above.
There is no undo. Only 'yes' will be accepted to confirm.
Enter a value: yes
##...
Destroy complete! Resources: 10 destroyed.
If you used HCP Terraform for this tutorial, after destroying your resources, delete the learn-terraform-expressions
workspace from your HCP Terraform organization.
Next steps
To learn more about how to create more complex, dynamic configuration, review the following resources:
- Learn how to use local values to simplify your configuration.
- Review how to use functions for computations in your configuration.
- Explore how to customize your configuration using input variables.