Terraform / Infrastructure as Code
Purpose: Best practices for provisioning cloud resources with Terraform, covering code style, file layout, naming, state management, modules, security, and testing.
When to Use This Skill
- Writing or editing
.tfor.tfvarsfiles - Creating reusable Terraform modules
- Managing remote state backends
- Securing infrastructure definitions
- Running Terratest or
terraform validate/terraform plan
Decision Tree
Terraform Decision
+-- New infrastructure project?
| +-- Azure only? -> Consider Bicep as alternative
| +-- Multi-cloud or AWS/GCP? -> Use Terraform
| +-- Existing Terraform codebase? -> Use Terraform
+-- Structuring code?
| +-- Single environment? -> One root module with terraform.tfvars
| +-- Multiple environments? -> Workspaces or separate root dirs per env
| +-- Shared patterns? -> Extract reusable modules/ with versioning
+-- Managing state?
| +-- Team project? -> Remote backend with locking (S3/Azure Storage/GCS)
| +-- Solo developer? -> Remote backend still recommended
| +-- Never -> Local .tfstate in production
+-- Validating changes?
| +-- Syntax? -> terraform validate
| +-- Drift detection? -> terraform plan
| +-- Integration tests? -> Terratest (Go)
Code Style
- Use Terraform 1.5+ features (import blocks,
checkblocks,movedblocks) - Maximum line length: 120 characters
- Use
terraform fmtfor formatting (enforced via pre-commit) - Use
tflintfor linting
File Organization
infra/
+-- main.tf # Provider config, data sources
+-- variables.tf # Input variable declarations
+-- outputs.tf # Output value declarations
+-- terraform.tfvars # Variable values (gitignored in prod)
+-- locals.tf # Local values and computed expressions
+-- versions.tf # Required providers and versions
-- modules/
-- networking/ # Reusable module
+-- main.tf
+-- variables.tf
-- outputs.tf
Naming Conventions
| Resource | Convention | Example |
|---|---|---|
| Resource names | snake_case | azurerm_resource_group.main |
| Variable names | snake_case | var.resource_group_name |
| Output names | snake_case | output.storage_account_id |
| Module names | kebab-case dirs | modules/app-service/ |
| Tags | PascalCase keys | Environment = "Production" |
Resource Definitions
# MUST: Use descriptive resource names (not generic "this" or "main")
resource "azurerm_resource_group" "app_rg" {
name = "${var.project_name}-${var.environment}-rg"
location = var.location
tags = local.common_tags
}
# MUST: Pin provider versions
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
}
}
Variables
# MUST: Add description, type, and validation to all variables
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."
}
}
# MUST: Use sensitive = true for secrets
variable "db_password" {
description = "Database administrator password"
type = string
sensitive = true
}
# SHOULD: Provide defaults where safe
variable "location" {
description = "Azure region for resources"
type = string
default = "eastus2"
}
State Management
- MUST use remote state backend (Azure Storage, S3, GCS) - never local state in production
- MUST enable state locking
- MUST NOT commit
.tfstatefiles or.tfvarswith secrets
# Remote state backend (Azure)
backend "azurerm" {
resource_group_name = "rg-terraform-state"
storage_account_name = "stterraformstate"
container_name = "tfstate"
key = "prod.terraform.tfstate"
}
Modules
- SHOULD create reusable modules for repeated patterns
- MUST version-pin module sources
- MUST document every module input/output
module "app_service" {
source = "./modules/app-service"
name = "${var.project_name}-${var.environment}"
resource_group_name = azurerm_resource_group.app_rg.name
location = var.location
sku_name = var.environment == "prod" ? "P1v3" : "B1"
tags = local.common_tags
}
Security
- MUST NOT hardcode secrets in
.tfor.tfvarsfiles - MUST use Key Vault references or
sensitivevariables - SHOULD use managed identity instead of service principal keys
- MUST enable HTTPS and encryption at rest for all applicable resources
- SHOULD run
checkovortfsecfor security scanning
Testing
- Use
terraform validatefor syntax checking - Use
terraform planfor drift detection - Use Terratest (Go) for integration testing
- Name test files:
*_test.go
Core Rules
- Pin Provider Versions - Always specify
version = "~> X.0"in required_providers; never use unversioned providers - Remote State Only - Use a remote backend with locking for all environments; never commit
.tfstatefiles - Validate All Variables - Every variable MUST have
description,type, andvalidationblocks where applicable - Mark Secrets Sensitive - Use
sensitive = trueon all password, key, and token variables - Use Modules for Reuse - Extract repeated resource patterns into
modules/with documented inputs and outputs - Format and Lint - Run
terraform fmtandtflintbefore every commit; enforce via pre-commit hooks - Plan Before Apply - Always run
terraform planand review changes beforeterraform applyin CI/CD - Descriptive Resource Names - Use meaningful snake_case resource names (e.g.,
app_rg), not genericthisormain
Anti-Patterns
- Local State in Production: Using local
.tfstatefor team projects -> Use remote backend with locking - Hardcoded Secrets: Passwords or keys in
.tf/.tfvarsfiles -> Use Key Vault references orsensitivevariables - Unpinned Providers: No version constraint on providers -> Pin with
~>operator to major version - No Validation Blocks: Accepting any input without checks -> Add
validation {}blocks to variables - Monolithic Root Module: All resources in one
main.tf-> Split into logical files and extract modules - Apply Without Plan: Running
terraform applywithout reviewing plan output -> Always plan first, review diff
Source: jnPiyush/AgentX — distributed by TomeVault.