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 5What 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.