Terraform: Learning Variables, Loops, Conditions, and Resource Creation

ESHNITHIN YADAVESHNITHIN YADAV
9 min read

This article dives into essential Terraform concepts—variables, variable precedence, conditions, count-based loops, and for_each loops—and shows how to use them to create EC2 instances, security groups, and Route 53 records. Each section explains the concept in simple terms, why it’s needed, and provides a practical example with file structures. The examples are inspired by real-world use cases, such as provisioning multiple EC2 instances with associated DNS records.


1. Terraform Variables

What are Variables?

Variables in Terraform allow you to parameterize your configurations, making them reusable and flexible. Instead of hardcoding values (e.g., AMI IDs, instance types), you define variables that can be set dynamically through defaults, input files, or command-line arguments.

Why Use Variables?

  • Reusability: Define a configuration once and reuse it across environments (e.g., dev, prod) by changing variable values.

  • Maintainability: Centralize values like AMI IDs or tags in one place, making updates easier.

  • Flexibility: Allow users to customize infrastructure without modifying the core code.

  • Readability: Make configurations clearer by using descriptive variable names.

How Variables Work

Variables are defined in a variables.tf file (or any .tf file) using the variable block. Each variable can have:

  • type: Specifies the data type (e.g., string, number, list, map).

  • default: An optional default value.

  • description: A note explaining the variable’s purpose.

Example: Defining Variables

Here’s a variables.tf file defining variables for EC2 instances, security groups, and Route 53 records.

# variables.tf
variable "ami_id" {
  type        = string
  default     = "ami-09c813fb71547fc4f"
  description = "AMI ID for EC2 instances"
}

variable "instance_type" {
  type        = string
  default     = "t2.micro"
  description = "EC2 instance type"
}

variable "ec2_tags" {
  type        = map(string)
  default     = {
    Name    = "HelloWorld"
    Purpose = "variables_demo"
  }
  description = "Tags for EC2 instances"
}

variable "sg_name" {
  type        = string
  default     = "allow-all"
  description = "Security group name"
}

variable "sg_description" {
  type        = string
  default     = "Allow all traffic for instance"
  description = "Security group description"
}

variable "cidr_blocks" {
  type        = list(string)
  default     = ["0.0.0.0/0"]
  description = "CIDR blocks for security group"
}

variable "instances" {
  type        = list(string)
  default     = ["mongodb", "mysql", "redis", "rabbitmq"]
  description = "Names of EC2 instances"
}

variable "zone_id" {
  type        = string
  default     = "Z06785221SBGYOQ3RLYGM"
  description = "Route 53 hosted zone ID"
}

variable "domain_name" {
  type        = string
  default     = "rshopdaws84s.site"
  description = "Domain name for Route 53 records"
}

Using Variables

Variables are referenced in other .tf files using var.<variable_name>. For example, var.ami_id refers to the ami_id variable.


2. Variable Precedence

What is Variable Precedence?

Terraform allows you to set variable values in multiple ways (e.g., defaults, files, command-line flags). Variable precedence determines which value Terraform uses when a variable is defined in multiple places.

Why is Variable Precedence Important?

  • Flexibility: You can override default values for specific environments or use cases.

  • Control: Ensures the correct value is used when multiple sources provide values.

  • Debugging: Understanding precedence helps troubleshoot unexpected variable values.

Precedence Order (Highest to Lowest)

  1. Command-line flags: -var "ami_id=ami-12345678" or -var-file=prod.tfvars.

  2. .tfvars files: Files like terraform.tfvars or prod.tfvars loaded explicitly.

  3. Environment variables: Variables prefixed with TF_VAR_ (e.g., TF_VAR_ami_id).

  4. Default values: Defined in the variable block’s default field.

Example: Setting Variable Values

  • Default in variables.tf:

      variable "instance_type" {
        type    = string
        default = "t2.micro"
      }
    
  • Override in prod.tfvars:

      instance_type = "t3.medium"
    
  • Override via CLI:

      terraform apply -var="instance_type=t3.large"
    

In this case, t3.large (CLI) takes precedence over t3.medium (prod.tfvars), which takes precedence over t2.micro (default).

Example File: prod.tfvars

# prod.tfvars
ami_id        = "ami-98765432"
instance_type = "t3.medium"
ec2_tags = {
  Name    = "ProdServer"
  Purpose = "production"
}

Run with:

terraform apply -var-file=prod.tfvars

3. Conditions

What are Conditions?

Conditions in Terraform allow you to make decisions in your configuration using conditional expressions. They enable you to apply different values or resources based on a condition, similar to if-else logic in programming.

Why Use Conditions?

  • Dynamic Configurations: Apply different settings based on environment (e.g., dev vs. prod).

  • Cost Optimization: Use smaller instance types in dev but larger ones in prod.

  • Simplification: Avoid duplicating code for similar resources with slight differences.

Syntax

A conditional expression follows this format:

condition ? value_if_true : value_if_false

Example: Conditional Instance Type

Suppose you want to use t2.micro for dev and t3.medium for prod based on a variable environment.

# variables.tf
variable "environment" {
  type        = string
  default     = "dev"
  description = "Deployment environment (dev or prod)"
}

# ec2.tf
resource "aws_instance" "example" {
  ami           = var.ami_id
  instance_type = var.environment == "prod" ? "t3.medium" : "t2.micro"
  vpc_security_group_ids = [aws_security_group.allow_all.id]
  tags = var.ec2_tags
}

Here, if var.environment is "prod", the instance type is t3.medium; otherwise, it’s t2.micro.


4. Count-Based Loop

What is a Count-Based Loop?

The count meta-argument in Terraform creates multiple instances of a resource based on a numeric value. It’s like a for loop that repeats a resource block a specified number of times.

Why Use Count?

  • Scalability: Create multiple resources (e.g., 4 EC2 instances) without duplicating code.

  • Simplicity: Manage similar resources with a single block.

  • Dynamic Provisioning: Adjust the number of resources based on variables.

How It Works

  • Set count = <number> in a resource block.

  • Access the index of each instance using count.index (starts at 0).

  • Use count with lists to assign unique values (e.g., names) to each resource.

Example: Creating Multiple EC2 Instances

This example creates 4 EC2 instances, each named after an entry in the instances variable list.

# ec2.tf
resource "aws_instance" "servers" {
  count                  = 4
  ami                    = var.ami_id
  instance_type          = var.instance_type
  vpc_security_group_ids = [aws_security_group.allow_all.id]

  tags = {
    Name = var.instances[count.index]
  }
}

Here:

  • count = 4 creates 4 EC2 instances.

  • var.instances[count.index] assigns names (mongodb, mysql, redis, rabbitmq) from the instances list.


5. For_Each Loop

What is a For_Each Loop?

The for_each meta-argument creates resources based on a map or set of strings, where each resource is tied to a unique key. Unlike count, which uses numeric indices, for_each uses keys for more explicit control.

Why Use For_Each?

  • Key-Based Control: Ideal when resources need unique identifiers (e.g., names or IDs).

  • Flexibility: Works with maps or sets, allowing complex configurations.

  • Stability: Adding or removing items doesn’t affect unrelated resources (unlike count, where index shifts can cause issues).

Syntax

resource "type" "name" {
  for_each = <map_or_set>
  # Access key/value with each.key and each.value
}

Example: Creating EC2 Instances with For_Each

This example creates EC2 instances using a map of instance names and types.

# variables.tf
variable "instance_configs" {
  type = map(object({
    name = string
    type = string
  }))
  default = {
    mongodb = { name = "mongodb", type = "t2.micro" }
    mysql   = { name = "mysql", type = "t3.small" }
    redis   = { name = "redis", type = "t2.micro" }
  }
}

# ec2.tf
resource "aws_instance" "servers" {
  for_each               = var.instance_configs
  ami                    = var.ami_id
  instance_type          = each.value.type
  vpc_security_group_ids = [aws_security_group.allow_all.id]

  tags = {
    Name = each.value.name
  }
}

Here:

  • for_each iterates over the instance_configs map.

  • each.key is the map key (e.g., mongodb).

  • each.value.name and each.value.type access the name and type for each instance.


6. Creating EC2 Instances, Security Groups, and Route 53 Records

Now, let’s combine the above concepts to create a complete setup with EC2 instances, a security group, and Route 53 DNS records.

File Structure

├── ec2.tf
├── output.tf
├── provider.tf
├── route53.tf
├── sg.tf
├── terraform.tfvars
└── variables.tf

Complete Example

provider.tf

Configures the AWS provider.

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "5.98.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"
}

variables.tf

Defines all variables (as shown in the Variables section).

sg.tf

Creates a security group allowing all traffic.

resource "aws_security_group" "allow_all" {
  name        = var.sg_name
  description = var.sg_description

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = var.cidr_blocks
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = var.cidr_blocks
  }

  tags = var.sg_tags
}

ec2.tf

Creates EC2 instances using a count-based loop and conditions.

resource "aws_instance" "servers" {
  count                  = length(var.instances)
  ami                    = var.ami_id
  instance_type          = var.environment == "prod" ? "t3.medium" : var.instance_type
  vpc_security_group_ids = [aws_security_group.allow_all.id]

  tags = {
    Name = var.instances[count.index]
  }
}

route53.tf

Creates Route 53 DNS records for each EC2 instance.

resource "aws_route53_record" "dns" {
  count   = length(var.instances)
  zone_id = var.zone_id
  name    = "${var.instances[count.index]}.${var.domain_name}"
  type    = "A"
  ttl     = 1
  records = [aws_instance.servers[count.index].private_ip]
}

output.tf

Outputs instance details.

output "instance_ips" {
  value = { for instance in aws_instance.servers : instance.tags.Name => instance.private_ip }
}

terraform.tfvars

Overrides defaults for a specific environment.

environment = "prod"
instance_type = "t3.medium"

Steps to Run

  1. Initialize: terraform init

  2. Plan: terraform plan

  3. Apply: terraform apply

  4. Destroy: terraform destroy (to clean up)


Interview Questions and Answers

1. What are Terraform variables, and why are they used?

Answer: Terraform variables allow you to parameterize configurations, making them reusable and flexible. They’re used to avoid hardcoding values, centralize settings, and enable customization across environments. Variables are defined with type, default, and description and referenced using var.<name>.

2. Explain Terraform variable precedence with an example.

Answer: Variable precedence determines which value Terraform uses when a variable is set in multiple places. The order is:

  1. Command-line flags (-var or -var-file)

  2. .tfvars files

  3. Environment variables (TF_VAR_)

  4. Default values
    Example: If instance_type has a default of t2.micro but prod.tfvars sets it to t3.medium and CLI uses -var="instance_type=t3.large", Terraform uses t3.large.

3. How do conditions work in Terraform? Provide an example.

Answer: Conditions use the syntax condition ? value_if_true : value_if_false to apply values dynamically. Example: instance_type = var.environment == "prod" ? "t3.medium" : "t2.micro" sets t3.medium for prod and t2.micro for other environments.

4. What is the difference between count and for_each in Terraform?

Answer:

  • Count: Creates multiple resources based on a number, using count.index. Best for ordered lists but can cause issues if the list changes.

  • For_Each: Creates resources based on a map or set, using each.key and each.value. Ideal for key-based resources and stable configurations.
    Example: Use count to create 4 EC2 instances with names from a list; use for_each to create instances from a map with specific names and types.

5. How would you create a security group in Terraform?

Answer: Define a security group using the aws_security_group resource, specifying name, description, ingress, and egress rules. Example:

resource "aws_security_group" "allow_all" {
  name        = "allow-all"
  description = "Allow all traffic"
  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

6. What is the purpose of Route 53 records in Terraform?

Answer: Route 53 records map domain names to resources (e.g., EC2 instances) in AWS. In Terraform, the aws_route53_record resource creates DNS records like A records. Example: Create an A record for mongodb.example.com pointing to an EC2 instance’s IP.

7. How do you handle dependencies in Terraform?

Answer: Terraform automatically manages dependencies based on resource references. For example, if an EC2 instance references a security group’s ID (vpc_security_group_ids = [aws_security_group.allow_all.id]), Terraform creates the security group first. Explicit dependencies can be defined using depends_on.


Conclusion

This guide covered Terraform’s concepts—variables, variable precedence, conditions, count-based loops, and for_each loops—and demonstrated their use in creating EC2 instances, security groups, and Route 53 records. By using these features, you can write flexible, scalable, and maintainable infrastructure code. The provided file structure and examples make it easy to get started, and the interview questions prepare you for real-world discussions.

0
Subscribe to my newsletter

Read articles from ESHNITHIN YADAV directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

ESHNITHIN YADAV
ESHNITHIN YADAV