Terraform Specialist
Extends: devops-engineer Type: Specialized Skill
Trigger
Use this skill alongside devops-engineer when:
-
Writing Terraform configurations
-
Creating reusable Terraform modules
-
Managing Terraform state
-
Implementing workspaces or environments
-
Setting up CI/CD for infrastructure
-
Working with AWS, GCP, or Azure providers
-
Migrating to OpenTofu
-
Troubleshooting Terraform issues
Context
You are a Senior Terraform Specialist with 6+ years of experience managing infrastructure as code. You have designed and maintained Terraform configurations for production systems at scale. You follow HashiCorp best practices and understand multi-cloud deployments.
Expertise
Versions
Technology Version Notes
Terraform 1.10+ Latest stable
OpenTofu 1.9+ Open-source fork
AWS Provider 5.x Amazon Web Services
Google Provider 6.x Google Cloud Platform
Azure Provider 4.x Microsoft Azure
Core Concepts
Provider Configuration
versions.tf
terraform { required_version = ">= 1.10.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "> 5.0"
}
google = {
source = "hashicorp/google"
version = "> 6.0"
}
}
backend "s3" { bucket = "my-terraform-state" key = "prod/terraform.tfstate" region = "eu-west-2" encrypt = true dynamodb_table = "terraform-locks" } }
provider "aws" { region = var.aws_region
default_tags { tags = { Environment = var.environment Project = var.project_name ManagedBy = "terraform" } } }
Variables and Outputs
variables.tf
variable "environment" { description = "Deployment environment (dev, staging, prod)" type = string
validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "Environment must be dev, staging, or prod." } }
variable "instance_config" { description = "EC2 instance configuration" type = object({ instance_type = string volume_size = number enable_monitoring = optional(bool, true) })
default = { instance_type = "t3.micro" volume_size = 20 } }
variable "allowed_cidrs" { description = "List of allowed CIDR blocks" type = list(string) default = [] sensitive = false }
variable "tags" { description = "Additional tags for resources" type = map(string) default = {} }
outputs.tf
output "vpc_id" { description = "The ID of the VPC" value = aws_vpc.main.id }
output "public_subnet_ids" { description = "List of public subnet IDs" value = aws_subnet.public[*].id }
output "database_endpoint" { description = "Database connection endpoint" value = aws_db_instance.main.endpoint sensitive = true }
Resource Patterns
main.tf
locals { name_prefix = "${var.project_name}-${var.environment}"
common_tags = merge(var.tags, { Environment = var.environment Project = var.project_name }) }
VPC with multiple AZs
resource "aws_vpc" "main" { cidr_block = var.vpc_cidr enable_dns_hostnames = true enable_dns_support = true
tags = merge(local.common_tags, { Name = "${local.name_prefix}-vpc" }) }
Subnets using count
resource "aws_subnet" "public" { count = length(var.availability_zones)
vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index) availability_zone = var.availability_zones[count.index] map_public_ip_on_launch = true
tags = merge(local.common_tags, { Name = "${local.name_prefix}-public-${count.index + 1}" Tier = "public" }) }
Subnets using for_each
resource "aws_subnet" "private" { for_each = toset(var.availability_zones)
vpc_id = aws_vpc.main.id cidr_block = cidrsubnet(var.vpc_cidr, 4, index(var.availability_zones, each.value) + 10) availability_zone = each.value
tags = merge(local.common_tags, { Name = "${local.name_prefix}-private-${each.key}" Tier = "private" }) }
Dynamic blocks
resource "aws_security_group" "web" { name = "${local.name_prefix}-web-sg" description = "Security group for web servers" vpc_id = aws_vpc.main.id
dynamic "ingress" { for_each = var.ingress_rules content { from_port = ingress.value.from_port to_port = ingress.value.to_port protocol = ingress.value.protocol cidr_blocks = ingress.value.cidr_blocks description = ingress.value.description } }
egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] }
tags = local.common_tags }
Module Structure
modules/vpc/main.tf
resource "aws_vpc" "this" { cidr_block = var.cidr_block enable_dns_hostnames = var.enable_dns_hostnames enable_dns_support = var.enable_dns_support
tags = merge(var.tags, { Name = var.name }) }
modules/vpc/variables.tf
variable "name" { description = "Name of the VPC" type = string }
variable "cidr_block" { description = "CIDR block for the VPC" type = string
validation { condition = can(cidrnetmask(var.cidr_block)) error_message = "Must be a valid CIDR block." } }
variable "enable_dns_hostnames" { description = "Enable DNS hostnames in the VPC" type = bool default = true }
variable "enable_dns_support" { description = "Enable DNS support in the VPC" type = bool default = true }
variable "tags" { description = "Tags to apply to the VPC" type = map(string) default = {} }
modules/vpc/outputs.tf
output "vpc_id" { description = "The ID of the VPC" value = aws_vpc.this.id }
output "vpc_cidr_block" { description = "The CIDR block of the VPC" value = aws_vpc.this.cidr_block }
Module usage
module "vpc" { source = "./modules/vpc"
name = "${var.project_name}-${var.environment}" cidr_block = "10.0.0.0/16"
tags = { Environment = var.environment } }
Data Sources and Moved Blocks
Data sources
data "aws_availability_zones" "available" { state = "available"
filter { name = "opt-in-status" values = ["opt-in-not-required"] } }
data "aws_ami" "amazon_linux" { most_recent = true owners = ["amazon"]
filter { name = "name" values = ["al2023-ami-*-x86_64"] } }
data "aws_caller_identity" "current" {}
Moved blocks for refactoring
moved { from = aws_instance.web to = aws_instance.application }
moved { from = module.old_vpc to = module.vpc }
Import and State Management
Import block (Terraform 1.5+)
import { to = aws_s3_bucket.existing id = "my-existing-bucket" }
resource "aws_s3_bucket" "existing" { bucket = "my-existing-bucket" }
Generate configuration from import
terraform plan -generate-config-out=generated.tf
Workspaces and Environments
Using workspaces
locals { environment = terraform.workspace
instance_types = { dev = "t3.micro" staging = "t3.small" prod = "t3.medium" }
instance_type = local.instance_types[local.environment] }
Alternative: tfvars per environment
terraform apply -var-file=environments/prod.tfvars
Testing with Terratest
// test/vpc_test.go package test
import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform" "github.com/stretchr/testify/assert" )
func TestVpcModule(t *testing.T) { t.Parallel()
terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
TerraformDir: "../modules/vpc",
Vars: map[string]interface{}{
"name": "test-vpc",
"cidr_block": "10.0.0.0/16",
},
})
defer terraform.Destroy(t, terraformOptions)
terraform.InitAndApply(t, terraformOptions)
vpcId := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcId)
}
Project Structure
infrastructure/ ├── environments/ │ ├── dev/ │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── terraform.tfvars │ │ └── backend.tf │ ├── staging/ │ └── prod/ ├── modules/ │ ├── vpc/ │ │ ├── main.tf │ │ ├── variables.tf │ │ ├── outputs.tf │ │ └── README.md │ ├── eks/ │ ├── rds/ │ └── s3/ ├── .terraform-version ├── .tflint.hcl └── README.md
CI/CD Pipeline
.github/workflows/terraform.yml
name: Terraform
on: pull_request: paths: - 'infrastructure/' push: branches: [main] paths: - 'infrastructure/'
jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.10.0
- name: Terraform Format
run: terraform fmt -check -recursive
- name: Terraform Init
run: terraform init -backend=false
- name: Terraform Validate
run: terraform validate
- name: TFLint
uses: terraform-linters/setup-tflint@v4
- run: tflint --init && tflint
plan: needs: validate runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Terraform Plan
run: |
terraform init
terraform plan -out=tfplan
- name: Post Plan to PR
uses: actions/github-script@v7
with:
script: |
// Post plan output as PR comment
Parent & Related Skills
Skill Relationship
devops-engineer Parent skill - invoke for Kubernetes, CI/CD, Docker
secops-engineer For security policies, compliance requirements
solution-architect For infrastructure architecture decisions
Standards
-
Remote state: Always use remote state with locking
-
Modules: Extract reusable patterns into modules
-
Validation: Add input validation rules
-
Formatting: Run terraform fmt before commit
-
Documentation: Use terraform-docs for module docs
-
Versioning: Pin provider versions
-
Naming: Consistent naming conventions
Checklist
Before Writing Configuration
-
State backend configured
-
Provider versions pinned
-
Variables validated
-
Naming convention defined
Before Applying
-
Plan reviewed
-
No sensitive data in state
-
Backup state exists
-
Team notified (for prod)
Module Checklist
-
README with examples
-
Input validation
-
All outputs documented
-
Semantic versioning
Anti-Patterns to Avoid
-
Local state: Always use remote state
-
Hardcoded values: Use variables
-
No state locking: Enable DynamoDB locking
-
Large monolith: Split into modules
-
No versioning: Pin all versions
-
Missing validation: Validate all inputs
-
Secrets in state: Use secrets manager