Meta Arguments
In Terraform, a meta-argument is a special kind of argument that can be used to customize the
behavior
of a resource block, Module block, and Data block.
Meta-arguments are the way to give you more control over how Terraform creates, updates, and destroys your infrastructure. Using them, you will be able to build complex and flexible configurations of the infrastructure.
Types of Meta Arguments Resource
Terraform provides several meta-arguments that can be used by all resource type. Here are the metaarguments used:
- depends_on
- count
- for_each
- lifecycle
- provider
depends_on Meta-Argument
depends_on is a metaargument in Terraform that explicitly specifies dependencies of resources. It is used where Terraform cannot, by itself, automatically infer the dependency between resources.
Why do I need depends_on?
Terraform does an outstanding job automatically inferring dependency relationships between resources via their access of each other's data. That is to say: if resource B uses resource A's output to configure itself, terraform will automatically create A before B.
However, there are scenarios when one resource depends on the behaviour of another resource, where it does not use any data of that other resource in its configuration. In such cases, Terraform can't automatically compute the dependency between the two resources. That's where depends_on comes in place.
When would I use depends_on?
Now, suppose you have two resources, A and B. Resource B depends on the behaviour of resource A, but it doesn't, in fact, use any of the data or output from resource A in its configuration. That is to say, B requires A to have been created, or otherwise configured, prior to its own creation/configuration, but it does not reference A's data in any way.
In such a case, you will want to use depends_on to tell Terraform that B depends on A's behavior. This says, in effect, even though B did not use any data from A, Terraform needs to make sure A is created before B is created.
Example
You'll have two resources: aws_security_group, Resource A is "my_sg", creating a security group and aws_instance, Resource B named "my_server" that would create an EC2 instance and attach to it a security group created by Resource A.
resource "aws_security_group" "my_sg" {
name = "Example Security Group"
vpc_id = aws_vpc.example.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "example_ec2_instance" {
ami = "ami-0123456789"
instance_type = "t2.micro"
tags = {
Name = "my-ec2-instance"
}
depends_on = [aws_security_group.my_sg]
}
In this case, Resource B depends on the behavior of Resource A - the EC2 instance needs to know about the security group at its time of creation. However, the configuration for the EC2 instance does not use any output of the resource for the security group, such as its ID or ARN. In order for Terraform to create the security group before creating the EC2 instance, you would use depends_on to specify that Resource B depends on Resource A's behavior:
The Count Meta-Argument
In Terraform, when you define a resource block, it usually configures only one infrastructure object. Sometimes you need to manage multiples of similar objects, say, for instance, a fixed pool of compute instances, without writing separate blocks for each.
For this purpose, Terraform provides two meta-arguments:
count
and for_each
. In this explanation, we will explain the count meta-argument.
How it works - Count?
Adding a count argument to a resource informs Terraform to create several instances of this resource. The value of count must be a
whole number
and indicates how many instances to create. Each instance has an associated infrastructure object, and each is created, updated or destroyed separately as this configuration is applied.
Here is an example using count on an AWS EC2 instance resource:
resource "aws_instance" "example_ec2_instance" { count = 4 ami = "ami-0123456789" instance_type = "t2.micro" }
Here, Terraform is going to create four identical EC2 instances with the same AMI and instance type. Each of them will have an infrastructure object associated with a distinct one, and each will separately be created, updated, or destroyed upon application of the configuration.
The count Object
When using the count meta-argument with a resource, Terraform also exposes an additional object,
count
, in expressions. This object can be used to customize the configuration of each instance that the count meta-argument creates.
The count object has only one attribute:
count.index
: This attribute returns the distinct index number starting with 0 for each instance that the count meta-argument creates.
For instance, consider a resource block using the count meta-argument to create three instances of the compute instance.
resource "aws_instance" "server" { count = 3 ami = "ami-0123456789" instance_type = "t2.micro" tags = { Name = "Ec2-${count.index}" } }
In this example, the count.index attribute would have the values 0, 1 and 2 for each of the three instances that is to be created. The resultant instance tag would be "Ec2-0", "Ec2-1", and "Ec2-2".
Referencing Instances of count
In Terraform, the count argument is used to create a resource multiple times. Terraform identifies multiple resources by assigning an index number for each instance, relying on the starting index 0.
Resources can be referred in Terraform in the following two ways:
- Entire Resource Block: To refer to the entire resource block, it should be in the format
<TYPE>.<NAME>
. For instance, aws_instance.server refers to the whole resource block.
- Individual Instances: An individual instance is referenced by adding the index number in square brackets. Syntax is
<TYPE>.<NAME>[<INDEX>]
. For example, aws_instance.server[0] refers to the first instance, while aws_instance.server[1] refers to the second, and so on.
The for_each Meta-Argument
The for_each metaargument is a way to create multiple instances of a resource block according to a collect of values. If a resource block includes a for_each argument whose value should be either a map or a set of strings, then Terraform will create an instance for each member of that map or set.
Each instance is handled independently, with its own infrastructural object associated with it. This means each instance will be independently created, updated, or destroyed based on the changes you've made when applying your Terraform configuration.
NOTE: A given resource block can't use the
count
and for_each
at the same time.
Map Example
In the following example, we will use the for_each meta-argument with a map to provision multiples AWS S3 buckets. We will then define a collected set of buckets operating under different names and regions, and Terraform will simply create the buckets for each of them.
resource "aws_s3_bucket" "buckets" { bucket = each.key region = each.value for_each = tomap ({ "my-bucket-1" = "us-east-1" "my-bucket-2" = "us-west-2" "my-bucket-3" = "eu-east-1" }) }
The tomap() here is a Terraform function that converts a list of key-value pairs into a map. The for_each meta-argument will step through the map, creating one instance of the aws_s3_bucket resource for each key-value pair. The
each.key
and each.value
expressions are used to access the key and value of each pair, respectively.
Set Example
Here is an example where for_each is used on a set of strings to create many AWS RDS:
resource "aws_db_instance" "database" { for_each = toset ([ "database-1", "database-2", "database-3" )] allocated_storage = each.key engine = "mysql" instance_class = "db.t2.micro" db_name = each.key username = "myuser" password = "mypassword" }
Here, toset() is a Terraform function that takes a list of strings in and returns a set. The for_each meta-argument will iterate over that set and create one instance of the aws_db_instance resource for every string in that set. Finally, the
each.key
expression is used to access the string value of each item of that set using the db_names variable as the database name.
The each Object
When you use the for_each inside of a Terraform block, you get this special object called
each
inside that block. So, it can be used to modify the configuration of each instance.
Each object has only two attribute:
each.key
: The identifier of each entity within the collection. If you are working with a map, that is a collection of key-value pairs, then each.key will return the key in the pair. If you're working with a set, meaning a collection of just unique values, then each.key yields the actual value.
each.value
: This is the value associated with your key in the map. In case of a set, each.value is same as each.key as sets do not have key-value pairs.
Following is an example using AWS EC2:
resource "aws_instance" "instance" { for_each = tomap ({ "dev" = "t2.micro" "prod" = "t2.micro" }) ami = each.key instance_type = each.value tags = { Name = "Instance ${each.key} with type ${each.value}" } }
In the above example, Terraform will create two instances of the aws_instance resource: one for "dev" and one for "prod". Within this block, the each object is available, whose properties can be used to configure each instance individually.
In the "dev" instance, each.key would be "dev" and each.value would be "t2.micro". Thus, the instance_type attribute is to be set to "t2.micro", and the tags.Name is to be "Instance dev with type t2.micro". Wherein, for an instance "prod", each.key will be "prod" and each.value will be "t2.large". So, instance_type is set to "t2.large" and tags.Name will set to "Instance prod with type t2.large".
Referencing Resource Blocks and Instances of for_each
When you attach a for_each to a Terraform block, Terraform will make several repetitions of that resource. In order to distinguish between them, Terraform uses the map key (or set of members) from the value provided to for_each.
There are two ways to reference resources in Terraform:
- Entire Resource Block: If you want to refer to the block itself, you can use the following syntax
<TYPE>.<NAME>
. Example - aws_s3_bucket.bucket will refer to the whole resource block.
- Individual Instances: In order to address individual Instances, you need to add the key of the map or the set member respectively to the reference. The syntax is the following:
<TYPE>.<NAME>[<KEY>]
. Example: aws_ami.ami["ubuntu"] refers to the first instance and so forth.
Chaining for_each Between Resources
The for_each meta-argument is a powerful feature in Terraform. It allows one to create resources multiple times, based on a map or set of values. When using resource, the for_each returns a map of objects where each key represents an individual instance and the respective value represents the attributes of that instance.
You could establish one-to-one relationships among resources by using the output from one for_each block as the input for another for_each block. This approach is referred to as "Chaining for_each between resources."
Example: Creating EC2 Instances and Elastic IP Addresses
Consider a scenario in which we need to create multiple EC2 instances and assign an Elastic IP address uniquely to each of the created instances. One can use for_each for creating EC2 instances, then make the output of that block as an input for another for_each block creating Elastic IP addresses.
resource "aws_instance" "instance" { for_each = tomap ({ "t2.micro" = { "ami" = "ami-abc123" "instance_type" = "t2-micro" }, "t2.small" = { "ami" = "ami-def456" "instance_type" = "t2-small" } }) ami = each.value.ami instance_type = each.value.instance_type tags = { Name = "Instance with type ${each.key}" } }
The above code creates a map of objects that contain two keys: "t2.micro" and "t2.small". Each of these keys here represents an individual EC2 instance, and the corresponding value represents the attributes of that instance.
The output of the above block is a map of objects, which looks like this:
{
"t2.micro" = {
"arn" = "arn:aws:ec2:ap-southeast-2:accountid:instance/i-id"
"id" = "i-04f36f59dc445df86115ec"
"availability_zone" = "ap-southeast-2a"
"cpu_core_count" = 1
"cpu_threads_per_core" = 2
"ebs_block_device" = []
"ami" = "ami-abc123"
"instance_type" = "t2.micro"
},
"t2.small" = {
"arn" = "arn:aws:ec2:us-east-1:accountid:instance/i-id"
"id" = "i-8738673ddv3t381h63y765"
"availability_zone" = "ap-southeast-2b"
"cpu_core_count" = 1
"cpu_threads_per_core" = 2
"ebs_block_device" = []
"ami" = "ami-def456"
"instance_type" = "t2.small"
}
}
Now we can use this output to be the input for another for_each block that creates the Elastic IP addresses.
resource "aws_instance" "instance" { for_each = aws_instance.instance instance_type = each.value.id }
Here, the for_each block is getting its input from the output of the previous block where each value ID refers to the IDs of each EC2 instance created by the previously block, and an AWS EIP resource will create an Elastic IP address for each one.
Limitations on values used in for_each
Values to be known in advance
When using for_each with a map or set of values, the keys of the map, or all the values in case of a set of strings, must be known values. That is, values present in for_each can't be empty during the Terraform configuration apply phase. If you try to use unknown values, you get an error message indicating that for_each has dependencies that cannot be determined before apply.
Sensitive Values Not Allowed
Sensitive input variables, sensitive outputs, sensitive resource attributes are particularly sensitive values that cannot be used as an argument for for_each. The value used in for_each is to identify the resource instance and will always be disclosed in UI output during plan which can potentially reveal sensitive information. Using sensitive values as an argument to for_each will result in an error.
The provider Meta-Argument
By default, Terraform will automatically select a provider configuration based on the name of the resource type. For instance, if you declare the aws_instance resource, it will automatically select the default AWS provider configuration. However, in some cases, you may wish to use a different provider configuration.
The provider meta-argument is available to set explicitly which provider configuration is used for a resource. It takes the form of adding the provider name and alias (when using multiple configurations of the one provider type) to the resource declaration.
Provider Syntax
The syntax for using the provider meta-argument is:
<PROVIDER>.<ALIAS>
<PROVIDER>
: This is the name of provider such as "google", "aws", or "azure".
<ALIAS>
: is the name of the configuration or alias for this provider. This is an optional part of this syntax. It is utilized to differentiate between configurations for the same provider, such as us-east1, europe, staging, and so on.
Example
Suppose you have two configurations of provider for AWS Cloud:
- First, there is the default AWS configuration, with its region set to "us-east-1".
- The other configuration would be an alternative to AWS with an alias named "europeregion" and a region set to "eu-west-1".
provider "aws" { region = "us-east-1" } provider "aws" { alias = "europeregion" region = "eu-west-1" }
Now you want to create an AWS instance, using the alternative "europeregion" configuration and not the default configuration. You can achieve that by adding the provider meta-argument to the aws_instance resource declaration:
resource "aws_instance" "example_ec2_instance" { provider = aws.europeregion ami = "ami-0123456789" instance_type = "t2.micro" tags = { Name = "my-ec2-instance" } }
Here, the provider meta-argument has been used to specify that the aws_instance resource should use this "europeregion" configuration instead of using the default configuration. In other words, the EC2 instance will be created in the "eu-west-1" region as defined in "europeregion".
The Lifecycle Meta-Argument
In Terraform, a lifecycle block is a nested block that can only be used inside a resource block. This block allows you to customize the behavior of a resource during its lifecycle.
There are three phases in a resource's lifecycle:
create
, update
, and destroy
. The Lifecycle block allows you to tell Terraform how to handle resources in each of these three phases.
NOTE: When using Terraform, one should remember that the
Lifecycle block
can only be used within Resource Blocks
.
Lifecycle arguments
When using a lifecycle block in a resource block with Terraform, one is able to specify several arguments to control what happens during the lifecycle of that resource. The available arguments inside a lifecycle block are :
1. create_before_destroy
By default, if Terraform needs to do an update for a resource argument which cannot be updated in-place due to some limitation of the remote API, it follows a behavior termed
destroy-and-recreate
where it deletes the existing resource object and then creates a new resource object with the updated configured arguments.
The create_before_destroy meta-argument reverses this default behavior. If set to true, Terraform will first create a new resource object with the updated configured arguments. If the new object is created, then Terraform destroys the old resource object. Such a behavior allows the new object to be created well before the old one is destroyed, hence useful in certain situations where such downtime must be minimized, or where the resource object must always remain available.
Example
resource "aws_instance" "example_ec2_instance" { ami = "ami-0123456789" instance_type = "t2.micro" lifecycle = { create_before_destroy = true } }
In this case, if either the ami or instance_type arguments were changed, Terraform would create a new AWS instance with the new arguments before destroying the old one. This would avoid the old instance being destroyed before a new one was provided, which could cause potential downtime or other problems.
When you set create_before_destroy on a resource, Terraform will automatically cascade the same behavior to all resources depending on it, and it records that within the statefile. For instance, consider having two resources Resource A and Resource B where Resource A depends on Resource B. You set the create_before_destroy on Resource A to True but do not on Resource B. Terraform will automatically enable create_before_destroy on Resource B even though you never set it explicitly. It is because Resource A depends on Resource B and you can't override the create_before_destroy to false on Resource B.
resource "aws_instance" "instance_a" { ami = "ami-0123456789" instance_type = "t2.small" lifecycle = { create_before_destroy = true } depends_on = [aws_instance.instance_b] } resource "aws_instance" "instance_b" { ami = "ami-7836726816" instance_type = "t2.small" }
NOTE: When
create_before_destroy
is true, Terraform will skip running the destroy provisioners for resource.
2. prevent_destroy
The prevent_destroy meta-argument of Terraform is used to avoid the accidental destruction of infrastructure objects, which may be expensive or hard to reproduce, for example, database instances.
Setting the
prevent_destroy=true
on a Terraform resource provides an instruction to Terraform to reject any plan that would eventually destroy the object of infrastructure associated with it. Therefore, if a user tries to run terraform apply or terraform destroy that would delete such an object, Terraform stops the operation and returns an error message.
However, if you remove the complete resource block from your Terraform configuration then the prevent_destroy setting is removed too and Terraform will no longer know about the protection it will permit the destroy of the remote object to go.
Example: Protecting a Database Instance with prevent_destroy
For example, let's say we have a critical database instance that holds very sensitive data regarding our application. We want to make certain it is never deleted accidentally, since that would incur very serious data loss and downtime.
One of the attributes that can be set is the Terraform prevent_destroy meta-argument. A sample for that configuration is as follows.
resource "aws_db_instance" "database" { engine = "mysql" instance_class = "db.t2.micro" db_name = "testing-database" username = "myuser" password = "mypassword" allocated_storage = 10 lifecycle = { prevent_destroy = true } }
Here we'll create an AWS database instance with the aws_db_instance resource. On the resource, we have one meta-argument, prevent_destroy, which is set to true. That would have Terraform refuse to apply a plan if it includes the destruction of this database instance.
3. ignore_changes
By default, Terraform will compare the statefile (remote state) with your configuration files (desired state). When it finds any difference, it creates a plan to update the remote infrastructure object to reflect your configuration. In other words, it will keep your infrastructure in sync with your desired state.
But in some cases, you will create a resource with references to data that can change in the future but should not change the resource itself. For example, say you have a virtual machine that references a dynamic IP address that periodically changes. In this case, each time you run terraform apply, Terraform would detect that the IP address attribute has changed compared to state file, and plan to update the virtual machine - even though this is a change to an ignorable attribute that doesn't impact the configuration of the resource itself.
The purpose of the ignore_changes meta-argument is to deal with this problem. It takes a list of resource attributes that Terraform should always ignore during terraform plan operations for the remote object associated with this resource. In other words, changes to these attributes won't be detected and Terraform won't plan updates to these resources unnecessarily.
Referencing Attributes
The ignore_changes argument value must be a List of attribute references. When referencing attributes, you can use its relative address within the resource. For example, if you have a tags attribute that is a map, you can reference a specific tag using tags["Name"]. If you have a list attribute, then you can reference a specific element using list[0].
Example: Ignoring changes to tags and ami on an AWS Instance
resource "aws_instance" "ignore_changes_instance" { ami = "ami-0123456789" instance_type = "t2.large" tags = { env = "production" } lifecycle = { ignore_changes = [ tags["env"] ami ] } }
In the example above, Terraform will neglect the changes of env tag and ami attribute. Thus, in the case of modification of env tag, or value of ami attribute, Terraform won't detect them and won't plan update of instance.
Using all to ignore all attributes
Rather than listing attributes to ignore, the special keyword
all
can be used to direct Terraform to ignore all resource attributes. So terraform will create and delete the object on the remote system, but will never update it.
resource "aws_instance" "ignore_all_instance" { ami = "ami-0123456789" instance_type = "t2.large" tags = { env = "production" } lifecycle = { ignore_changes = [ all ] } }
The above example tells terraform to ignore all of the attributes of the aws_instance resource including ami, instance_type, tags, and any others which may be defined.
Limitations with ignore_changes
- You can only use ignore_changes to ignore attributes that are explicitly defined in the resource's definition. You can't use it to ignore attributes that don't take part of the resource's definition, such as read-only attributes generated by a provider.
- You can't use ignore_changes to ignore changes on the ignore_changes meta-argument itself, or other meta-arguments like
depends_on
,count
etc. These meta-arguments give you a way to control Terraform's behavior. Ignoring changes on these may lead to unexpected results.
4. replace_triggered_by
replace_triggered_by argument in Terraform allows users to instruct the Terraform to replace the resource whenever any of the referenced items change. Any of the listed referenced items in this replace_triggered_by argument get updated, then Terraform will destroy the existing resource that has this replace_triggered_by argument and will create a new one.
The value of the this argument must be a List of resource or attribute references.
Types of referenced items
There are three types of referenced items to be used in this argument:
- Resources: You can also reference other resources as attribute in replace_triggered_by. Example:
resource "aws_instance" "machine" {
ami = "ami-0123456789"
instance_type = "t2.large"
}
resource "aws_s3_bucket" "bucket" {
bucket = "my-bucket"
acl = "private"
lifecycle {
replace_triggered_by = [aws_instance.machine]
}
}
In the example, the replace_triggered_by attribute notifies changes of the aws_instance resource to the aws_s3_bucket resource. Therefore, in case any of the attributes from the aws_instance resource are updated, it will destroy and re-create the aws_s3_bucket resource.
- Instances of resources: You can refer resources instances when we used count or for_each. Example:
resource "aws_instance" "machine" {
count = 2
ami = "ami-0123456789"
instance_type = "t2.large"
}
resource "aws_s3_bucket" "bucket" {
bucket = "my-bucket"
acl = "private"
lifecycle {
replace_triggered_by = [aws_instance.machine[0]]
}
}
In the above example, the aws_s3_bucket resource has a replace_triggered_by attribute referring to the first instance of the aws_instance resource, i.e., aws_instance.machine[0]. This implies that when the first instance of the aws_instance resource is replaced, the aws_s3_bucket resource will be replaced, too.
- Attributes of resources: You may refer to resources specific attributes. An example is as follows:
resource "aws_instance" "machine" {
count = 2
ami = "ami-0123456789"
instance_type = "t2.large"
}
resource "aws_s3_bucket" "bucket" {
bucket = "my-bucket"
acl = "private"
lifecycle {
replace_triggered_by = [aws_instance.machine.ami]
}
}
In this example, the aws_s3_bucket resource example has a replace_triggered_by attribute that references the ami attribute of the aws_instance resource example. This means that if the ami attribute of the aws_instance resource is updated, the aws_s3_bucket resource will also be updated.
Related Pages
Feedback
Was this page helpful?