Terraform Syntax - Part 2
In part 2, we would cover some basics of meta-arguments, expressions, and functions. If you have not read part 1, it is highly recommended to read that part first before proceeding to read this since this part builds upon the example from part 1.
Before we proceed, let us first organize our code into multiple files. As a general practice, the Terraform codebase is divided into multiple files based on the providers, resources, and variables. Let us create 3 files as below:
variables.tf
- This file would contain all the declared input variables. In our example, we have input variables defined for region, ami, and type and output variable instance_id.provider.tf
- This file would contain declarations for providers being used. In our case, we have terraform, and the provider aws blocks.main.tf
- This file would contain the declarations for actual resources to be created.
Refer to this commit on Github repository.
By default, Terraform assumes all the code placed in a particular directory as part of the same configuration. So technically it doesn't make much of a difference if you put the code in a single file or divide it into multiple files and sub-directories. From the maintainability point of view, it makes a lot of sense to do so.
Meta-Arguments
Note: While working through the examples, please make sure to run “terraform destroy
” after every terraform apply
run.
Meta-arguments are special constructs provided for resources
. We have seen that resource blocks are the actual cloud resources that are created by Terraform. Often, it becomes tricky to declare resources in a way that satisfies certain requirements. Meta-arguments come in handy in situations like creating resources in the same cloud provider but in different regions
, or when we are creating multiple identical resources with different names, or when we have to declare implicit dependencies at places where Terraform is not able to identify the dependency itself.
There aren’t many but a few meta-arguments available currently. They are as follows:
Provider:
The provider meta-argument is used when we have multiple provider configurations in a given Terraform config. Terraform automatically maps the given resource to the default provider identified by the resource’s identifier. For example, the default provider for “aws_instance
” is “aws
”. This aws
provider is currently configured to deploy a resource in a particular region. However, if we would want to have another aws
provider for another region, or with a different configuration setting, we can write another provider block.
Even though it is possible to write multiple provider configs, Terraform by default would pick the same provider for aws for creating resources. This is where aliases come into the picture. Every provider configuration can be tagged with an alias and the value of this alias is used in our provider meta-argument in the resource block to specify different provider configurations for identical resources.
In the given example, let us duplicate the aws provider and give them appropriate aliases. Modified providers with an alias
should look like below in provider.tf
file.
provider "aws" {
alias = "aws_west"
region = var.region_west
}
provider "aws" {
alias = "aws_east"
region = var.region_east
}
Notice that, we have also modified variables for the region to represent 2 different regions - west and east. Do the corresponding changes to variables.tf
file as below:
variable "region_west" {
default = "us-west-1"
description = "AWS West Region"
}
variable "region_east" {
default = "us-east-1"
description = "AWS East Region"
}
One final change that we need to do is in the main.tf
file. Where we can now use provider meta-argument to specify a specific provider alias. We can mention the desired provider config by specifying <provider>.<alias>
in the meta-argument. Refer to the modified main.tf
file below:
resource "aws_instance" "demo" {
provider = aws.aws_west
ami = var.ami
instance_type = var.type
tags = {
name = "Demo System"
}
}
Validate the final configuration by running terraform validate
, and it should say “Success!
”
Lifecycle
The lifecycle
meta-argument specifies the settings related to the lifecycle
of resources managed by Terraform. By default, whenever a configuration is changed and applied, Terraform operates in the sequence below.
Create new resources.
Destroy those resources which do not exist in config anymore.
Update those resources which can be updated without destruction.
Destroy and re-create change resources that cannot be changed on the fly.
A lifecycle
meta-argument can be used if we would like to alter this default behavior. These meta-arguments are used in resource blocks similar to provider meta-argument. There are 3 lifecycle meta-argument settings:
create_before_destroy
: Used when we want to avoid accidental loss of infrastructure when a changed config is applied. This setting when set to true, Terraform will first create the new resource before destroying the older resource.prevent_destroy
: When set to true, any attempt to destroy this in the config would result in an error. This is often useful in the case of those resources where reproduction can prove to be expensive.ignore_changes
: This is a list typed meta-argument which specifies the attributes of a specific resource in the form of a list. During the update operations, often there is a situation where we would like to prevent changes caused by external factors. In those cases, it becomes essential to declare the list of attributes that should not be changed without being reviewed.
lifecycle
meta-arguments come in very handy when we are in the process of setting up complex infrastructure. By altering the default behavior of Terraform, we can put some protection in the form of lifecycle
meta-arguments for confirmed and finalized resource blocks. In our example, we would not use any lifecycle meta-argument.
Depends_on
Generally, Terraform is aware of dependencies while performing the creation or modification of resources and takes care of the sequence by itself. However, in certain cases Terraform cannot deduce the implicit dependencies and just moves on creating the resources parallelly if it doesn’t see any dependency.
Let us take, for example, a Terraform configuration for 2 EC2 instances enclosed in a VPC. When this configuration is applied, Terraform automatically knows that the creation of VPC should be done before spinning the EC2 instances. This is general knowledge and Terraform knows it very well. In situations where dependencies are not so obvious, the depends_on
meta-argument comes to the rescue. It is a list type of argument that takes in the list of resource identifiers declared in the configuration.
Count
Imagine a situation where you would like to create multiple similar resources. By default, Terraform creates one real resource for a single resource block. But in the case of multiple resources, Terraform provides a meta-argument named count. As the name suggests, the count can be assigned with a whole number, to represent multiple resources.
In our example, let us create 3 similar EC2 instances. Into your main.tf
file, add an attribute count to the resource aws_instance.demo
, and assign it with a value of 3. It should look something like the below.
resource "aws_instance" "demo" {
count = 3
provider = aws.aws_west
ami = var.ami
instance_type = var.type
tags = {
name = "Demo System"
}
}
By doing this, we let Terraform know that we need to create 3 EC2 instances with the same configuration. Save the file and execute terraform validate
. It throws an error saying “Missing resource instance key
”. Remember in our variables.tf
file we have mentioned an output variable to output the id
of the created resource. Since we have asked Terraform to create 3 instances, it is not very clear - ID of which of the 3 instances should be printed?
To get around this problem, we would use a special expression called “splat
” expression. The ideal case here would be to run a for loop over the instance set and print out the ID property. Splat expression is a better way to do the same task with lesser lines of code. All you need to do is - in the variables.tf
file, replace the output value code to below:
output "instance_id" {
value = aws_instance.demo[*].id
}
Save this file and run terraform validate
to see if everything is okay. Once successful, go ahead and run terraform plan
and apply
and check your AWS management console in us-west-1 region a.k.a aws_west
. Let me know the IDs too.
Splat expression is one of its kind and we would take a better look at expressions in upcoming sections.
for_each
for_each
, as the name suggests, is essentially a “for each” loop. for_each
meta-argument is used to create multiple similar cloud resources. Yes, it does sound similar to count meta-argument but there is a difference.
Firstly, for_each
and count
cannot be used together.
Secondly, you can say this is an enhanced version of the count
. Count meta-argument is a number type. Terraform simply creates those many resources. However, if you would like to create these resources with some customizations in the output, or if you already have an object of type map or list based on which you want to create resources, then for_each
meta-argument is the way to go.
As mentioned earlier, for_each
can be assigned a map and list type of values. A map is a collection of key-value pairs, whereas a list is a collection of values (in this case string values).
for_each
comes with a special object “each”. This is the iterator in the loop which can be used to refer to the key
or value
, or only key in case of list. Let us take a look at our example. We would like to create EC2 instances for the given map. The map is assigned to for_each
meta-argument and Terraform creates an EC2 instance for each key-value pair in the map. Lastly, we use the key
and value
information using each
object to set the name attribute in the tag
.
The resource block in main.tf
now looks something like this.
resource "aws_instance" "demo" {
for_each = {
fruit = "apple"
vehicle = "car"
continent = "Europe"
}
provider = aws.aws_west
ami = var.ami
instance_type = var.type
tags = {
name = "${each.key}: ${each.value}"
}
}
Execute terraform validate
and observe the output. It throws an error for the output variable - “This object does not have an attribute named id
”. A quick note here - splat
expressions work for the list type of variables. Since we have used a map while setting our for_each
meta-argument, we need to change the return value expression for each, as below:
output "instance_id" {
//value = aws_instance.demo[*].id
value = [for b in aws_instance.demo : b.id]
}
Execute terraform validate
again, if successful, go ahead and apply
the configuration. Check the AWS management console for the machines created and the names assigned to them.
Expressions
Expressions are ways to make the Terraform code dynamic. Expressions come in 2 forms, simple and complex. Till now in our examples, we have mostly dealt with simple expressions. A simple expression is any argument used as part of some block. Writing down an argument with a primitive value assigned to is a form of expression.
We have made use of a complex expression called splat ( *
) in our example while working with meta-arguments. However, there are even more complex expressions that can be used to make the Terraform code more dynamic, readable, and flexible. There are various types of expressions that you can take a look at in the Terraform documentation.
Functions
Terraform has built-in functions that can be used with expressions. These are utility functions that are useful in number and string manipulations. There are functions to work with file systems, date and time, network, type conversion, etc.
Functions along with expressions make it super easy to write a really dynamic IaC. You can refer to the list of functions here.
This brings us to the end of Terraform Syntax - Part 2. Next, we would take a look at Terraform CLI.