terraform-skill
π‘ Summary
A comprehensive guide for Terraform and OpenTofu best practices covering testing strategies, module architecture, CI/CD, and production patterns.
π― Target Audience
π€ AI Roast: βThis skill is like a seasoned DevOps engineer who can't stop talking about best practices, but at least they're the right ones.β
The skill promotes security scanning tools (trivy, checkov) but does not enforce their use. The main risk is that users might follow architectural patterns without implementing the recommended security scanning, leading to insecure IaC deployments. Mitigation: Always integrate security scanning into CI/CD pipelines as a mandatory gate.
name: terraform-skill description: Use when working with Terraform or OpenTofu - creating modules, writing tests (native test framework, Terratest), setting up CI/CD pipelines, reviewing configurations, choosing between testing approaches, debugging state issues, implementing security scanning (trivy, checkov), or making infrastructure-as-code architecture decisions license: Apache-2.0 metadata: author: Anton Babenko version: 1.5.0
Terraform Skill for Claude
Comprehensive Terraform and OpenTofu guidance covering testing, modules, CI/CD, and production patterns. Based on terraform-best-practices.com and enterprise experience.
When to Use This Skill
Activate this skill when:
- Creating new Terraform or OpenTofu configurations or modules
- Setting up testing infrastructure for IaC code
- Deciding between testing approaches (validate, plan, frameworks)
- Structuring multi-environment deployments
- Implementing CI/CD for infrastructure-as-code
- Reviewing or refactoring existing Terraform/OpenTofu projects
- Choosing between module patterns or state management approaches
Don't use this skill for:
- Basic Terraform/OpenTofu syntax questions (Claude knows this)
- Provider-specific API reference (link to docs instead)
- Cloud platform questions unrelated to Terraform/OpenTofu
Core Principles
1. Code Structure Philosophy
Module Hierarchy:
| Type | When to Use | Scope | |------|-------------|-------| | Resource Module | Single logical group of connected resources | VPC + subnets, Security group + rules | | Infrastructure Module | Collection of resource modules for a purpose | Multiple resource modules in one region/account | | Composition | Complete infrastructure | Spans multiple regions/accounts |
Hierarchy: Resource β Resource Module β Infrastructure Module β Composition
Directory Structure:
environments/ # Environment-specific configurations
βββ prod/
βββ staging/
βββ dev/
modules/ # Reusable modules
βββ networking/
βββ compute/
βββ data/
examples/ # Module usage examples (also serve as tests)
βββ complete/
βββ minimal/
Key principle from terraform-best-practices.com:
- Separate environments (prod, staging) from modules (reusable components)
- Use examples/ as both documentation and integration test fixtures
- Keep modules small and focused (single responsibility)
For detailed module architecture, see: Code Patterns: Module Types & Hierarchy
2. Naming Conventions
Resources:
# Good: Descriptive, contextual resource "aws_instance" "web_server" { } resource "aws_s3_bucket" "application_logs" { } # Good: "this" for singleton resources (only one of that type) resource "aws_vpc" "this" { } resource "aws_security_group" "this" { } # Avoid: Generic names for non-singletons resource "aws_instance" "main" { } resource "aws_s3_bucket" "bucket" { }
Singleton Resources:
Use "this" when your module creates only one resource of that type:
β DO:
resource "aws_vpc" "this" {} # Module creates one VPC resource "aws_security_group" "this" {} # Module creates one SG
β DON'T use "this" for multiple resources:
resource "aws_subnet" "this" {} # If creating multiple subnets
Use descriptive names when creating multiple resources of the same type.
Variables:
# Prefix with context when needed var.vpc_cidr_block # Not just "cidr" var.database_instance_class # Not just "instance_class"
Files:
main.tf- Primary resourcesvariables.tf- Input variablesoutputs.tf- Output valuesversions.tf- Provider versionsdata.tf- Data sources (optional)
Testing Strategy Framework
Decision Matrix: Which Testing Approach?
| Your Situation | Recommended Approach | Tools | Cost |
|----------------|---------------------|-------|------|
| Quick syntax check | Static analysis | terraform validate, fmt | Free |
| Pre-commit validation | Static + lint | validate, tflint, trivy, checkov | Free |
| Terraform 1.6+, simple logic | Native test framework | Built-in terraform test | Free-Low |
| Pre-1.6, or Go expertise | Integration testing | Terratest | Low-Med |
| Security/compliance focus | Policy as code | OPA, Sentinel | Free |
| Cost-sensitive workflow | Mock providers (1.7+) | Native tests + mocking | Free |
| Multi-cloud, complex | Full integration | Terratest + real infra | Med-High |
Testing Pyramid for Infrastructure
/\
/ \ End-to-End Tests (Expensive)
/____\ - Full environment deployment
/ \ - Production-like setup
/________\
/ \ Integration Tests (Moderate)
/____________\ - Module testing in isolation
/ \ - Real resources in test account
/________________\ Static Analysis (Cheap)
- validate, fmt, lint
- Security scanning
Native Test Best Practices (1.6+)
Before generating test code:
-
Validate schemas with Terraform MCP:
Search provider docs β Get resource schema β Identify block types -
Choose correct command mode:
command = plan- Fast, for input validationcommand = apply- Required for computed values and set-type blocks
-
Handle set-type blocks correctly:
- Cannot index with
[0] - Use
forexpressions to iterate - Or use
command = applyto materialize
- Cannot index with
Common patterns:
- S3 encryption rules: set (use for expressions)
- Lifecycle transitions: set (use for expressions)
- IAM policy statements: set (use for expressions)
For detailed testing guides, see:
- Testing Frameworks Guide - Deep dive into static analysis, native tests, and Terratest
- Quick Reference - Decision flowchart and command cheat sheet
Code Structure Standards
Resource Block Ordering
Strict ordering for consistency:
countorfor_eachFIRST (blank line after)- Other arguments
tagsas last real argumentdepends_onafter tags (if needed)lifecycleat the very end (if needed)
# β GOOD - Correct ordering resource "aws_nat_gateway" "this" { count = var.create_nat_gateway ? 1 : 0 allocation_id = aws_eip.this[0].id subnet_id = aws_subnet.public[0].id tags = { Name = "${var.name}-nat" } depends_on = [aws_internet_gateway.this] lifecycle { create_before_destroy = true } }
Variable Block Ordering
description(ALWAYS required)typedefaultvalidationnullable(when setting to false)
variable "environment" { description = "Environment name for resource tagging" type = string default = "dev" validation { condition = contains(["dev", "staging", "prod"], var.environment) error_message = "Environment must be one of: dev, staging, prod." } nullable = false }
For complete structure guidelines, see: Code Patterns: Block Ordering & Structure
Count vs For_Each: When to Use Each
Quick Decision Guide
| Scenario | Use | Why |
|----------|-----|-----|
| Boolean condition (create or don't) | count = condition ? 1 : 0 | Simple on/off toggle |
| Simple numeric replication | count = 3 | Fixed number of identical resources |
| Items may be reordered/removed | for_each = toset(list) | Stable resource addresses |
| Reference by key | for_each = map | Named access to resources |
| Multiple named resources | for_each | Better maintainability |
Common Patterns
Boolean conditions:
# β GOOD - Boolean condition resource "aws_nat_gateway" "this" { count = var.create_nat_gateway ? 1 : 0 # ... }
Stable addressing with for_each:
# β GOOD - Removing "us-east-1b" only affects that subnet resource "aws_subnet" "private" { for_each = toset(var.availability_zones) availability_zone = each.key # ... } # β BAD - Removing middle AZ recreates all subsequent subnets resource "aws_subnet" "private" { count = length(var.availability_zones) availability_zone = var.availability_zones[count.index] # ... }
For migration guides and detailed examples, see: Code Patterns: Count vs For_Each
Locals for Dependency Management
Use locals to ensure correct resource deletion order:
# Problem: Subnets might be deleted after CIDR blocks, causing errors # Solution: Use try() in locals to hint deletion order locals { # References secondary CIDR first, falling back to VPC # Forces Terraform to delete subnets before CIDR association vpc_id = try( aws_vpc_ipv4_cidr_block_association.this[0].vpc_id, aws_vpc.this.id, "" ) } resource "aws_vpc" "this" { cidr_block = "10.0.0.0/16" } resource "aws_vpc_ipv4_cidr_block_association" "this" { count = var.add_secondary_cidr ? 1 : 0 vpc_id = aws_vpc.this.id cidr_block = "10.1.0.0/16" } resource "aws_subnet" "public" { vpc_id = local.vpc_id # Uses local, not direct reference cidr_block = "10.1.0.0/24" }
Why this matters:
- Prevents deletion errors when destroying infrastructure
- Ensures correct dependency order without explicit
depends_on - Particularly useful for VPC configurations with secondary CIDR blocks
For detailed examples, see: Code Patterns: Locals for Dependency Management
Module Development
Standard Module Structure
my-module/
βββ README.md # Usage documentation
βββ main.tf # Primary resources
βββ variables.tf # Input variables with descriptions
βββ outputs.tf # Output values
βββ versions.tf
Pros
- Extensive coverage of real-world IaC challenges
- Clear decision frameworks for testing and architecture
- Based on established best practices from terraform-best-practices.com
- Practical examples and anti-patterns highlighted
Cons
- Assumes intermediate to advanced Terraform knowledge
- May be overwhelming for beginners
- Focuses heavily on AWS examples
- Requires user to navigate between multiple reference documents
Related Skills
infra-skills
AβPowerful, but the setup might scare off the impatient.β
hosted-agents
BβPowerful, but the setup might scare off the impatient.β
pytorch
SβIt's the Swiss Army knife of deep learning, but good luck figuring out which of the 47 installation methods is the one that won't break your system.β
Disclaimer: This content is sourced from GitHub open source projects for display and rating purposes only.
Copyright belongs to the original author antonbabenko.
