Terraform Generator
Generate Terraform modules, configure providers, define variables and outputs, set up remote state backends, and write production-ready HCL (.tf files) following current standards. Use when creating new Terraform resources, building Terraform projects, writing infrastructure as code (IaC), scaffolding AWS/Azure/GCP cloud infrastructure, working with terraform plan/apply workflows, or structuring multi-environment configurations.
npx skills add github:pantheon-org/tekhne --skill infrastructure--terraform--generatorOverview
This skill enables the generation of production-ready Terraform configurations following best practices and current standards. Automatically integrates validation and documentation lookup for custom providers and modules.
When to Use
- Creating new Terraform resources, modules or whole projects from a plain-language request.
- Scaffolding AWS, Azure or GCP infrastructure with providers, variables, outputs and a state backend.
- Structuring multi-environment or multi-provider configurations.
- Adding custom or third-party providers and modules that need a documentation lookup.
When Not to Use
- Validating or security-scanning existing HCL: use
terraform-validatorinstead. - Running
terraform applyor changing live infrastructure: this skill only writes configuration. - Non-Terraform IaC such as CloudFormation, Pulumi or Ansible.
- Explaining or debugging a failed apply from state alone, without generating new configuration.
Philosophy
- Production-ready by default. Pin versions, encrypt, use least-privilege and protect critical resources from the first draft.
- Look up, do not guess. Custom providers and modules need version-specific documentation before any HCL is written.
- Validate before handing over. Configuration is not finished until the validator passes all checks.
- Prefer data sources to hardcoded values. Regions, accounts, AZs and AMIs are looked up, not typed in.
- Keep it readable. Follow the principle of least surprise and explain non-obvious choices in comments.
Core Workflow
Work through these steps in order. Do not skip any step.
Step 1: Understand Requirements
Analyze the user’s request to determine:
- Which Terraform providers are required (AWS, Azure, GCP, custom, etc.)
- Whether any modules are being used (official, community, or custom)
- Version constraints for providers and modules
- Variable inputs and outputs needed
- State backend configuration (local, S3, remote, etc.)
Step 2: Check for Custom Providers/Modules
Standard providers (no lookup needed):
- hashicorp/aws, hashicorp/azurerm, hashicorp/google, hashicorp/kubernetes, other official HashiCorp providers
Custom/third-party providers/modules (require documentation lookup):
- Third-party providers (e.g., datadog/datadog, mongodb/mongodbatlas)
- Custom or community modules from Terraform Registry
- Private or company-specific modules
When custom providers/modules are detected:
-
Use WebSearch to find version-specific documentation:
Search query format: "[provider/module name] terraform [version] documentation [specific resource]" Example: "datadog terraform provider v3.30 monitor resource documentation" Example: "terraform-aws-modules vpc version 5.0 documentation" -
Focus searches on official documentation (registry.terraform.io, provider websites): required/optional arguments, attribute references, example usage, version compatibility notes.
-
If Context7 MCP is available and the provider/module is supported, use it as an alternative:
mcp__context7__resolve-library-id → mcp__context7__get-library-docs
Step 2.5: Consult Reference Files
Before generating configuration, read the relevant reference files:
cat references/terraform_best_practices.md
cat references/provider_examples.md
When to consult each reference:
| Reference | Read When |
|---|---|
terraform_best_practices.md |
Always - contains required patterns |
common_patterns.md |
Multi-environment, workspace, or complex setups |
provider_examples.md |
Generating AWS, Azure, GCP, or K8s resources |
modern_features.md |
Using Terraform 1.8+ features (ephemeral resources, write-only args, actions, etc.) |
Step 3: Generate Terraform Configuration
File Organization:
terraform-project/
├── main.tf # Primary resource definitions
├── variables.tf # Input variable declarations
├── outputs.tf # Output value declarations
├── versions.tf # Provider version constraints
├── terraform.tfvars # Variable values (optional, for examples)
└── backend.tf # Backend configuration (optional)
Best Practices to Follow:
-
Provider Configuration:
terraform { required_version = ">= 1.10, < 2.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 6.0" # Latest: v6.23.0 (Dec 2025) } } } provider "aws" { region = var.aws_region } -
Resource Naming: Use descriptive snake_case names; include resource type in name when helpful.
resource "aws_instance" "web_server" { ... } -
Variable Declarations:
variable "instance_type" { description = "EC2 instance type for web servers" type = string default = "t3.micro" validation { condition = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type) error_message = "Instance type must be t3.micro, t3.small, or t3.medium." } } -
Output Values:
output "instance_public_ip" { description = "Public IP address of the web server" value = aws_instance.web_server.public_ip } -
Use Data Sources for References:
data "aws_ami" "ubuntu" { most_recent = true owners = ["099720109477"] # Canonical filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] } } -
Module Usage:
module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.0.0" name = "my-vpc" cidr = "10.0.0.0/16" azs = ["us-east-1a", "us-east-1b"] private_subnets = ["10.0.1.0/24", "10.0.2.0/24"] public_subnets = ["10.0.101.0/24", "10.0.102.0/24"] } -
Use locals for Computed Values:
locals { common_tags = { Environment = var.environment ManagedBy = "Terraform" Project = var.project_name } } -
Lifecycle Rules When Appropriate:
resource "aws_instance" "example" { lifecycle { create_before_destroy = true prevent_destroy = true ignore_changes = [tags] } } -
Dynamic Blocks for Repeated Configuration:
resource "aws_security_group" "example" { 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 } } } -
Comments and Documentation: Add comments explaining complex logic, document why certain values are used, and include examples in variable descriptions.
Security Best Practices:
- Never hardcode sensitive values (use variables)
- Use data sources for AMIs and other dynamic values
- Implement least-privilege IAM policies
- Enable encryption by default
- Use secure backend configurations
Data Sources for Dynamic Values
Always use data sources for dynamic infrastructure values instead of hardcoding:
| Use Case | Data Source | Example |
|---|---|---|
| Current region | data "aws_region" "current" {} |
data.aws_region.current.name |
| Current account | data "aws_caller_identity" "current" {} |
data.aws_caller_identity.current.account_id |
| Available AZs | data "aws_availability_zones" "available" {} |
data.aws_availability_zones.available.names |
| Latest AMI | data "aws_ami" "..." |
With most_recent = true and filters |
| Existing VPC | data "aws_vpc" "..." |
Reference existing infrastructure |
Lifecycle Rules on Critical Resources
Add prevent_destroy = true on resources that could cause data loss or service disruption if accidentally destroyed:
# KMS Keys - protect from deletion
resource "aws_kms_key" "encryption" {
lifecycle { prevent_destroy = true }
}
# Databases - protect from deletion
resource "aws_db_instance" "main" {
lifecycle { prevent_destroy = true }
}
# S3 Buckets with data - protect from deletion
resource "aws_s3_bucket" "data" {
lifecycle { prevent_destroy = true }
}
Resources that must have prevent_destroy = true:
- KMS keys (
aws_kms_key) - RDS databases (
aws_db_instance,aws_rds_cluster) - S3 buckets containing data
- DynamoDB tables with data
- ElastiCache clusters
- Secrets Manager secrets
S3 Lifecycle Best Practices
When creating S3 buckets with lifecycle configurations, always include a rule to abort incomplete multipart uploads:
resource "aws_s3_bucket_lifecycle_configuration" "main" {
bucket = aws_s3_bucket.main.id
# Abort incomplete multipart uploads to prevent storage costs (Checkov CKV_AWS_300)
rule {
id = "abort-incomplete-uploads"
status = "Enabled"
filter {}
abort_incomplete_multipart_upload {
days_after_initiation = 7
}
}
rule {
id = "transition-to-ia"
status = "Enabled"
filter {
prefix = ""
}
transition {
days = 90
storage_class = "STANDARD_IA"
}
noncurrent_version_transition {
noncurrent_days = 30
storage_class = "STANDARD_IA"
}
noncurrent_version_expiration {
noncurrent_days = 365
}
}
}
Step 4: Validate Generated Configuration
After generating Terraform files, validate them using the devops-skills:terraform-validator skill:
Invoke: Skill(devops-skills:terraform-validator)
The devops-skills:terraform-validator skill will:
- Check HCL syntax with
terraform fmt -check - Initialize the configuration with
terraform init - Validate the configuration with
terraform validate - Run security scan with Checkov
- Perform dry-run testing (if requested) with
terraform plan
Fix-and-Revalidate Loop
If validation fails, fix the issues and re-run the validator. Do NOT proceed to Step 5 until all checks pass.
┌─────────────────────────────────────────────────────────┐
│ VALIDATION FAILED? │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────────────────┐ │
│ │ Fix │───▶│ Re-run │───▶│ All checks pass? │ │
│ │ Issue │ │ Skill │ │ YES → Step 5 │ │
│ └─────────┘ └─────────┘ │ NO → Loop back │ │
│ ▲ └─────────────────────┘ │
│ │ │ │
│ └────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Common validation failures to fix:
| Check | Issue | Fix |
|---|---|---|
CKV_AWS_300 |
Missing abort multipart upload | Add abort_incomplete_multipart_upload rule |
CKV_AWS_24 |
SSH open to 0.0.0.0/0 | Restrict to specific CIDR |
CKV_AWS_16 |
RDS encryption disabled | Add storage_encrypted = true |
terraform validate |
Invalid resource argument | Check provider documentation |
If custom providers are detected during validation:
- The devops-skills:terraform-validator skill will automatically fetch documentation
- Use the fetched documentation to fix any issues
Run the same checks by hand from the project directory when needed:
terraform fmt -check
terraform init
terraform validate
Step 5: Provide Usage Instructions
After successful generation and validation with all checks passing, provide the user with:
## Generated Files
| File | Description |
|------|-------------|
| `path/to/main.tf` | Main resource definitions |
| `path/to/variables.tf` | Input variables |
| `path/to/outputs.tf` | Output values |
| `path/to/versions.tf` | Provider version constraints |
## Next Steps
1. Review and customize `terraform.tfvars` with your values
2. Initialize Terraform: `terraform init`
3. Review the execution plan: `terraform plan`
4. Apply the configuration: `terraform apply`
## Customization Notes
- [ ] Update `variable_name` in terraform.tfvars
- [ ] Configure backend in backend.tf for remote state
- [ ] Adjust resource names/tags as needed
## Security Reminders
⚠️ Before applying:
- Review IAM policies and permissions
- Ensure sensitive values are NOT committed to version control
- Configure state backend with encryption enabled
- Set up state locking for team collaboration
Common Generation Patterns
Pattern 1: Simple Resource Creation
User request: “Create an AWS S3 bucket with versioning”
Generated files:
main.tf- S3 bucket resource with versioning enabledvariables.tf- Bucket name, tags variablesoutputs.tf- Bucket ARN and name outputsversions.tf- AWS provider version constraints
Pattern 2: Module-Based Infrastructure
User request: “Set up a VPC using the official AWS VPC module”
Actions:
- Identify module: terraform-aws-modules/vpc/aws
- Web search for latest version and documentation
- Generate configuration using module with appropriate inputs
- Validate with devops-skills:terraform-validator
Pattern 3: Multi-Provider Configuration
User request: “Create infrastructure across AWS and Datadog”
Actions:
- Identify standard provider (AWS) and custom provider (Datadog)
- Web search for Datadog provider documentation with version
- Generate configuration with both providers properly configured
- Ensure provider aliases if needed
- Validate with devops-skills:terraform-validator
Pattern 4: Complex Resource with Dependencies
User request: “Create an ECS cluster with ALB and auto-scaling”
Generated structure:
- Multiple resource blocks with proper dependencies
- Data sources for AMIs, availability zones, etc.
- Local values for computed configurations
- Comprehensive variables and outputs
- Proper dependency management using implicit references
Error Handling
Terraform-specific gotchas:
-
Provider Not Found: Verify source address format (
namespace/name) and version constraint syntax. -
Circular Dependencies: Use explicit
depends_onor break into separate modules. -
Validation Failures: Run devops-skills:terraform-validator for detailed errors, fix iteratively.
Version Awareness
Always consider version compatibility:
-
Terraform Version:
- Use
required_versionconstraint with both lower and upper bounds - Default to
>= 1.10, < 2.0for modern features (ephemeral resources, write-only) - Use
>= 1.14, < 2.0for latest features (actions, query command) - Document any version-specific features used
- Use
-
Provider Versions (as of December 2025):
- AWS:
~> 6.0(latest: v6.23.0) - Azure:
~> 4.0(latest: v4.54.0) - GCP:
~> 7.0(latest: v7.12.0) - 7.0 includes ephemeral resources & write-only attributes - Kubernetes:
~> 2.23 - Use
~>for minor version flexibility, pin major versions
- AWS:
-
Module Versions: Always pin module versions, review module documentation for compatibility, test updates in non-production first.
Version feature matrix and modern Terraform features (1.8+): See
references/modern_features.mdfor the full feature matrix, ephemeral resources, write-only arguments, actions blocks, import blocks withfor_each, list resources, and query command examples.
Notes
- Always run devops-skills:terraform-validator after generation
- Web search is essential for custom providers/modules
- Follow the principle of least surprise in configurations
- Make configurations readable and maintainable
- Include helpful comments and documentation
- Generate realistic examples in terraform.tfvars when helpful
Anti-Patterns
NEVER mix input variable defaults with environment-specific values in the same variables.tf
WHY: Variables with environment-specific defaults break reuse across workspaces; every workspace that sources the module inherits the wrong default silently.
BAD: default = "us-east-1" on a region variable in a shared module.
GOOD: Omit default on environment-specific variables; pass values via .tfvars files or environment variables (TF_VAR_region).
NEVER use count to manage resources that differ only by configuration
WHY: count creates indexed resources (e.g., aws_instance.web[0]) that break when items are removed from the middle of a list, forcing unwanted destroys and recreates.
BAD: count = length(var.instance_names) combined with var.instance_names[count.index] for named resources.
GOOD: Use for_each with a map or set to create named resources (e.g., for_each = toset(var.instance_names)) that survive insertions and deletions.
NEVER store Terraform state in a local backend for team or CI use
WHY: Local state cannot be shared, locked, or versioned; concurrent applies corrupt it and there is no recovery path.
BAD: No backend {} block (defaults to local state in terraform.tfstate).
GOOD: Configure backend "s3" {} (or equivalent) with a dynamodb_table argument for state locking.
NEVER hardcode provider credentials or region in .tf files
WHY: Credentials in source control are a security incident; hardcoded regions prevent cross-region reuse and require code changes for every deployment.
BAD: provider "aws" { region = "us-east-1" access_key = "AKIA..." secret_key = "..." }.
GOOD: Configure credentials via environment variables (AWS_REGION, AWS_ACCESS_KEY_ID) or IAM instance/execution roles; set region via var.aws_region.
NEVER use terraform apply without a saved plan in CI/CD
WHY: Running apply without a saved plan allows Terraform to pick up state changes that occurred between plan and apply, producing a different result than reviewed.
BAD: terraform apply -auto-approve as a single pipeline step.
GOOD: terraform plan -out=tfplan then terraform apply tfplan as two separate, sequential pipeline steps.
NEVER leave critical stateful resources without prevent_destroy
WHY: KMS keys, databases and data-bearing S3 buckets cause data loss or outage if an unreviewed plan destroys them.
BAD: resource "aws_db_instance" "main" { ... } with no lifecycle block.
GOOD: resource "aws_db_instance" "main" { lifecycle { prevent_destroy = true } }
NEVER hardcode AMI IDs, account IDs or availability zones
WHY: Hardcoded values go stale and tie the configuration to one account or region. Data sources resolve them at plan time.
BAD: ami = "ami-0abcdef1234567890"
GOOD: ami = data.aws_ami.ubuntu.id with a data "aws_ami" "ubuntu" block using most_recent = true and filters.
NEVER use an unpinned module or provider version
WHY: An unpinned module or provider can change behaviour between runs, so the same code plans differently over time.
BAD: module "vpc" { source = "terraform-aws-modules/vpc/aws" } with no version.
GOOD: module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "5.0.0" } and required_providers with version = "~> 6.0".
NEVER create an S3 lifecycle configuration without aborting incomplete multipart uploads
WHY: Incomplete uploads keep accruing storage cost and are flagged by Checkov (CKV_AWS_300).
BAD: An aws_s3_bucket_lifecycle_configuration containing only a transition rule.
GOOD: Add a rule with abort_incomplete_multipart_upload { days_after_initiation = 7 }.
NEVER hand over configuration before the validator passes
WHY: Skipping validation ships syntax, init or security-scan failures to the user, who then finds them at plan time.
BAD: Generate the files and stop at Step 3.
GOOD: Run the validator, fix failures, and re-run until all checks pass before Step 5.
NEVER guess arguments for a custom provider or module
WHY: Third-party providers differ by version, and invented arguments fail at terraform validate or, worse, silently misconfigure resources.
BAD: Write datadog_monitor arguments from memory.
GOOD: Search for version-specific documentation first, for example “datadog terraform provider v3.30 monitor resource documentation”.
NEVER omit required_version or give it only a lower bound
WHY: A lower bound alone lets a future major release run code written for older behaviour.
BAD: required_version = ">= 1.10"
GOOD: required_version = ">= 1.10, < 2.0"
References
references/
terraform_best_practices.md- Comprehensive best practices guidecommon_patterns.md- Common Terraform patterns and examplesprovider_examples.md- Example configurations for popular providersmodern_features.md- Terraform 1.8+ features: ephemeral resources, write-only args, actions, importfor_each, version feature matrix
To load a reference, read the file by its path relative to this skill:
cat references/terraform_best_practices.md
assets/
Template files for common setups:
minimal-project/- Minimal Terraform project templateaws-web-app/- AWS web application infrastructure templatemulti-env/- Multi-environment configuration template
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
No eval scenarios available.
Common Terraform Patterns
Multi-Environment Pattern
Directory Structure
terraform/
├── modules/
│ └── app/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
└── environments/
├── dev/
│ ├── main.tf
│ ├── variables.tf
│ ├── terraform.tfvars
│ └── backend.tf
├── staging/
│ ├── main.tf
│ ├── variables.tf
│ ├── terraform.tfvars
│ └── backend.tf
└── production/
├── main.tf
├── variables.tf
├── terraform.tfvars
└── backend.tf
Implementation
# environments/dev/main.tf
module "app" {
source = "../../modules/app"
environment = "dev"
instance_type = "t3.micro"
instance_count = 1
enable_backups = false
}
# environments/production/main.tf
module "app" {
source = "../../modules/app"
environment = "production"
instance_type = "t3.large"
instance_count = 3
enable_backups = true
}
Workspace Pattern
Using Workspaces for Environments
# main.tf
locals {
environment = terraform.workspace
environment_config = {
dev = {
instance_type = "t3.micro"
instance_count = 1
enable_backups = false
}
staging = {
instance_type = "t3.small"
instance_count = 2
enable_backups = true
}
prod = {
instance_type = "t3.large"
instance_count = 3
enable_backups = true
}
}
config = local.environment_config[local.environment]
}
resource "aws_instance" "app" {
count = local.config.instance_count
instance_type = local.config.instance_type
# ...
}
# Usage:
# terraform workspace new dev
# terraform workspace select dev
# terraform apply
Blue-Green Deployment Pattern
Infrastructure for Blue-Green Deployments
variable "active_environment" {
description = "Active environment (blue or green)"
type = string
default = "blue"
validation {
condition = contains(["blue", "green"], var.active_environment)
error_message = "Active environment must be blue or green."
}
}
# Blue environment
module "blue_environment" {
source = "./modules/environment"
name = "blue"
instance_count = var.active_environment == "blue" ? var.desired_capacity : 1
min_size = var.active_environment == "blue" ? var.min_capacity : 0
max_size = var.active_environment == "blue" ? var.max_capacity : 1
ami_id = var.blue_ami_id
# ...
}
# Green environment
module "green_environment" {
source = "./modules/environment"
name = "green"
instance_count = var.active_environment == "green" ? var.desired_capacity : 1
min_size = var.active_environment == "green" ? var.min_capacity : 0
max_size = var.active_environment == "green" ? var.max_capacity : 1
ami_id = var.green_ami_id
# ...
}
# Load balancer with weighted routing
resource "aws_lb_target_group" "blue" {
name = "blue-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
}
resource "aws_lb_target_group" "green" {
name = "green-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
}
resource "aws_lb_listener_rule" "weighted" {
listener_arn = aws_lb_listener.main.arn
action {
type = "forward"
forward {
target_group {
arn = aws_lb_target_group.blue.arn
weight = var.active_environment == "blue" ? 100 : 0
}
target_group {
arn = aws_lb_target_group.green.arn
weight = var.active_environment == "green" ? 100 : 0
}
}
}
condition {
path_pattern {
values = ["/*"]
}
}
}
Data Layer Separation Pattern
Separating Stateful and Stateless Infrastructure
terraform/
├── data-layer/ # Stateful resources (databases, storage)
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── backend.tf
└── app-layer/ # Stateless resources (compute, networking)
├── main.tf
├── variables.tf
├── outputs.tf
└── backend.tf
# data-layer/main.tf
resource "aws_db_instance" "main" {
allocated_storage = 100
engine = "postgres"
engine_version = "14.7"
instance_class = "db.t3.large"
name = "appdb"
username = var.db_username
password = var.db_password
# Prevent accidental deletion
deletion_protection = true
skip_final_snapshot = false
lifecycle {
prevent_destroy = true
}
}
# data-layer/outputs.tf
output "database_endpoint" {
value = aws_db_instance.main.endpoint
}
# app-layer/main.tf
data "terraform_remote_state" "data_layer" {
backend = "s3"
config = {
bucket = "terraform-state"
key = "data-layer/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_instance" "app" {
# ...
user_data = templatefile("${path.module}/user_data.sh", {
database_endpoint = data.terraform_remote_state.data_layer.outputs.database_endpoint
})
}
Module Composition Pattern
Building Complex Infrastructure from Simple Modules
# Root configuration
module "network" {
source = "./modules/network"
vpc_cidr = var.vpc_cidr
availability_zones = var.availability_zones
public_subnet_cidrs = var.public_subnet_cidrs
private_subnet_cidrs = var.private_subnet_cidrs
tags = local.common_tags
}
module "security" {
source = "./modules/security"
vpc_id = module.network.vpc_id
allowed_cidr_blocks = var.allowed_cidr_blocks
tags = local.common_tags
}
module "compute" {
source = "./modules/compute"
vpc_id = module.network.vpc_id
subnet_ids = module.network.private_subnet_ids
security_group_ids = [module.security.app_security_group_id]
instance_type = var.instance_type
instance_count = var.instance_count
tags = local.common_tags
}
module "load_balancer" {
source = "./modules/load_balancer"
vpc_id = module.network.vpc_id
subnet_ids = module.network.public_subnet_ids
security_group_ids = [module.security.lb_security_group_id]
target_instances = module.compute.instance_ids
tags = local.common_tags
}
module "database" {
source = "./modules/database"
vpc_id = module.network.vpc_id
subnet_ids = module.network.database_subnet_ids
security_group_ids = [module.security.db_security_group_id]
database_name = var.database_name
master_username = var.db_username
master_password = var.db_password
tags = local.common_tags
}
Conditional Resource Creation Pattern
Creating Resources Based on Conditions
# Using count for conditional creation
resource "aws_instance" "bastion" {
count = var.create_bastion ? 1 : 0
ami = data.aws_ami.ubuntu.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public[0].id
tags = {
Name = "bastion-host"
}
}
# Using for_each with conditional map
resource "aws_cloudwatch_log_group" "optional" {
for_each = var.enable_logging ? toset(var.log_group_names) : toset([])
name = each.value
retention_in_days = var.log_retention_days
}
# Conditional module inclusion
module "cdn" {
count = var.enable_cdn ? 1 : 0
source = "./modules/cdn"
origin_domain_name = aws_lb.main.dns_name
# ...
}
# Accessing conditionally created resources
output "bastion_public_ip" {
value = var.create_bastion ? aws_instance.bastion[0].public_ip : null
}
Service Mesh Pattern
Implementing Service Discovery and Mesh
# Service registry
resource "aws_service_discovery_private_dns_namespace" "main" {
name = "internal.example.com"
vpc = aws_vpc.main.id
}
# Service definitions
resource "aws_service_discovery_service" "backend" {
name = "backend"
dns_config {
namespace_id = aws_service_discovery_private_dns_namespace.main.id
dns_records {
ttl = 10
type = "A"
}
routing_policy = "MULTIVALUE"
}
health_check_custom_config {
failure_threshold = 1
}
}
# ECS service with service discovery
resource "aws_ecs_service" "backend" {
name = "backend"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.backend.arn
desired_count = 3
network_configuration {
subnets = aws_subnet.private[*].id
security_groups = [aws_security_group.backend.id]
}
service_registries {
registry_arn = aws_service_discovery_service.backend.arn
}
}
Tagging Strategy Pattern
Comprehensive Tagging Implementation
locals {
# Mandatory tags
mandatory_tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
Owner = var.owner_email
CostCenter = var.cost_center
}
# Optional tags
optional_tags = var.additional_tags
# Combined tags
common_tags = merge(local.mandatory_tags, local.optional_tags)
# Resource-specific tags
database_tags = merge(
local.common_tags,
{
Type = "Database"
Backup = "Required"
Retention = "30days"
}
)
compute_tags = merge(
local.common_tags,
{
Type = "Compute"
AutoScaling = var.enable_autoscaling
PatchSchedule = "Sundays-2AM"
}
)
}
# Apply tags to resources
resource "aws_instance" "web" {
# ...
tags = merge(
local.compute_tags,
{
Name = "${var.project_name}-web-${count.index + 1}"
Role = "WebServer"
}
)
}
resource "aws_db_instance" "main" {
# ...
tags = merge(
local.database_tags,
{
Name = "${var.project_name}-db"
}
)
}
Secret Injection Pattern
Securely Managing Secrets
# Using AWS Secrets Manager
data "aws_secretsmanager_secret" "app_secrets" {
name = "${var.environment}/app/secrets"
}
data "aws_secretsmanager_secret_version" "app_secrets" {
secret_id = data.aws_secretsmanager_secret.app_secrets.id
}
locals {
app_secrets = jsondecode(data.aws_secretsmanager_secret_version.app_secrets.secret_string)
}
# ECS task definition with secrets
resource "aws_ecs_task_definition" "app" {
family = "app"
container_definitions = jsonencode([
{
name = "app"
image = var.app_image
secrets = [
{
name = "DB_PASSWORD"
valueFrom = "${data.aws_secretsmanager_secret.app_secrets.arn}:password::"
},
{
name = "API_KEY"
valueFrom = "${data.aws_secretsmanager_secret.app_secrets.arn}:api_key::"
}
]
environment = [
{
name = "DB_HOST"
value = aws_db_instance.main.endpoint
}
]
}
])
}
# Lambda function with secrets
resource "aws_lambda_function" "processor" {
# ...
environment {
variables = {
DB_HOST = aws_db_instance.main.endpoint
SECRETS_ARN = data.aws_secretsmanager_secret.app_secrets.arn
}
}
}
Auto-Scaling Pattern
Comprehensive Auto-Scaling Configuration
# Launch template
resource "aws_launch_template" "app" {
name_prefix = "${var.project_name}-"
image_id = data.aws_ami.app.id
instance_type = var.instance_type
iam_instance_profile {
name = aws_iam_instance_profile.app.name
}
network_interfaces {
associate_public_ip_address = false
security_groups = [aws_security_group.app.id]
delete_on_termination = true
}
user_data = base64encode(templatefile("${path.module}/user_data.sh", {
environment = var.environment
}))
tag_specifications {
resource_type = "instance"
tags = local.compute_tags
}
}
# Auto Scaling Group
resource "aws_autoscaling_group" "app" {
name = "${var.project_name}-asg"
vpc_zone_identifier = aws_subnet.private[*].id
target_group_arns = [aws_lb_target_group.app.arn]
health_check_type = "ELB"
health_check_grace_period = 300
min_size = var.min_capacity
max_size = var.max_capacity
desired_capacity = var.desired_capacity
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
enabled_metrics = [
"GroupDesiredCapacity",
"GroupInServiceInstances",
"GroupMaxSize",
"GroupMinSize",
"GroupPendingInstances",
"GroupStandbyInstances",
"GroupTerminatingInstances",
"GroupTotalInstances"
]
dynamic "tag" {
for_each = local.compute_tags
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
}
# Target tracking scaling policy - CPU
resource "aws_autoscaling_policy" "cpu_target" {
name = "${var.project_name}-cpu-target"
autoscaling_group_name = aws_autoscaling_group.app.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 70.0
}
}
# Target tracking scaling policy - Request count
resource "aws_autoscaling_policy" "request_count_target" {
name = "${var.project_name}-request-count-target"
autoscaling_group_name = aws_autoscaling_group.app.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ALBRequestCountPerTarget"
resource_label = "${aws_lb.main.arn_suffix}/${aws_lb_target_group.app.arn_suffix}"
}
target_value = 1000.0
}
}
# Scheduled scaling
resource "aws_autoscaling_schedule" "scale_up_morning" {
scheduled_action_name = "scale-up-morning"
min_size = var.min_capacity
max_size = var.max_capacity
desired_capacity = var.desired_capacity * 2
recurrence = "0 8 * * MON-FRI"
autoscaling_group_name = aws_autoscaling_group.app.name
}
resource "aws_autoscaling_schedule" "scale_down_evening" {
scheduled_action_name = "scale-down-evening"
min_size = var.min_capacity
max_size = var.max_capacity
desired_capacity = var.desired_capacity
recurrence = "0 18 * * MON-FRI"
autoscaling_group_name = aws_autoscaling_group.app.name
}
Disaster Recovery Pattern
Multi-Region DR Setup
# Primary region provider
provider "aws" {
alias = "primary"
region = var.primary_region
}
# DR region provider
provider "aws" {
alias = "dr"
region = var.dr_region
}
# Primary region resources
module "primary_infrastructure" {
source = "./modules/infrastructure"
providers = {
aws = aws.primary
}
environment = var.environment
is_primary = true
# ...
}
# DR region resources
module "dr_infrastructure" {
source = "./modules/infrastructure"
providers = {
aws = aws.dr
}
environment = var.environment
is_primary = false
# ...
}
# Route53 health check and failover
resource "aws_route53_health_check" "primary" {
fqdn = module.primary_infrastructure.load_balancer_dns
port = 443
type = "HTTPS"
resource_path = "/health"
failure_threshold = 3
request_interval = 30
}
resource "aws_route53_record" "primary" {
zone_id = aws_route53_zone.main.zone_id
name = "app.example.com"
type = "A"
set_identifier = "primary"
failover_routing_policy {
type = "PRIMARY"
}
alias {
name = module.primary_infrastructure.load_balancer_dns
zone_id = module.primary_infrastructure.load_balancer_zone_id
evaluate_target_health = true
}
health_check_id = aws_route53_health_check.primary.id
}
resource "aws_route53_record" "dr" {
zone_id = aws_route53_zone.main.zone_id
name = "app.example.com"
type = "A"
set_identifier = "dr"
failover_routing_policy {
type = "SECONDARY"
}
alias {
name = module.dr_infrastructure.load_balancer_dns
zone_id = module.dr_infrastructure.load_balancer_zone_id
evaluate_target_health = true
}
}
# Cross-region replication for S3
resource "aws_s3_bucket_replication_configuration" "replication" {
provider = aws.primary
bucket = module.primary_infrastructure.data_bucket_id
role = aws_iam_role.replication.arn
rule {
id = "replicate-all"
status = "Enabled"
destination {
bucket = module.dr_infrastructure.data_bucket_arn
storage_class = "STANDARD_IA"
replication_time {
status = "Enabled"
time {
minutes = 15
}
}
metrics {
status = "Enabled"
event_threshold {
minutes = 15
}
}
}
}
}
# RDS read replica in DR region
resource "aws_db_instance" "read_replica" {
provider = aws.dr
replicate_source_db = module.primary_infrastructure.database_arn
instance_class = var.db_instance_class
publicly_accessible = false
skip_final_snapshot = true
tags = merge(
local.common_tags,
{
Name = "${var.project_name}-db-replica"
Role = "DR"
}
)
}
Cost Optimization Pattern
Implementing Cost Controls
# Use Spot Instances for non-critical workloads
resource "aws_autoscaling_group" "batch_processing" {
# ...
mixed_instances_policy {
instances_distribution {
on_demand_base_capacity = 1
on_demand_percentage_above_base_capacity = 20
spot_allocation_strategy = "capacity-optimized"
}
launch_template {
launch_template_specification {
launch_template_id = aws_launch_template.batch.id
version = "$Latest"
}
override {
instance_type = "t3.medium"
weighted_capacity = 1
}
override {
instance_type = "t3.large"
weighted_capacity = 2
}
}
}
}
# Schedule shutdown for non-production environments
resource "aws_autoscaling_schedule" "shutdown_evening" {
count = var.environment != "production" ? 1 : 0
scheduled_action_name = "shutdown-evening"
min_size = 0
max_size = 0
desired_capacity = 0
recurrence = "0 20 * * *"
autoscaling_group_name = aws_autoscaling_group.app.name
}
resource "aws_autoscaling_schedule" "startup_morning" {
count = var.environment != "production" ? 1 : 0
scheduled_action_name = "startup-morning"
min_size = var.min_capacity
max_size = var.max_capacity
desired_capacity = var.desired_capacity
recurrence = "0 8 * * MON-FRI"
autoscaling_group_name = aws_autoscaling_group.app.name
}
# Use lifecycle policies for S3
resource "aws_s3_bucket_lifecycle_configuration" "data" {
bucket = aws_s3_bucket.data.id
rule {
id = "transition-old-data"
status = "Enabled"
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER"
}
expiration {
days = 365
}
noncurrent_version_transition {
noncurrent_days = 30
storage_class = "STANDARD_IA"
}
noncurrent_version_expiration {
noncurrent_days = 90
}
}
}
# Budget alerts
resource "aws_budgets_budget" "monthly" {
name = "${var.project_name}-monthly-budget"
budget_type = "COST"
limit_amount = var.monthly_budget
limit_unit = "USD"
time_period_start = "2024-01-01_00:00"
time_unit = "MONTHLY"
cost_filter {
name = "TagKeyValue"
values = [
"Project$${var.project_name}",
]
}
notification {
comparison_operator = "GREATER_THAN"
threshold = 80
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = [var.budget_alert_email]
}
notification {
comparison_operator = "GREATER_THAN"
threshold = 100
threshold_type = "PERCENTAGE"
notification_type = "ACTUAL"
subscriber_email_addresses = [var.budget_alert_email]
}
}
Terraform Provider Examples
AWS Provider
Provider Configuration
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0" # Latest: v6.23.0 (Dec 2025)
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = var.project_name
}
}
}
# Additional provider for different region
provider "aws" {
alias = "us_west"
region = "us-west-2"
}
Common AWS Resources
# EC2 Instance
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = aws_subnet.private[0].id
vpc_security_group_ids = [aws_security_group.web.id]
root_block_device {
volume_type = "gp3"
volume_size = 30
encrypted = true
}
user_data = file("${path.module}/user_data.sh")
tags = {
Name = "${var.project_name}-web"
}
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-vpc"
}
}
# Subnet
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-${var.availability_zones[count.index]}"
}
}
# Security Group
resource "aws_security_group" "web" {
name_prefix = "${var.project_name}-web-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.project_name}-web-sg"
}
}
# S3 Bucket
resource "aws_s3_bucket" "data" {
bucket = "${var.project_name}-data-${random_id.bucket_suffix.hex}"
tags = {
Name = "${var.project_name}-data"
}
}
resource "aws_s3_bucket_versioning" "data" {
bucket = aws_s3_bucket.data.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
bucket = aws_s3_bucket.data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
# RDS Database
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-db"
allocated_storage = 100
engine = "postgres"
engine_version = "14.7"
instance_class = var.db_instance_class
db_name = var.database_name
username = var.db_username
password = var.db_password
vpc_security_group_ids = [aws_security_group.database.id]
db_subnet_group_name = aws_db_subnet_group.main.name
backup_retention_period = 7
backup_window = "03:00-04:00"
maintenance_window = "sun:04:00-sun:05:00"
storage_encrypted = true
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${var.project_name}-db-final-snapshot"
tags = {
Name = "${var.project_name}-db"
}
}
# Lambda Function
resource "aws_lambda_function" "processor" {
filename = "lambda_function.zip"
function_name = "${var.project_name}-processor"
role = aws_iam_role.lambda.arn
handler = "index.handler"
source_code_hash = filebase64sha256("lambda_function.zip")
runtime = "python3.11"
timeout = 300
memory_size = 512
environment {
variables = {
ENVIRONMENT = var.environment
TABLE_NAME = aws_dynamodb_table.main.name
}
}
vpc_config {
subnet_ids = aws_subnet.private[*].id
security_group_ids = [aws_security_group.lambda.id]
}
tags = {
Name = "${var.project_name}-processor"
}
}
# Application Load Balancer
resource "aws_lb" "main" {
name = "${var.project_name}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = aws_subnet.public[*].id
enable_deletion_protection = var.environment == "production"
tags = {
Name = "${var.project_name}-alb"
}
}
resource "aws_lb_target_group" "app" {
name = "${var.project_name}-tg"
port = 80
protocol = "HTTP"
vpc_id = aws_vpc.main.id
health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = "/health"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 2
}
tags = {
Name = "${var.project_name}-tg"
}
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
Azure Provider
Provider Configuration
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
provider "azurerm" {
features {
resource_group {
prevent_deletion_if_contains_resources = true
}
key_vault {
purge_soft_delete_on_destroy = false
}
}
}
Common Azure Resources
# Resource Group
resource "azurerm_resource_group" "main" {
name = "${var.project_name}-rg"
location = var.location
tags = {
Environment = var.environment
Project = var.project_name
}
}
# Virtual Network
resource "azurerm_virtual_network" "main" {
name = "${var.project_name}-vnet"
address_space = ["10.0.0.0/16"]
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
tags = azurerm_resource_group.main.tags
}
# Subnet
resource "azurerm_subnet" "app" {
name = "app-subnet"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.0.1.0/24"]
service_endpoints = ["Microsoft.Storage", "Microsoft.Sql"]
}
# Network Security Group
resource "azurerm_network_security_group" "app" {
name = "${var.project_name}-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "AllowHTTP"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "80"
source_address_prefix = "*"
destination_address_prefix = "*"
}
security_rule {
name = "AllowHTTPS"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "*"
destination_address_prefix = "*"
}
tags = azurerm_resource_group.main.tags
}
# Virtual Machine
resource "azurerm_linux_virtual_machine" "app" {
name = "${var.project_name}-vm"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
size = var.vm_size
admin_username = var.admin_username
network_interface_ids = [
azurerm_network_interface.app.id,
]
admin_ssh_key {
username = var.admin_username
public_key = file("~/.ssh/id_rsa.pub")
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}
tags = azurerm_resource_group.main.tags
}
# Storage Account
resource "azurerm_storage_account" "main" {
name = "${var.project_name}storage"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
account_tier = "Standard"
account_replication_type = "GRS"
blob_properties {
versioning_enabled = true
delete_retention_policy {
days = 7
}
}
tags = azurerm_resource_group.main.tags
}
# SQL Database
resource "azurerm_mssql_server" "main" {
name = "${var.project_name}-sqlserver"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "12.0"
administrator_login = var.sql_admin_username
administrator_login_password = var.sql_admin_password
tags = azurerm_resource_group.main.tags
}
resource "azurerm_mssql_database" "main" {
name = "${var.project_name}-db"
server_id = azurerm_mssql_server.main.id
sku_name = "S1"
tags = azurerm_resource_group.main.tags
}
# App Service
resource "azurerm_service_plan" "main" {
name = "${var.project_name}-plan"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
os_type = "Linux"
sku_name = "P1v2"
tags = azurerm_resource_group.main.tags
}
resource "azurerm_linux_web_app" "main" {
name = "${var.project_name}-app"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
service_plan_id = azurerm_service_plan.main.id
site_config {
application_stack {
node_version = "18-lts"
}
always_on = true
}
app_settings = {
"WEBSITE_NODE_DEFAULT_VERSION" = "18-lts"
"ENVIRONMENT" = var.environment
}
tags = azurerm_resource_group.main.tags
}
Google Cloud Provider
Provider Configuration
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 7.0" # Latest: v7.12.0 - includes ephemeral resources & write-only attributes
}
}
}
provider "google" {
project = var.project_id
region = var.region
}
Common GCP Resources
# VPC Network
resource "google_compute_network" "main" {
name = "${var.project_name}-vpc"
auto_create_subnetworks = false
}
# Subnet
resource "google_compute_subnetwork" "app" {
name = "${var.project_name}-subnet"
ip_cidr_range = "10.0.1.0/24"
region = var.region
network = google_compute_network.main.id
secondary_ip_range {
range_name = "pods"
ip_cidr_range = "10.1.0.0/16"
}
secondary_ip_range {
range_name = "services"
ip_cidr_range = "10.2.0.0/16"
}
}
# Firewall Rule
resource "google_compute_firewall" "allow_http" {
name = "${var.project_name}-allow-http"
network = google_compute_network.main.name
allow {
protocol = "tcp"
ports = ["80", "443"]
}
source_ranges = ["0.0.0.0/0"]
target_tags = ["web"]
}
# Compute Instance
resource "google_compute_instance" "web" {
name = "${var.project_name}-web"
machine_type = var.machine_type
zone = var.zone
tags = ["web", var.environment]
boot_disk {
initialize_params {
image = "debian-cloud/debian-11"
size = 20
type = "pd-ssd"
}
}
network_interface {
subnetwork = google_compute_subnetwork.app.id
access_config {
// Ephemeral public IP
}
}
metadata_startup_script = file("${path.module}/startup.sh")
service_account {
email = google_service_account.app.email
scopes = ["cloud-platform"]
}
}
# Cloud Storage Bucket
resource "google_storage_bucket" "data" {
name = "${var.project_id}-${var.project_name}-data"
location = var.region
force_destroy = false
uniform_bucket_level_access = true
versioning {
enabled = true
}
lifecycle_rule {
condition {
age = 30
}
action {
type = "SetStorageClass"
storage_class = "NEARLINE"
}
}
lifecycle_rule {
condition {
age = 90
}
action {
type = "SetStorageClass"
storage_class = "COLDLINE"
}
}
}
# Cloud SQL Instance
resource "google_sql_database_instance" "main" {
name = "${var.project_name}-db"
database_version = "POSTGRES_14"
region = var.region
settings {
tier = var.db_tier
availability_type = "REGIONAL"
disk_size = 100
disk_type = "PD_SSD"
backup_configuration {
enabled = true
start_time = "03:00"
point_in_time_recovery_enabled = true
transaction_log_retention_days = 7
backup_retention_settings {
retained_backups = 30
}
}
ip_configuration {
ipv4_enabled = false
private_network = google_compute_network.main.id
}
}
deletion_protection = true
}
# GKE Cluster
resource "google_container_cluster" "primary" {
name = "${var.project_name}-gke"
location = var.region
remove_default_node_pool = true
initial_node_count = 1
network = google_compute_network.main.name
subnetwork = google_compute_subnetwork.app.name
ip_allocation_policy {
cluster_secondary_range_name = "pods"
services_secondary_range_name = "services"
}
workload_identity_config {
workload_pool = "${var.project_id}.svc.id.goog"
}
addons_config {
http_load_balancing {
disabled = false
}
horizontal_pod_autoscaling {
disabled = false
}
}
}
resource "google_container_node_pool" "primary_nodes" {
name = "${var.project_name}-node-pool"
location = var.region
cluster = google_container_cluster.primary.name
node_count = var.node_count
autoscaling {
min_node_count = 1
max_node_count = 10
}
node_config {
machine_type = var.node_machine_type
disk_size_gb = 100
disk_type = "pd-standard"
oauth_scopes = [
"https://www.googleapis.com/auth/cloud-platform"
]
labels = {
environment = var.environment
project = var.project_name
}
tags = ["gke-node", var.environment]
}
}
# Cloud Function
resource "google_cloudfunctions_function" "processor" {
name = "${var.project_name}-processor"
description = "Process data from storage"
runtime = "python311"
available_memory_mb = 256
source_archive_bucket = google_storage_bucket.functions.name
source_archive_object = google_storage_bucket_object.function_code.name
trigger_http = true
entry_point = "process"
environment_variables = {
ENVIRONMENT = var.environment
PROJECT_ID = var.project_id
}
}
Kubernetes Provider
Provider Configuration
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.23"
}
}
}
provider "kubernetes" {
host = var.cluster_endpoint
cluster_ca_certificate = base64decode(var.cluster_ca_cert)
token = var.cluster_token
}
Common Kubernetes Resources
# Namespace
resource "kubernetes_namespace" "app" {
metadata {
name = var.namespace
labels = {
name = var.namespace
environment = var.environment
}
}
}
# Deployment
resource "kubernetes_deployment" "app" {
metadata {
name = "${var.app_name}-deployment"
namespace = kubernetes_namespace.app.metadata[0].name
labels = {
app = var.app_name
environment = var.environment
}
}
spec {
replicas = var.replica_count
selector {
match_labels = {
app = var.app_name
}
}
template {
metadata {
labels = {
app = var.app_name
environment = var.environment
}
}
spec {
container {
name = var.app_name
image = "${var.image_repository}:${var.image_tag}"
port {
container_port = var.container_port
}
env {
name = "ENVIRONMENT"
value = var.environment
}
resources {
limits = {
cpu = "500m"
memory = "512Mi"
}
requests = {
cpu = "250m"
memory = "256Mi"
}
}
liveness_probe {
http_get {
path = "/health"
port = var.container_port
}
initial_delay_seconds = 30
period_seconds = 10
}
readiness_probe {
http_get {
path = "/ready"
port = var.container_port
}
initial_delay_seconds = 10
period_seconds = 5
}
}
}
}
}
}
# Service
resource "kubernetes_service" "app" {
metadata {
name = "${var.app_name}-service"
namespace = kubernetes_namespace.app.metadata[0].name
}
spec {
selector = {
app = var.app_name
}
port {
port = 80
target_port = var.container_port
}
type = "LoadBalancer"
}
}
# ConfigMap
resource "kubernetes_config_map" "app" {
metadata {
name = "${var.app_name}-config"
namespace = kubernetes_namespace.app.metadata[0].name
}
data = {
"config.json" = jsonencode({
environment = var.environment
log_level = var.log_level
})
}
}
# Secret
resource "kubernetes_secret" "app" {
metadata {
name = "${var.app_name}-secret"
namespace = kubernetes_namespace.app.metadata[0].name
}
data = {
database_password = base64encode(var.database_password)
api_key = base64encode(var.api_key)
}
type = "Opaque"
}
# Ingress
resource "kubernetes_ingress_v1" "app" {
metadata {
name = "${var.app_name}-ingress"
namespace = kubernetes_namespace.app.metadata[0].name
annotations = {
"kubernetes.io/ingress.class" = "nginx"
"cert-manager.io/cluster-issuer" = "letsencrypt-prod"
"nginx.ingress.kubernetes.io/ssl-redirect" = "true"
}
}
spec {
tls {
hosts = [var.domain_name]
secret_name = "${var.app_name}-tls"
}
rule {
host = var.domain_name
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service.app.metadata[0].name
port {
number = 80
}
}
}
}
}
}
}
}
# Horizontal Pod Autoscaler
resource "kubernetes_horizontal_pod_autoscaler_v2" "app" {
metadata {
name = "${var.app_name}-hpa"
namespace = kubernetes_namespace.app.metadata[0].name
}
spec {
scale_target_ref {
api_version = "apps/v1"
kind = "Deployment"
name = kubernetes_deployment.app.metadata[0].name
}
min_replicas = 2
max_replicas = 10
metric {
type = "Resource"
resource {
name = "cpu"
target {
type = "Utilization"
average_utilization = 70
}
}
}
metric {
type = "Resource"
resource {
name = "memory"
target {
type = "Utilization"
average_utilization = 80
}
}
}
}
}
Terraform Best Practices
Project Structure
Standard Project Layout
terraform-project/
├── main.tf # Primary resource definitions
├── variables.tf # Input variable declarations
├── outputs.tf # Output value declarations
├── versions.tf # Terraform and provider version constraints
├── terraform.tfvars # Variable values (gitignored if sensitive)
├── backend.tf # Backend configuration (optional)
├── locals.tf # Local values (optional)
├── data.tf # Data source definitions (optional)
└── modules/ # Local modules (optional)
└── networking/
├── main.tf
├── variables.tf
└── outputs.tf
Multi-Environment Structure
terraform-project/
├── modules/ # Reusable modules
│ └── vpc/
├── environments/ # Environment-specific configurations
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── staging/
│ └── production/
└── shared/ # Shared resources
Naming Conventions
Resource Naming
- Use snake_case for all names
- Be descriptive but concise
- Include resource type when helpful
- Avoid redundant prefixes
# Good
resource "aws_instance" "web_server" {}
resource "aws_security_group" "web_server_sg" {}
# Avoid
resource "aws_instance" "aws_instance_web" {}
resource "aws_security_group" "sg" {}
Variable Naming
- Use descriptive names
- Include units in name when applicable
- Use consistent naming across modules
# Good
variable "instance_count" {}
variable "backup_retention_days" {}
variable "enable_encryption" {}
# Avoid
variable "count" {}
variable "retention" {}
variable "encrypt" {}
Version Pinning
Terraform Version
terraform {
required_version = ">= 1.8, < 2.0" # Use 1.8+ for modern features
}
Provider Versions
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0" # Allow patch versions
}
random = {
source = "hashicorp/random"
version = "3.5.1" # Pin exact version if needed
}
}
}
Module Versions
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0" # Always pin module versions
# ...
}
State Management
Remote Backend Configuration
# Modern S3 backend with native locking (Terraform 1.11+)
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "project/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true # S3-native locking (recommended for 1.11+)
kms_key_id = "alias/terraform-state"
}
}
# Legacy S3 backend with DynamoDB locking (Terraform < 1.11)
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "project/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-locks" # Deprecated in 1.11+
kms_key_id = "alias/terraform-state"
}
}
State Locking
Always use state locking for remote backends:
- S3: Use
use_lockfile = true(Terraform 1.11+) or DynamoDB table (legacy) - Azure Storage: Built-in locking
- GCS: Built-in locking
Variable Management
Variable Definitions
variable "instance_type" {
description = "EC2 instance type for web servers"
type = string
default = "t3.micro"
validation {
condition = can(regex("^t[23]\\.", var.instance_type))
error_message = "Instance type must be from t2 or t3 family."
}
}
variable "tags" {
description = "Common tags to apply to all resources"
type = map(string)
default = {}
}
variable "availability_zones" {
description = "List of availability zones"
type = list(string)
validation {
condition = length(var.availability_zones) >= 2
error_message = "At least 2 availability zones required."
}
}
Sensitive Variables
variable "database_password" {
description = "Password for database admin user"
type = string
sensitive = true
}
output "connection_string" {
value = "postgresql://user:${var.database_password}@${aws_db_instance.main.endpoint}"
sensitive = true
}
Variable Precedence
- Environment variables (
TF_VAR_name) terraform.tfvarsfileterraform.tfvars.jsonfile*.auto.tfvarsfiles (alphabetical order)-varand-var-filecommand-line flags- Default values in variable declarations
Resource Management
Dependencies
# Implicit dependency (preferred)
resource "aws_eip" "example" {
instance = aws_instance.web.id # Implicit dependency
}
# Explicit dependency (when needed)
resource "aws_instance" "web" {
# ...
depends_on = [
aws_iam_role_policy.example
]
}
Lifecycle Rules
resource "aws_instance" "web" {
# ...
lifecycle {
create_before_destroy = true # Create replacement before destroying
prevent_destroy = true # Prevent accidental deletion
ignore_changes = [ # Ignore external changes
tags["LastModified"],
user_data,
]
}
}
Provisioners (Use Sparingly)
resource "aws_instance" "web" {
# ...
provisioner "local-exec" {
command = "echo ${self.private_ip} >> private_ips.txt"
on_failure = continue # Continue if provisioner fails
}
provisioner "remote-exec" {
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
inline = [
"sudo apt-get update",
"sudo apt-get install -y nginx",
]
}
}
Data Sources
Using Data Sources
# Fetch latest AMI
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Reference in resource
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
}
# Fetch availability zones
data "aws_availability_zones" "available" {
state = "available"
}
# Use in resources
resource "aws_subnet" "private" {
count = length(data.aws_availability_zones.available.names)
availability_zone = data.aws_availability_zones.available.names[count.index]
# ...
}
Local Values
Using Locals
locals {
# Common tags
common_tags = {
Environment = var.environment
Project = var.project_name
ManagedBy = "Terraform"
CostCenter = var.cost_center
}
# Computed values
az_count = length(data.aws_availability_zones.available.names)
subnet_count = var.subnet_count != null ? var.subnet_count : local.az_count
# Complex expressions
instance_name = "${var.project_name}-${var.environment}-web"
# Conditional values
instance_type = var.environment == "production" ? "t3.large" : "t3.micro"
# Map transformations
subnet_cidrs = {
for idx, az in data.aws_availability_zones.available.names :
az => cidrsubnet(var.vpc_cidr, 8, idx)
}
}
Dynamic Blocks
Dynamic Block Patterns
# Dynamic ingress rules
resource "aws_security_group" "web" {
name_prefix = "web-"
vpc_id = aws_vpc.main.id
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
}
}
tags = local.common_tags
}
# Variable definition
variable "ingress_rules" {
type = list(object({
description = string
from_port = number
to_port = number
protocol = string
cidr_blocks = list(string)
}))
default = [
{
description = "HTTP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
},
{
description = "HTTPS"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
]
}
Count and For_Each
Using Count
# Create multiple similar resources
resource "aws_instance" "web" {
count = var.instance_count
ami = data.aws_ami.ubuntu.id
instance_type = var.instance_type
subnet_id = element(aws_subnet.private[*].id, count.index)
tags = merge(
local.common_tags,
{
Name = "${local.instance_name}-${count.index + 1}"
}
)
}
Using For_Each
# Create resources from map
resource "aws_iam_user" "users" {
for_each = toset(var.user_names)
name = each.value
tags = {
Team = lookup(var.user_teams, each.value, "default")
}
}
# Create resources with different configurations
variable "environments" {
type = map(object({
instance_type = string
instance_count = number
}))
default = {
dev = {
instance_type = "t3.micro"
instance_count = 1
}
prod = {
instance_type = "t3.large"
instance_count = 3
}
}
}
resource "aws_instance" "env_servers" {
for_each = var.environments
ami = data.aws_ami.ubuntu.id
instance_type = each.value.instance_type
count = each.value.instance_count
tags = {
Name = "${each.key}-server"
Environment = each.key
}
}
Module Best Practices
Module Structure
module/
├── main.tf # Main resources
├── variables.tf # Input variables
├── outputs.tf # Output values
├── versions.tf # Version constraints
├── README.md # Documentation
└── examples/ # Usage examples
└── complete/
├── main.tf
└── variables.tf
Module Input Variables
# modules/vpc/variables.tf
variable "name" {
description = "Name to be used on all resources"
type = string
}
variable "cidr" {
description = "CIDR block for VPC"
type = string
validation {
condition = can(cidrhost(var.cidr, 0))
error_message = "Must be valid IPv4 CIDR."
}
}
variable "enable_nat_gateway" {
description = "Should be true to provision NAT Gateways"
type = bool
default = true
}
variable "tags" {
description = "A map of tags to add to all resources"
type = map(string)
default = {}
}
Module Outputs
# 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
}
output "private_subnet_ids" {
description = "List of IDs of private subnets"
value = aws_subnet.private[*].id
}
output "public_subnet_ids" {
description = "List of IDs of public subnets"
value = aws_subnet.public[*].id
}
Using Modules
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.1.0"
name = "${var.project_name}-vpc"
cidr = "10.0.0.0/16"
azs = data.aws_availability_zones.available.names
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
enable_vpn_gateway = false
tags = local.common_tags
}
# Reference module outputs
resource "aws_instance" "web" {
subnet_id = module.vpc.private_subnet_ids[0]
# ...
}
Security Best Practices
Secrets Management
# NEVER hardcode secrets
# BAD
resource "aws_db_instance" "database" {
password = "supersecretpassword" # NEVER DO THIS
}
# GOOD - Use variables
variable "db_password" {
type = string
sensitive = true
}
resource "aws_db_instance" "database" {
password = var.db_password
}
# BETTER - Use secrets management service
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/database/password"
}
resource "aws_db_instance" "database" {
password = jsondecode(data.aws_secretsmanager_secret_version.db_password.secret_string)["password"]
}
Encryption
# Enable encryption by default
resource "aws_s3_bucket" "data" {
bucket = "my-data-bucket"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
bucket = aws_s3_bucket.data.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.data.arn
}
}
}
# Encrypt EBS volumes
resource "aws_instance" "web" {
# ...
root_block_device {
encrypted = true
kms_key_id = aws_kms_key.data.arn
volume_type = "gp3"
}
}
IAM Policies
# Use least privilege principle
data "aws_iam_policy_document" "lambda_execution" {
statement {
effect = "Allow"
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = [
"arn:aws:logs:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/${var.function_name}:*"
]
}
statement {
effect = "Allow"
actions = [
"s3:GetObject"
]
resources = [
"${aws_s3_bucket.data.arn}/*"
]
}
}
resource "aws_iam_policy" "lambda_execution" {
name = "${var.function_name}-execution"
policy = data.aws_iam_policy_document.lambda_execution.json
}
Testing and Validation
Input Validation
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "instance_count" {
type = number
validation {
condition = var.instance_count > 0 && var.instance_count <= 10
error_message = "Instance count must be between 1 and 10."
}
}
Pre-commit Hooks
Use terraform fmt and terraform validate in pre-commit hooks:
#!/bin/bash
# .git/hooks/pre-commit
terraform fmt -check -recursive || exit 1
terraform validate || exit 1
Documentation
Code Comments
# Create VPC for application infrastructure
# This VPC uses a /16 CIDR block to accommodate multiple subnets
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true # Required for ECS task networking
enable_dns_support = true
tags = merge(
local.common_tags,
{
Name = "${var.project_name}-vpc"
}
)
}
README Documentation
Include in project README:
- Purpose of the infrastructure
- Prerequisites
- Required variables
- Usage examples
- Output descriptions
- How to run terraform commands
- Maintenance notes
Performance Optimization
Parallel Resource Creation
Terraform automatically parallelizes resource creation when possible. Help it by:
- Avoiding unnecessary dependencies
- Using data sources efficiently
- Structuring modules properly
State File Optimization
- Use targeted operations when possible:
terraform apply -target=resource - Split large configurations into multiple state files
- Use workspaces for similar environments
- Consider using
-refresh=falsewhen appropriate
Provider Plugin Caching
# ~/.terraformrc or terraform.rc
plugin_cache_dir = "$HOME/.terraform.d/plugin-cache"
Common Pitfalls to Avoid
- Hardcoding values - Use variables and data sources
- Not pinning versions - Always pin provider and module versions
- Ignoring state - Never edit state files manually
- Circular dependencies - Structure resources properly
- Overly complex modules - Keep modules focused and simple
- Not using remote state - Always use remote state for team collaboration
- Forgetting state locking - Always use state locking mechanism
- Mixing concerns - Separate infrastructure layers (network, compute, data)
- Not validating inputs - Use validation blocks for variables
- Ignoring costs - Tag resources appropriately for cost tracking