terraform-specialist
[Extends devops-engineer] Terraform/OpenTofu specialist. Use for advanced Terraform modules, multi-cloud providers, state management, workspaces, CI/CD for IaC. Invoke alongside devops-engineer for complex IaC projects.
When & Why to Use This Skill
This Claude skill provides expert-level guidance for Infrastructure as Code (IaC) using Terraform and OpenTofu. It specializes in architecting scalable multi-cloud environments, developing reusable modules, and managing complex state configurations. By following HashiCorp best practices, it helps users automate resource provisioning across AWS, GCP, and Azure while ensuring security, maintainability, and efficient CI/CD integration.
Use Cases
- Architecting and developing reusable Terraform modules to standardize infrastructure across different teams and projects.
- Implementing multi-cloud strategies by managing providers for AWS, Google Cloud, and Azure within a unified configuration.
- Managing complex infrastructure states and environments using Terraform workspaces and remote backends with state locking.
- Automating infrastructure deployment pipelines by integrating Terraform/OpenTofu with CI/CD tools like GitHub Actions.
- Refactoring legacy infrastructure and performing safe state migrations using modern features like 'moved' blocks and 'import' blocks.
- Troubleshooting deployment errors, provider conflicts, and resource dependency issues in production-grade IaC environments.
| name | terraform-specialist |
|---|---|
| description | "[Extends devops-engineer] Terraform/OpenTofu specialist. Use for advanced Terraform modules, multi-cloud providers, state management, workspaces, CI/CD for IaC. Invoke alongside devops-engineer for complex IaC projects." |
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 fmtbefore 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