Meta-Arguments in Terraform

Meta-Arguments in Terraform

Day 69 of #90DaysOfDevOps

When you define a resource block in Terraform, by default, this specifies one resource that will be created. To manage several of the same resources, you can use either count or for_each, which removes the need to write a separate block of code for each one. Using these options reduces overhead and makes your code neater.

Count is what is known as a ‘meta-argument’ defined by the Terraform language. Meta-arguments help achieve certain requirements within the resource block.

Count

The count meta-argument accepts a whole number and creates the number of instances of the resource specified.

When each instance is created, it has its own distinct infrastructure object associated with it, so each can be managed separately. When the configuration is applied, each object can be created, destroyed, or updated as appropriate.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.16"
    }
  }
  required_version = ">= 1.2.0"
}
provider "aws" {
  region = "us-east-1"
}
resource "aws_instance" "server_for_count" {
  count         = 4
  ami           = "ami-0005e0cfe09cc9050"
  instance_type = "t2.micro"
  tags = {
    Name = "Server ${count.index}"
  }
}

Create a file with .tf extension and paste the above code. Next, write command terraform init and then terraform apply

Check the console, 4 instances will be created with the 0 to 3 (total 4) name.

If your instances are almost identical, count is appropriate. If some of their arguments need distinct values that can't be directly derived from an integer, it's safer to use for_each.

for_each

Like the count argument, the for_each meta-argument creates multiple instances of a module or resource block.

However, instead of specifying the number of resources, the for_each meta-argument accepts a map or a set of strings.

This is useful when multiple resources are required that have different values.

Example with a set of strings:

locals {
  ami_ids = toset([
    "ami-0005e0cfe09cc9050",
    "ami-0c7217cdde317cfec",
  ])
}

resource "aws_instance" "server" {
  for_each      = local.ami_ids
  ami           = each.key
  instance_type = "t2.micro"
  tags = {
    Name = "Server ${each.key}"
  }
}

Write command terraform apply

Check console for output

Example with multiple key value iteration:

locals {
  ami_ids = {
    "linux" : "ami-0005e0cfe09cc9050",
    "ubuntu" : "ami-0c7217cdde317cfec",
  }
}

resource "aws_instance" "server" {
  for_each      = local.ami_ids
  ami           = each.value
  instance_type = "t2.micro"
  tags = {
    Name = "Server ${each.key}"
  }
}

Write command terraform apply

Check console for output.

Task 2

Write about meta-arguments and its use in Terraform.

Meta-arguments in Terraform are special arguments that provide additional functionality and control over resource configuration. They can be used with resource blocks and modules to control their behavior or influence the infrastructure provisioning process.

Meta-arguments are not specific to any particular resource type, but rather provide a way to configure behavior across all resources in a Terraform configuration. They are used to define how resources are managed and interacted with in a broader context.

There are 5 meta-arguments available in terraform:

  • depends_on

  • count

  • for_each

  • provider

  • lifecycle

depends_on:

Use the depends_on meta-argument to handle hidden resource or module dependencies that Terraform cannot automatically infer. You only need to explicitly specify a dependency when a resource or module relies on another resource's behavior but does not access any of that resource's data in its arguments.

You can use the depends_on meta-argument in module blocks and in all resource blocks, regardless of resource type. It requires a list of references to other resources or child modules in the same calling module. This list cannot include arbitrary expressions because the depends_on value must be known before Terraform knows resource relationships and thus before it can safely evaluate expressions.

example:

resource "aws_iam_role" "example" {
  name = "example"

  # assume_role_policy is omitted for brevity in this example. Refer to the
  # documentation for aws_iam_role for a complete example.
  assume_role_policy = "..."
}

resource "aws_iam_instance_profile" "example" {
  # Because this expression refers to the role, Terraform can infer
  # automatically that the role must be created first.
  role = aws_iam_role.example.name
}

resource "aws_iam_role_policy" "example" {
  name   = "example"
  role   = aws_iam_role.example.name
  policy = jsonencode({
    "Statement" = [{
      # This policy allows software running on the EC2 instance to
      # access the S3 API.
      "Action" = "s3:*",
      "Effect" = "Allow",
    }],
  })
}

resource "aws_instance" "example" {
  ami           = "ami-a1b2c3d4"
  instance_type = "t2.micro"

  # Terraform can infer from this that the instance profile must
  # be created before the EC2 instance.
  iam_instance_profile = aws_iam_instance_profile.example

  # However, if software running in this EC2 instance needs access
  # to the S3 API in order to boot properly, there is also a "hidden"
  # dependency on the aws_iam_role_policy that Terraform cannot
  # automatically infer, so it must be declared explicitly:
  depends_on = [
    aws_iam_role_policy.example
  ]
}

provider:

The provider meta-argument specifies which provider configuration to use for a resource, overriding Terraform's default behavior of selecting one based on the resource type name. Its value should be an unquoted <PROVIDER>.<ALIAS> reference.

By default, Terraform interprets the initial word in the resource type name (separated by underscores) as the local name of a provider, and uses that provider's default configuration. For example, the resource type google_compute_instance is associated automatically with the default configuration for the provider named google.

By using the provider meta-argument, you can select an alternate provider configuration for a resource:

# default configuration
provider "google" {
  region = "us-central1"
}

# alternate configuration, whose alias is "europe"
provider "google" {
  alias  = "europe"
  region = "europe-west1"
}

resource "google_compute_instance" "example" {
  # This "provider" meta-argument selects the google provider
  # configuration whose alias is "europe", rather than the
  # default configuration.
  provider = google.europe

  # ...
}

lifecycle:

lifecycle is a nested block that can appear within a resource block. The lifecycle block and its contents are meta-arguments, available for all resource blocks regardless of type.

The arguments available within a lifecycle block are create_before_destroy, prevent_destroy, ignore_changes, and replace_triggered_by.

resource "azurerm_resource_group" "example" {
  # ...

  lifecycle {
    create_before_destroy = true
  }
}