DevOps

Terraform Advanced

Master advanced Terraform: modules, workspaces, remote state, loops, conditionals, and best practices for production infrastructure.

By TechCoder TeamLast updated: 2026-06-02
In a Nutshell

Master advanced Terraform: modules, workspaces, remote state, loops, conditionals, and best practices for production infrastructure. This hands-on tutorial focuses on practical implementation of terraform advanced concepts.

Terraform Advanced

Master advanced Terraform concepts for building production-grade infrastructure.

Modules

Reusable, composable infrastructure components.

Module Structure

terraform-aws-vpc/
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── README.md
├── examples/
│   ├── complete/
│   └── simple/
└── modules/
    └── vpc-endpoints/

Creating a Module

# modules/ec2-instance/main.tf
resource "aws_instance" "this" {
  count = var.instance_count
  
  ami                    = var.ami
  instance_type          = var.instance_type
  subnet_id              = var.subnet_id
  vpc_security_group_ids = var.security_group_ids
  key_name               = var.key_name
  iam_instance_profile   = var.iam_instance_profile
  
  user_data = var.user_data
  user_data_replace_on_change = var.user_data_replace_on_change
  
  monitoring = var.monitoring
  
  root_block_device {
    volume_size           = var.root_volume_size
    volume_type           = var.root_volume_type
    iops                  = var.root_volume_iops
    encrypted             = var.root_volume_encrypted
    kms_key_id            = var.root_volume_kms_key_id
    delete_on_termination = true
  }
  
  dynamic "ebs_block_device" {
    for_each = var.ebs_block_devices
    
    content {
      device_name           = ebs_block_device.value.device_name
      volume_size           = ebs_block_device.value.volume_size
      volume_type           = ebs_block_device.value.volume_type
      iops                  = lookup(ebs_block_device.value, "iops", null)
      encrypted             = lookup(ebs_block_device.value, "encrypted", true)
      kms_key_id            = lookup(ebs_block_device.value, "kms_key_id", null)
      delete_on_termination = lookup(ebs_block_device.value, "delete_on_termination", true)
    }
  }
  
  tags = merge(
    {
      Name = var.instance_count > 1 ? "${var.name}-${count.index + 1}" : var.name
    },
    var.tags
  )
  
  volume_tags = merge(
    {
      Name = var.instance_count > 1 ? "${var.name}-${count.index + 1}-root" : "${var.name}-root"
    },
    var.tags
  )
}
# modules/ec2-instance/variables.tf
variable "name" {
  description = "Name to be used on EC2 instance created"
  type        = string
}

variable "instance_count" {
  description = "Number of instances to launch"
  type        = number
  default     = 1
}

variable "ami" {
  description = "ID of AMI to use for the instance"
  type        = string
}

variable "instance_type" {
  description = "The type of instance to start"
  type        = string
  default     = "t3.micro"
}

variable "ebs_block_devices" {
  description = "Additional EBS block devices to attach"
  type        = list(map(string))
  default     = []
}

variable "tags" {
  description = "A mapping of tags to assign to the resource"
  type        = map(string)
  default     = {}
}
# modules/ec2-instance/outputs.tf
output "id" {
  description = "The instance ID"
  value       = try(aws_instance.this[0].id, null)
}

output "arn" {
  description = "The ARN of the instance"
  value       = try(aws_instance.this[0].arn, null)
}

output "private_ip" {
  description = "The private IP address assigned to the instance"
  value       = try(aws_instance.this[0].private_ip, null)
}

output "public_ip" {
  description = "The public IP address assigned to the instance"
  value       = try(aws_instance.this[0].public_ip, null)
}

output "ids" {
  description = "List of instance IDs"
  value       = aws_instance.this[*].id
}

Using Modules

# main.tf
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"
  
  name = "production"
  cidr = "10.0.0.0/16"
  
  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]
  
  enable_nat_gateway   = true
  single_nat_gateway   = false
  enable_dns_hostnames = true
  enable_dns_support   = true
  
  public_subnet_tags = {
    "kubernetes.io/role/elb" = 1
  }
  
  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = 1
  }
}

module "web_servers" {
  source = "./modules/ec2-instance"
  
  name           = "web-server"
  instance_count = 3
  
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t3.small"
  subnet_id     = module.vpc.public_subnets[0]
  
  security_group_ids = [module.security_group.security_group_id]
  
  root_volume_size = 20
  
  ebs_block_devices = [
    {
      device_name = "/dev/sdf"
      volume_size = 100
      volume_type = "gp3"
    }
  ]
  
  tags = {
    Environment = "production"
    Team        = "platform"
  }
}

Dynamic Blocks and Meta-Arguments

Dynamic Blocks

resource "aws_security_group" "example" {
  name        = "example"
  description = "Example security group"
  vpc_id      = var.vpc_id
  
  # Dynamic ingress rules
  dynamic "ingress" {
    for_each = var.ingress_rules
    
    content {
      description = ingress.value.description
      from_port   = ingress.value.from_port
      to_port     = ingress.value.to_port
      protocol    = ingress.value.protocol
      cidr_blocks = ingress.value.cidr_blocks
    }
  }
  
  dynamic "egress" {
    for_each = var.egress_rules
    
    content {
      from_port   = egress.value.from_port
      to_port     = egress.value.to_port
      protocol    = egress.value.protocol
      cidr_blocks = egress.value.cidr_blocks
    }
  }
}

# Variable definition
variable "ingress_rules" {
  type = list(object({
    description = string
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
  }))
  default = []
}

for_each (Map Resources)

# Instead of count (which uses index)
resource "aws_instance" "web" {
  for_each = var.instances
  
  ami           = each.value.ami
  instance_type = each.value.instance_type
  subnet_id     = each.value.subnet_id
  
  tags = {
    Name = each.key
  }
}

# Variable
variable "instances" {
  type = map(object({
    ami           = string
    instance_type = string
    subnet_id     = string
  }))
  default = {
    "web-1" = {
      ami           = "ami-123"
      instance_type = "t3.micro"
      subnet_id     = "subnet-1"
    }
    "web-2" = {
      ami           = "ami-123"
      instance_type = "t3.small"
      subnet_id     = "subnet-2"
    }
  }
}

# Output
output "instance_ips" {
  value = { for k, v in aws_instance.web : k => v.public_ip }
}

Lifecycle Rules

resource "aws_instance" "example" {
  ami           = var.ami
  instance_type = var.instance_type
  
  lifecycle {
    # Create new before destroying old (for zero-downtime)
    create_before_destroy = true
    
    # Prevent accidental destruction
    prevent_destroy = false
    
    # Ignore changes to specific attributes
    ignore_changes = [
      user_data,
      tags["LastModified"]
    ]
    
    # Replace resource when these change
    replace_triggered_by = [
      aws_security_group.example
    ]
  }
}

Workspaces

Manage multiple environments with the same configuration.

# List workspaces
terraform workspace list

# Create new workspace
terraform workspace new staging
terraform workspace new production

# Select workspace
terraform workspace select staging

# Show current
terraform workspace show
# Using workspace in configuration
locals {
  env = terraform.workspace
  
  instance_count = {
    default = 1
    staging = 2
    production = 4
  }
  
  instance_type = {
    default = "t3.micro"
    staging = "t3.small"
    production = "t3.medium"
  }
}

resource "aws_instance" "web" {
  count = lookup(local.instance_count, local.env, local.instance_count["default"])
  
  ami           = var.ami
  instance_type = lookup(local.instance_type, local.env, local.instance_type["default"])
  
  tags = {
    Name        = "web-${terraform.workspace}-${count.index + 1}"
    Environment = terraform.workspace
  }
}

Terraform Best Practices

Project Structure

terraform/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   ├── staging/
│   └── production/
├── modules/
│   ├── vpc/
│   ├── compute/
│   └── database/
├── shared/
│   ├── backend.tf
│   └── provider.tf
└── README.md

State Management Best Practices

# Remote state with S3 and DynamoDB
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "production/vpc/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:us-east-1:123456789:key/terraform-state"
    dynamodb_table = "terraform-state-locks"
    
    # State locking and consistency
    acl = "bucket-owner-full-control"
  }
}

# State data source (access another workspace's state)
data "terraform_remote_state" "vpc" {
  backend = "s3"
  
  config = {
    bucket = "mycompany-terraform-state"
    key    = "production/vpc/terraform.tfstate"
    region = "us-east-1"
  }
}

# Use remote state
resource "aws_instance" "web" {
  subnet_id = data.terraform_remote_state.vpc.outputs.private_subnets[0]
}

Security Best Practices

# Encrypt EBS volumes
resource "aws_instance" "example" {
  ami           = var.ami
  instance_type = var.instance_type
  
  root_block_device {
    volume_size = 50
    encrypted   = true
    kms_key_id  = aws_kms_key.this.arn
  }
}

# Secrets in AWS Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "production/database/password"
}

locals {
  db_password = jsondecode(data.aws_secretsmanager_secret_version.db_password.secret_string)["password"]
}

# Use IAM roles instead of access keys
resource "aws_iam_role" "ec2_role" {
  name = "ec2-application-role"
  
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

Terraform Cloud / Enterprise

# Connect to Terraform Cloud
terraform {
  cloud {
    organization = "mycompany"
    
    workspaces {
      name = "production-vpc"
    }
  }
}

# Remote execution
terraform {
  cloud {
    organization = "mycompany"
    
    workspaces {
      tags = ["production", "vpc"]
    }
  }
}

Quiz

Quiz

Question 1 of 5

What is the benefit of using for_each instead of count?

It's faster
Resources are identified by key rather than index, preventing reordering issues
It uses less memory
It supports more resource types

Next Steps

Now let's explore Ansible for configuration management.