Terraform Skill
When to activate
- Writing Terraform modules for AWS, GCP, or Azure infrastructure
- Defining VPCs, subnets, security groups, and networking resources
- Provisioning compute resources (EC2, GKE, AKS, ECS, Lambda)
- Managing database infrastructure (RDS, Cloud SQL, Aurora)
- Setting up IAM roles, policies, and service accounts
- Writing remote state configuration (S3 backend, GCS, Terraform Cloud)
- Refactoring existing Terraform to use modules
- Writing CI/CD pipelines for
terraform plan and terraform apply
- Importing existing infrastructure into Terraform state
When NOT to use
- Pulumi, CDK, or Crossplane — different IaC tools, different patterns
- Helm chart configuration (use the Kubernetes skill instead)
- Application-level config (Kubernetes ConfigMaps, app env vars)
- One-off CLI operations that won't be repeated
Instructions
Module structure
Every Terraform project must follow this structure:
infrastructure/
├── modules/
│ ├── networking/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── outputs.tf
│ │ └── versions.tf
│ └── compute/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── environments/
│ ├── production/
│ │ ├── main.tf ← calls modules
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ └── staging/
│ └── ...
└── versions.tf ← root provider versions
State management — always remote
# backend.tf
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "production/networking/terraform.tfstate"
region = "eu-west-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
- Never use local state for anything shared
- Enable encryption and state locking (DynamoDB for S3 backend)
- Separate state files per environment and per module (not one giant state)
Variable and output discipline
# variables.tf — always include description and type
variable "environment" {
description = "Deployment environment (production, staging, development)"
type = string
validation {
condition = contains(["production", "staging", "development"], var.environment)
error_message = "Environment must be production, staging, or development."
}
}
# outputs.tf — output everything a consuming module might need
output "vpc_id" {
description = "The ID of the VPC"
value = aws_vpc.main.id
}
Secrets — never in state or code
- Never put secrets in
terraform.tfvars or hardcode them in .tf files
- Use
data "aws_secretsmanager_secret_version" or data "google_secret_manager_secret_version" to read secrets at apply time
- Sensitive outputs: mark with
sensitive = true to suppress in plan output
.gitignore must include: *.tfvars, *.tfstate, *.tfstate.backup, .terraform/
Resource naming conventions
# Consistent naming: {project}-{environment}-{resource}-{suffix}
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = {
Name = "${var.project}-${var.environment}-vpc"
Environment = var.environment
ManagedBy = "terraform"
}
}
Always tag every resource with Environment and ManagedBy = "terraform".
Plan before apply — always
- CI/CD pipeline:
terraform plan -out=tfplan on PR, terraform apply tfplan on merge
- Never run
terraform apply without a saved plan in production
- Use
-target sparingly — it creates drift between real state and plan
Common pitfalls
terraform destroy with no -target destroys everything — always confirm scope
- Changing a resource attribute that forces replacement (e.g., VPC CIDR) deletes and recreates — check plan carefully
- Provider version pinning is mandatory: use
~> 5.0 not >= 5.0
count vs for_each: use for_each with maps — count causes index drift when items are removed
Example
User: Create a Terraform module for a private RDS PostgreSQL instance on AWS with Multi-AZ, encrypted storage, and a dedicated security group.
Expected output structure:
modules/rds/main.tf — aws_db_instance, aws_db_subnet_group, aws_security_group
modules/rds/variables.tf — instance class, engine version, db name, VPC/subnet IDs, ingress CIDR
modules/rds/outputs.tf — endpoint, port, security group ID
- Security group: allows PostgreSQL (5432) only from app security group, no public access
storage_encrypted = true, multi_az = true, deletion_protection = true for production
- Password via
aws_secretsmanager_secret reference, never hardcoded
1---2name: terraform3description: Terraform modules, state management, workspaces, provider config, plan/apply workflow, remote backends4---56# Terraform Skill78## When to activate9- Writing Terraform modules for AWS, GCP, or Azure infrastructure10- Defining VPCs, subnets, security groups, and networking resources11- Provisioning compute resources (EC2, GKE, AKS, ECS, Lambda)12- Managing database infrastructure (RDS, Cloud SQL, Aurora)13- Setting up IAM roles, policies, and service accounts14- Writing remote state configuration (S3 backend, GCS, Terraform Cloud)15- Refactoring existing Terraform to use modules16- Writing CI/CD pipelines for `terraform plan` and `terraform apply`17- Importing existing infrastructure into Terraform state1819## When NOT to use20- Pulumi, CDK, or Crossplane — different IaC tools, different patterns21- Helm chart configuration (use the Kubernetes skill instead)22- Application-level config (Kubernetes ConfigMaps, app env vars)23- One-off CLI operations that won't be repeated2425## Instructions2627### Module structure28Every Terraform project must follow this structure:29```30infrastructure/31├── modules/32│ ├── networking/33│ │ ├── main.tf34│ │ ├── variables.tf35│ │ ├── outputs.tf36│ │ └── versions.tf37│ └── compute/38│ ├── main.tf39│ ├── variables.tf40│ └── outputs.tf41├── environments/42│ ├── production/43│ │ ├── main.tf ← calls modules44│ │ ├── variables.tf45│ │ ├── terraform.tfvars46│ │ └── backend.tf47│ └── staging/48│ └── ...49└── versions.tf ← root provider versions50```5152### State management — always remote53```hcl54# backend.tf55terraform {56 backend "s3" {57 bucket = "company-terraform-state"58 key = "production/networking/terraform.tfstate"59 region = "eu-west-1"60 encrypt = true61 dynamodb_table = "terraform-state-lock"62 }63}64```65- Never use local state for anything shared66- Enable encryption and state locking (DynamoDB for S3 backend)67- Separate state files per environment and per module (not one giant state)6869### Variable and output discipline70```hcl71# variables.tf — always include description and type72variable "environment" {73 description = "Deployment environment (production, staging, development)"74 type = string75 validation {76 condition = contains(["production", "staging", "development"], var.environment)77 error_message = "Environment must be production, staging, or development."78 }79}8081# outputs.tf — output everything a consuming module might need82output "vpc_id" {83 description = "The ID of the VPC"84 value = aws_vpc.main.id85}86```8788### Secrets — never in state or code89- Never put secrets in `terraform.tfvars` or hardcode them in `.tf` files90- Use `data "aws_secretsmanager_secret_version"` or `data "google_secret_manager_secret_version"` to read secrets at apply time91- Sensitive outputs: mark with `sensitive = true` to suppress in plan output92- `.gitignore` must include: `*.tfvars`, `*.tfstate`, `*.tfstate.backup`, `.terraform/`9394### Resource naming conventions95```hcl96# Consistent naming: {project}-{environment}-{resource}-{suffix}97resource "aws_vpc" "main" {98 cidr_block = var.vpc_cidr99 tags = {100 Name = "${var.project}-${var.environment}-vpc"101 Environment = var.environment102 ManagedBy = "terraform"103 }104}105```106Always tag every resource with `Environment` and `ManagedBy = "terraform"`.107108### Plan before apply — always109- CI/CD pipeline: `terraform plan -out=tfplan` on PR, `terraform apply tfplan` on merge110- Never run `terraform apply` without a saved plan in production111- Use `-target` sparingly — it creates drift between real state and plan112113### Common pitfalls114- `terraform destroy` with no `-target` destroys everything — always confirm scope115- Changing a resource attribute that forces replacement (e.g., VPC CIDR) deletes and recreates — check plan carefully116- Provider version pinning is mandatory: use `~> 5.0` not `>= 5.0`117- `count` vs `for_each`: use `for_each` with maps — `count` causes index drift when items are removed118119## Example120121**User:** Create a Terraform module for a private RDS PostgreSQL instance on AWS with Multi-AZ, encrypted storage, and a dedicated security group.122123**Expected output structure:**124- `modules/rds/main.tf` — `aws_db_instance`, `aws_db_subnet_group`, `aws_security_group`125- `modules/rds/variables.tf` — instance class, engine version, db name, VPC/subnet IDs, ingress CIDR126- `modules/rds/outputs.tf` — endpoint, port, security group ID127- Security group: allows PostgreSQL (5432) only from app security group, no public access128- `storage_encrypted = true`, `multi_az = true`, `deletion_protection = true` for production129- Password via `aws_secretsmanager_secret` reference, never hardcoded130131---