Terraform Skill
Anton BabenkoCommunityCreated by Anton Babenko
- Skill
- Tool integration
- Cloud and DevOps
- Apache-2.0
- Medium risk
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
This skill provides a structured workflow for managing Terraform and OpenTofu configurations. It emphasizes a diagnose-before-generate approach, requiring explicit context such as runtime versions, provider details, and environment criticality. It includes a response contract to ensure risk-aware planning, validation, and rollback procedures, while offering specific guidance on module hierarchy, state management, security, and testing strategies.
What it does
- Diagnoses infrastructure failure modes like identity churn, state corruption, and secret exposure.
- Enforces a response contract requiring version floors, risk assessment, and validation plans.
- Provides patterns for module development, directory layout, and naming conventions.
- Offers guidance on CI/CD pipeline stages, including plan artifact review and drift prevention.
- Facilitates safe state management, including backend selection and locking protocols.
- Supports semantic code navigation using terraform-ls or fallback search tools.
When to use it
- When writing or reviewing Terraform/OpenTofu modules and configurations.
- When debugging CI/CD pipeline failures or infrastructure drift.
- When planning complex refactors or state migrations.
- When configuring remote state backends or implementing security compliance checks.
Advantages
- Reduces risk of destructive changes through mandatory plan-destroy protocols.
- Improves code maintainability via standardized module and variable contracts.
- Ensures consistent security practices through integrated policy and secret handling.
- Provides clear version-aware guidance to prevent common configuration errors.
Usage examples
- “I am refactoring a module and worried about resource identity churn. How should I proceed?”
- The agent will guide you through using 'moved' blocks, comparing 'count' vs 'for_each' patterns, and validating the plan to ensure no unexpected resource destruction occurs.
- “How do I securely manage secrets in my Terraform configuration?”
- The agent will advise on using cloud-native secret managers, 'sensitive' variable flags, or 'write_only' arguments, and will check your implementation against security best practices.
- “Help me set up a CI/CD pipeline for my infrastructure.”
- The agent will provide a structured pipeline template including validation, testing, plan artifact generation, and environment-protected apply stages.
- “My state file is locked and I cannot run any operations. What should I do?”
- The agent will diagnose the state backend, verify locking mechanisms, and provide recovery steps based on your specific provider and backend configuration.
Install
Download the skill as a ZIP:
https://autogineer.com/agent-skills/antonbabenko/terraform-skill/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip terraform-skill.zip -d .agents/skills
Documentation by Anton Babenko
Terraform Skill for Claude
Diagnose-first guidance for Terraform and OpenTofu. Core file is a workflow; depth lives in references loaded on demand.
Response Contract
Every Terraform/OpenTofu response must include:
Assumptions & version floor — runtime (
terraformortofu), exact version, providers, state backend, execution path (local/CI/Cloud/Atlantis), environment criticality. State assumptions explicitly if the user did not provide them.Risk category addressed — one or more of: identity churn, secret exposure, blast radius, CI drift, compliance gaps, state corruption, provider upgrade risk, testing blind spots.
Chosen remediation & tradeoffs — what was chosen, what was traded off, why.
Validation plan — exact commands (
fmt -check,validate,plan -out, policy check) tailored to runtime and risk tier.Rollback notes — for any destructive or state-mutating change: how to undo, what evidence to keep.
Never recommend direct production apply without a reviewed plan artifact and approval.
Never run terraform destroy (targeted or full) without first running terraform plan -destroy and showing the user every resource that will be deleted — including implicit dependents pulled in via locals or for_each. Get explicit confirmation before proceeding. Never use -auto-approve on destroy.
Workflow
Capture execution context — runtime+version, provider(s), backend, execution path, environment criticality.
Diagnose failure mode(s) using the routing table below. If intent spans categories, load both references.
Load only the matching reference file(s) — do not preload depth the task does not need.
Propose fix with risk controls — why this addresses the mode, what could still go wrong, guardrails (tests/approvals/rollback).
Generate artifacts — HCL, migration blocks (
moved,import), CI changes, policy rules.Validate before finalizing — run validation commands tailored to risk tier.
Emit the Response Contract at the end.
Diagnose Before You Generate
| Failure category | Symptoms | Primary references |
|---|---|---|
| Identity churn | Resource addresses shift after refactor, count index churn, missing moved blocks | Code Patterns: count vs for_each, Code Patterns: moved blocks, Code Patterns: LLM mistakes |
| Secret exposure | Secrets in defaults, state, logs, CI artifacts | Security & Compliance, Code Patterns: write-only, State Management |
| Blast radius | Oversized stacks, shared prod/non-prod state, unsafe applies | State Management, Module Patterns |
| Destroy cascade | Targeted destroy deletes more than expected; locals referencing a targeted resource make all for_each consumers implicit dependents | Response Contract: plan-destroy first; State Management: Safe Destroy |
| CI drift | Local plan ≠ CI plan, apply without reviewed artifact, unpinned versions | CI/CD Workflows, Code Patterns: versions |
| Compliance gaps | Missing policy stage, no approval model, no evidence retention | Security & Compliance, CI/CD Workflows |
| Testing blind spots | Plan-only validation of computed values, set-type indexing, mock/real confusion | Testing Frameworks |
| State corruption / recovery | Stuck lock, backend migration, drift reconciliation | State Management |
| Provider upgrade risk | Breaking-change provider bump, unpinned modules | Code Patterns: versions, Module Patterns |
| Provider lifecycle | Removing a provider with resources still in state, orphaned resources, removed block usage | State Management: Provider Removal |
| Bootstrap / orchestration misuse | null_resource + local-exec for bootstrap, remote-exec for setup scripts, provisioner stdout leaking secrets in CI logs | Code Patterns: Provisioners as Last Resort |
| Navigation / safe-rename blind spots | Cannot locate symbol defs/refs semantically, value-symbol rename done as blind text replace, grep-only refactor missing refs, hallucinated rg shim | Code Intelligence |
| Cross-cloud / provider mapping | "What's the Azure/GCP equivalent of X", picking a backend/auth model per cloud | State Management: Cross-cloud equivalents |
When to Use This Skill
Activate when: creating or reviewing Terraform/OpenTofu configurations or modules, setting up or debugging tests, structuring multi-environment deployments, implementing IaC CI/CD, choosing module patterns or state organization, configuring or migrating remote state backends.
Don't use for: basic HCL syntax questions Claude already knows, provider API reference (link to docs), cloud-platform questions unrelated to Terraform/OpenTofu.
Core Principles
Module Hierarchy
| Type | When to Use | Scope |
|---|---|---|
| Resource module | Single logical group of connected resources | VPC + subnets, SG + rules |
| Infrastructure module | Collection of resource modules for a purpose | Multiple resource modules in one region/account |
| Composition | Complete infrastructure | Spans multiple regions/accounts |
Flow: resource → resource module → infrastructure module → composition.
Directory Layout
environments/ # prod/ staging/ dev/ — per-env configurations
modules/ # networking/ compute/ data/ — reusable modules
examples/ # minimal/ complete/ — docs + integration fixturesSeparate environments from modules. Use examples/ as both documentation and test fixtures. Keep modules small and single-responsibility.
See Module Patterns for architecture principles, naming conventions, variable/output contracts.
Naming Conventions (summary)
Descriptive resource names (
aws_instance.web_server, notaws_instance.main)Reserve
thisfor genuine singleton resources onlyPrefix variables with context (
vpc_cidr_block, notcidr)Standard files:
main.tf,variables.tf,outputs.tf,versions.tf
See Module Patterns: Variable Naming and Code Patterns: Block Ordering for examples.
Block Ordering (summary)
Resource blocks: count/for_each first → arguments → tags → depends_on → lifecycle.
Variable blocks: description → type → default → validation → nullable → sensitive.
See Code Patterns: Block Ordering & Structure for the full rules and examples.
Testing Strategy
Decision Matrix: Which Testing Approach?
| Situation | Approach | Tools | Cost |
|---|---|---|---|
| Quick syntax check | Static analysis | validate, fmt | Free |
| Pre-commit validation | Static + lint | validate, tflint, trivy, checkov | Free |
| Terraform 1.6+, simple logic | Native test framework | 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 + mocks | Free |
| Multi-cloud, complex | Full integration | Terratest + real infra | Med-High |
Native Test Rules (1.6+)
Before writing test code: validate resource schemas via Terraform MCP so assertions target real attributes.
command = plan— fast, for input-derived values onlycommand = apply— required for computed values (ARNs, generated names) and set-type nested blocksSet-type blocks cannot be indexed with
[0]— useforexpressions or materialize viacommand = applyCommon set types: S3 encryption rules, lifecycle transitions, IAM policy statements
See Testing Frameworks for static-analysis pipelines, native-test patterns, Terratest integration, mock providers, and the full LLM-mistake checklist.
Count vs For_Each — Quick Rule
| Scenario | Use | Why |
|---|---|---|
| Boolean condition (create / don't) | count = condition ? 1 : 0 | Optional singleton toggle |
| Items may be reordered or removed | for_each = toset(list) | Stable resource addresses |
| Reference by key | for_each = map | Named access |
| Multiple named resources | for_each | Better identity stability |
Never use list index as long-lived identity — removing a middle element reshuffles every address after it. For the decision matrix, safe migration playbook, moved block patterns, and known-at-plan failure cases, see Code Patterns: count vs for_each.
Locals for Dependency Management
Using try() in a local to prefer a conditional resource's attribute over its parent is a specialized but high-value pattern — it forces correct deletion order without explicit depends_on. Common use: VPC + secondary CIDR associations + subnets.
See Code Patterns: Locals for Dependency Management for the full pattern and worked example.
Module Development
Standard layout:
my-module/
├── README.md # Usage documentation
├── main.tf # Primary resources
├── variables.tf # Typed inputs with descriptions
├── outputs.tf # Output values
├── versions.tf # required_version + required_providers
├── examples/
│ ├── minimal/
│ └── complete/
└── tests/
└── module_test.tftest.hcl # or Go for TerratestVariable contracts: always description, always explicit type, use validation for complex constraints, use sensitive = true for secrets, prefer optional() with typed defaults (1.3+) over untyped map(any).
Output contracts: always description, mark sensitive outputs, expose stable subsets (not whole provider objects).
See Module Patterns for the full contract patterns, module release checklist, and LLM-mistake checklist.
CI/CD
Pipeline stages: validate → test → plan → apply (with environment protection).
Cost control: mock providers on PR validation, real-cloud integration only on main or scheduled, tag test resources, auto-cleanup.
Drift prevention: pin runtime and providers, commit .terraform.lock.hcl, apply the reviewed plan artifact from the plan stage (do not re-run plan inside the apply job), run policy/security stage on every path to apply.
See CI/CD Workflows for GitHub Actions, GitLab CI, and Atlantis templates plus the LLM-mistake checklist.
Security & Compliance
Essential checks:
trivy config .
checkov -d .Don't: store secrets in variables or .tfvars, use default VPC, skip encryption, open security groups to 0.0.0.0/0, use inline ingress/egress blocks in aws_security_group.
Do: source secrets from a cloud secret manager (AWS Secrets Manager / Azure Key Vault / GCP Secret Manager) or use write_only arguments on 1.11+, create dedicated VPCs, enforce encryption at rest and TLS, least-privilege SGs, use separate aws_vpc_security_group_{ingress,egress}_rule resources (e.g. AWS provider v5+).
Marking a variable sensitive = true masks display only — the value still lives in state. Use write_only / *_wo on 1.11+, or keep secret material out of Terraform entirely via runtime lookups.
See Security & Compliance for trivy/checkov pipelines, state-file hardening, compliance mappings, and the LLM-mistake checklist.
State Management
Never use local state in teams or production. Remote backends provide automatic locking, encryption, versioning, audit logging, and safe collaboration.
Choosing a Remote Backend
AWS example (Azure azurerm / GCP gcs / TF Cloud syntax: see State Management: Choosing a Remote Backend):
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/vpc/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true # Native S3 locking, 1.10+
}
}On Terraform < 1.10, use dynamodb_table = "terraform-state-lock" instead of use_lockfile. Azure Storage, GCS, and Terraform Cloud all offer built-in locking - see the State Management reference for syntax. For choosing among backends and their locking models, see Choosing a Remote Backend.
State Organization
| Pattern | Use When | Example Path |
|---|---|---|
| Per environment | Different teams per env | prod/terraform.tfstate, staging/... |
| Per component | Independent lifecycles | prod/vpc/, prod/eks/, prod/rds/ |
| Hybrid (recommended) | Both benefits | prod/networking/, prod/compute/, staging/networking/ |
Split state when: different teams, different update cadences, or >500 resources. Combine when: tightly coupled resources, <100 resources, same lifecycle.
See State Management for locking, migration, multi-team isolation, disaster recovery, and the LLM-mistake checklist.
Version Management
| Component | Strategy | Example |
|---|---|---|
| Terraform runtime | Pin minor | required_version = "~> 1.9" |
| Providers | Pin major | version = "~> 5.0" |
| Modules (prod) | Pin exact | version = "5.1.2" |
| Modules (dev) | Allow patch | version = "~> 5.1" |
Commit .terraform.lock.hcl intentionally. Keep provider/runtime upgrades in a separate PR from functional changes. See Code Patterns: Version Management for constraint syntax and upgrade workflow.
Modern Terraform Features (1.0+)
| Feature | Min version | Common use |
|---|---|---|
try() | 0.13+ | Safe fallbacks, replaces element(concat()) |
nullable = false | 1.1+ | Prevent null silently overriding defaults |
moved blocks | 1.1+ | Refactor without destroy/recreate |
optional() with defaults | 1.3+ | Typed object attributes |
import blocks | 1.5+ | Declarative imports, reviewable in VCS |
check blocks | 1.5+ | Runtime assertions |
Native terraform test | 1.6+ | Built-in test framework |
| Mock providers | 1.7+ | Cost-free unit testing |
removed blocks | 1.7+ | Declarative resource removal |
| Provider-defined functions | 1.8+ | Provider-specific transformations (requires provider to declare functions) |
| Cross-variable validation | 1.9+ | Reference other var.* in validation blocks |
write_only arguments | 1.11+ | Secrets never stored in state |
| S3 native lock-file | 1.10+ | State locking without DynamoDB |
Before emitting a feature, verify the runtime floor. See Code Patterns: Feature Guard Table for the full table with common LLM error patterns per feature.
Runtime-Specific Guidance
Terraform 1.0-1.5 (OpenTofu starts at 1.6): Terratest for integration, static analysis + plan validation only (no native tests).
1.6+: native
terraform test/tofu testavailable — migrate simple unit tests, keep Terratest for complex integration.1.7+: mock providers cut test cost — mock for unit tests, real runs for final integration.
1.10+: S3 native lock-file (
use_lockfile) is the correct default for new configurations — DynamoDB locking is no longer required.1.11+:
write_onlyarguments for secret handling keep credentials out of state.Terraform vs OpenTofu: both supported. For licensing, governance, and feature delta, see Quick Reference: Terraform vs OpenTofu.
Code Intelligence (terraform-ls)
Semantic navigation for HCL. terraform-ls is optional; without it every row below degrades to a disclosed rg + Read fallback.
Self-contained terraform-ls layer of a generic code-intelligence discipline - apply the rows below directly. Recommended companion: the code-intelligence plugin (same antonbabenko/agent-plugins marketplace) carries the generic discipline (position anchoring, degradation gate, disclosure format, anti-phantom-shim) and ships /code-intelligence:doctor for readiness. If it is installed, defer to its generic protocol; this skill stays fully self-contained without it.
| Goal | Use | Tradeoff |
|---|---|---|
| Find definition / all references | terraform-ls goToDefinition / findReferences | Needs init + a position anchor |
| Rename value symbol (var/local/output/provider alias) | Manual: findReferences -> per-file fresh Read -> edit -> validate | No rename provider |
| Rename resource/module address | moved block + plan shows 0 destroy | Text rename forces destroy/recreate |
Exact text / known name / .tfvars / non-HCL | rg + Read | No semantic scope |
✅ Supported: goToDefinition, findReferences, documentSymbol, hover, workspaceSymbol.
❌ Unsupported: goToImplementation, call hierarchy, rename provider. Do not call these then report their absence as a finding.
✅ Prereq: local
terraform/tofuon PATH,terraform initrun; cold start may need one retry.✅ LSP calls are position-anchored (
file:line:character) - anchor withrgfirst, never symbol-name-only.❌ Do not claim "LSP broken, using rg" until the Degradation Gate passes; disclose any tool substitution on the first line.
Depth: Code Intelligence.
Reference Files
Progressive disclosure — essentials here, depth on demand:
Testing Frameworks — static analysis, native tests, Terratest, mock providers
Module Patterns — structure, variable/output contracts,
terraform_remote_staterules, release checklistCI/CD Workflows — GitHub Actions, GitLab CI, Atlantis, cost control
Security & Compliance — trivy/checkov, secrets handling, compliance mappings
State Management — backends, locking, migration, multi-team, recovery
Code Patterns — block ordering,
count/for_eachdeep dive, modern features, version management, localsCode Intelligence - terraform-ls capabilities, position-anchored calls, manual rename, degradation gate
Quick Reference — command cheat sheets, flowcharts, troubleshooting
License
Apache License 2.0. See LICENSE for full terms.
Copyright © 2026 Anton Babenko
Files
- ATTRIBUTION.mdAdded by AutoGineer946 B
- LICENSELicence file added by AutoGineer10.6 KiB
- SKILL.md19.9 KiB
- references/ci-cd-workflows.md13.8 KiB
- references/code-intelligence-lsp.md6.6 KiB
- references/code-patterns.md30.9 KiB
- references/module-patterns.md22.9 KiB
- references/quick-reference.md19.4 KiB
- references/security-compliance.md18.6 KiB
- references/state-management.md45.1 KiB
- references/testing-frameworks.md13.7 KiB
references/state-management.md
State Management
Part of: terraform-skill Purpose: Comprehensive state management patterns and best practices for Terraform/OpenTofu
This document provides detailed guidance on state management, from remote backend configuration to recovery strategies and multi-team isolation patterns.
Table of Contents
Remote State Configuration
State Locking
State Security
State Migration
Multi-Team State Isolation
State Recovery & Troubleshooting
State Best Practices Summary
Safe Destroy Protocol
Remote State Configuration
Why Remote State?
Never use local state in teams or production:
❌ No locking → concurrent operations → state corruption
❌ No backup → accidental deletion → infrastructure loss
❌ No versioning → rollback impossible
❌ No collaboration → single point of failure
❌ Secrets in plaintext → security risk
Use remote backends for:
✅ Automatic state locking
✅ Encryption at rest and in transit
✅ State versioning and backup
✅ Team collaboration
✅ Audit logging
Choosing a Remote Backend
| Backend | Use when |
|---|---|
s3 | AWS workloads, existing AWS state |
azurerm | Azure workloads |
gcs | GCP workloads |
cloud / TF Cloud / HCP | hosted state, run management, policy enforcement |
Locking mechanism per backend: see Backend Locking Support below.
Cross-cloud equivalents
| Concern | AWS | Azure | GCP |
|---|---|---|---|
| Backend block | backend "s3" { bucket, key, region, encrypt, use_lockfile } | backend "azurerm" { resource_group_name, storage_account_name, container_name, key } | backend "gcs" { bucket, prefix } |
| Access control | IAM policy on bucket/role | RBAC role assignment on storage account/container | IAM binding on the bucket |
| Remote-state data source | terraform_remote_state (backend s3) | terraform_remote_state (backend azurerm) | terraform_remote_state (backend gcs) |
Bootstrap parity
| Concern | AWS | Azure | GCP |
|---|---|---|---|
| Versioning | S3 bucket versioning | storage account / blob versioning | GCS object versioning |
| Encryption at rest | explicit: SSE / KMS (aws_s3_bucket_server_side_encryption_configuration) | default-on (SSE; optional CMK) | default-on (Google-managed; optional CMEK) |
| Public-access block | aws_s3_bucket_public_access_block | allow_nested_items_to_be_public = false + private container | uniform bucket-level access + public access prevention |
| Bootstrap auth | IAM / OIDC | RBAC / federated credentials | IAM / Workload Identity Federation |
AWS S3 Backend (Recommended)
S3 with Native Lock-File (Terraform 1.10+, Recommended)
Simplest setup - no DynamoDB required:
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/vpc/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true # Native S3 locking (Terraform 1.10+)
# Optional but recommended
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
}
}Tradeoffs vs DynamoDB locking: no separate table, no DynamoDB charges, state and locks co-located in one bucket.
S3 with DynamoDB Locking (Pre-1.10 or Legacy)
Complete setup with DynamoDB:
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/vpc/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
# Optional but recommended
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/12345678-1234-1234-1234-123456789012"
}
}When to use DynamoDB locking:
Terraform versions < 1.10
Existing infrastructure already using DynamoDB
Need DynamoDB for other purposes
Migration note: Existing setups using DynamoDB will continue to work. The use_lockfile option is opt-in.
Backend infrastructure setup (Terraform 1.10+ with lock-file):
# bootstrap/main.tf - Run this ONCE to create state backend
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.terraform_state.arn
}
}
}
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# MFA Delete for production
# Note: Terraform cannot enable S3 MFA Delete. This must be configured
# outside of Terraform using the AWS CLI or an SDK with the root account.
#
# Example (run once, after bucket creation and versioning are enabled):
#
# aws s3api put-bucket-versioning \
# --bucket my-terraform-state \
# --versioning-configuration Status=Enabled,MFADelete=Enabled \
# --mfa "arn-of-mfa-device mfa-code"
# KMS key for encryption
resource "aws_kms_key" "terraform_state" {
description = "KMS key for Terraform state encryption"
deletion_window_in_days = 30
enable_key_rotation = true
tags = {
Name = "terraform-state-encryption"
}
}
resource "aws_kms_alias" "terraform_state" {
name = "alias/terraform-state"
target_key_id = aws_kms_key.terraform_state.key_id
}Backend infrastructure setup (Pre-1.10 with DynamoDB):
# If using DynamoDB locking, add this resource to the above configuration:
# DynamoDB table for state locking
resource "aws_dynamodb_table" "terraform_state_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
lifecycle {
prevent_destroy = true
}
tags = {
Name = "Terraform State Lock Table"
Environment = "shared"
}
}Key organization pattern:
s3://my-terraform-state/
├── prod/
│ ├── vpc/terraform.tfstate
│ ├── eks/terraform.tfstate
│ └── rds/terraform.tfstate
├── staging/
│ ├── vpc/terraform.tfstate
│ └── eks/terraform.tfstate
└── dev/
└── vpc/terraform.tfstateAzure Storage Backend
# backend.tf
terraform {
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "tfstatestorage"
container_name = "tfstate"
key = "prod.terraform.tfstate"
# Optional: Use service principal or managed identity
use_azuread_auth = true
}
}Backend setup:
# bootstrap/main.tf
resource "azurerm_resource_group" "terraform_state" {
name = "terraform-state-rg"
location = "East US"
}
resource "azurerm_storage_account" "terraform_state" {
name = "tfstatestorage"
resource_group_name = azurerm_resource_group.terraform_state.name
location = azurerm_resource_group.terraform_state.location
account_tier = "Standard"
account_replication_type = "GRS" # Geo-redundant
# Security settings
min_tls_version = "TLS1_2"
allow_nested_items_to_be_public = false
blob_properties {
versioning_enabled = true
}
}
resource "azurerm_storage_container" "terraform_state" {
name = "tfstate"
storage_account_name = azurerm_storage_account.terraform_state.name
container_access_type = "private"
}Google Cloud Storage Backend
# backend.tf
terraform {
backend "gcs" {
bucket = "my-terraform-state"
prefix = "prod/vpc"
# For customer-managed encryption, configure the bucket itself with
# `default_kms_key_name` (see bootstrap below) rather than the backend.
# The backend's `encryption_key` attribute is for CSEK (a base64-encoded
# 32-byte AES-256 key), NOT a Cloud KMS resource name.
}
}Backend setup:
# bootstrap/main.tf
resource "google_storage_bucket" "terraform_state" {
name = "my-terraform-state"
location = "US"
force_destroy = false
versioning {
enabled = true
}
encryption {
default_kms_key_name = google_kms_crypto_key.terraform_state.id
}
uniform_bucket_level_access = true
lifecycle_rule {
condition {
num_newer_versions = 10
}
action {
type = "Delete"
}
}
}
resource "google_kms_key_ring" "terraform_state" {
name = "terraform-state"
location = "us-central1"
}
resource "google_kms_crypto_key" "terraform_state" {
name = "terraform-state-key"
key_ring = google_kms_key_ring.terraform_state.id
rotation_period = "7776000s" # 90 days
}Terraform Cloud/Enterprise Backend
# backend.tf
terraform {
cloud {
organization = "my-org"
workspaces {
name = "prod-infrastructure"
# Or use tags for dynamic workspace selection
# tags = ["prod", "networking"]
}
}
}
# Alternative: Terraform Enterprise with custom hostname
terraform {
cloud {
hostname = "terraform.company.com"
organization = "my-org"
workspaces {
name = "prod-infrastructure"
}
}
}Terraform Cloud provides: built-in state management and locking, remote execution, Sentinel policy enforcement, cost estimation, private module registry, VCS integration — no backend infra to manage.
Backend Configuration Best Practices
✅ DO:
Use separate state files per logical component
Enable versioning on state storage
Use encryption at rest (KMS)
Configure state locking
Use separate backends per environment
Store backend config in version control
Use partial configuration for sensitive values
❌ DON'T:
Use local state for teams or production
Share state files across unrelated resources
Hardcode credentials in backend config
Disable versioning
Skip encryption
Use same state for all environments
Partial backend configuration:
# backend.tf - No sensitive values
terraform {
backend "s3" {
bucket = "my-terraform-state"
region = "us-east-1"
# key, dynamodb_table specified via -backend-config
}
}# Pass sensitive config at init time
terraform init \
-backend-config="key=prod/vpc/terraform.tfstate" \
-backend-config="dynamodb_table=terraform-state-lock"
# Or use a file
terraform init -backend-config=backend-prod.hclState Locking
Why Locking Matters
Without locking:
User A: terraform apply (starts)
User B: terraform apply (starts at same time)
Result: Both read same state, make conflicting changes
→ State corruption
→ Infrastructure drift
→ Potential outagesWith locking:
User A: terraform apply (acquires lock)
User B: terraform apply (waits for lock)
Result: Operations are serialized
→ State consistency maintainedBackend Locking Support
| Backend | Locking | Lock Mechanism |
|---|---|---|
| S3 (Terraform 1.10+) | ✅ Native | Lock files |
| S3 (Pre-1.10) | ✅ With DynamoDB | DynamoDB table |
| Azure Storage | ✅ Native | Blob lease |
| GCS | ✅ Native | Object metadata |
| Terraform Cloud | ✅ Native | Built-in |
| Consul | ✅ Native | Consul KV |
| Postgres | ✅ Native | Row locking |
| Local | ❌ None | N/A |
S3 Native Lock-File (Terraform 1.10+)
How it works:
Uses regular S3 objects as lock files
Lock files stored in the same bucket as state files
No additional AWS services required
Automatically deleted when operations complete
Configuration:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true # Enable native S3 locking
}
}Migration from DynamoDB: Set both dynamodb_table and use_lockfile = true during Terraform 1.10+ migration — locks acquire via both mechanisms. Once every workflow runs on 1.10+, remove dynamodb_table.
DynamoDB Locking for S3 (Pre-1.10 or Legacy)
Lock table attributes:
LockID(String, Hash Key) - Must be exactly "LockID"No other attributes needed
Pay-per-request billing recommended
Lock behavior:
# Terraform acquires lock
terraform plan
# Creates lock: LockID = "bucket/path/to/state"
# Another user attempts operation
terraform apply
# Sees: Error acquiring the state lock
# Default: `-lock-timeout=0s` — fail immediately on lock contention.
# Set `-lock-timeout=<duration>` (e.g. `5m`) to retry with backoff for the specified window.
# terraform apply -lock-timeout=5mView current locks:
# Check DynamoDB for active locks
aws dynamodb scan \
--table-name terraform-state-lock \
--projection-expression "LockID,Info"Handling Lock Conflicts
Scenario 1: Lock Already Held
Symptom:
Error: Error acquiring the state lock
Lock Info:
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Path: bucket/prod/terraform.tfstate
Operation: OperationTypeApply
Who: user@host
Created: 2026-01-20 12:00:00.123456789 +0000 UTCSolutions:
Wait for operation to complete (recommended)
# Just wait - the other operation will release the lockCheck if operation is actually running
# If user@host is accessible, check if terraform is running ssh user@host "ps aux | grep terraform"Force unlock if operation crashed (DANGEROUS)
# Only if you're CERTAIN the lock is stale terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890
Scenario 2: Stale Lock (Operation Crashed)
When to force-unlock:
✅ Process crashed/killed
✅ Network interruption
✅ CI/CD job terminated
✅ You verified no operation is running
When NOT to force-unlock:
❌ Just because you're impatient
❌ Without checking if operation is running
❌ In automation (should fail instead)
Safe force-unlock workflow:
# 1. Verify lock exists
terraform plan
# Note the Lock ID from error message
# 2. Check if operation is actually running
# - SSH to the host if accessible
# - Check CI/CD job status
# - Ask team members
# 3. Only if confirmed stale, force unlock
terraform force-unlock LOCK_ID
# 4. Document why you force-unlocked
git commit -m "Force-unlocked state after CI job termination"Automatic Lock Timeout
Terraform Cloud lock timeout:
Lock timeout in Terraform Cloud is configured through workspace settings in the Terraform Cloud UI under "General Settings" → "Remote Operations" → "Lock Timeout". It cannot be configured through the cloud block in Terraform code.
For other backends, implement timeout in automation:
#!/bin/bash
# wrapper-script.sh
LOCK_TIMEOUT=300 # 5 minutes
timeout $LOCK_TIMEOUT terraform apply -auto-approve
if [ $? -eq 124 ]; then
echo "Terraform timed out - likely lock held"
exit 1
fiState Locking in CI/CD
Prevent concurrent runs:
# GitHub Actions - Use concurrency control
concurrency:
group: terraform-${{ github.ref }}
cancel-in-progress: false # Don't cancel, wait instead
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- name: Terraform Apply
run: terraform apply -auto-approveGitLab CI:
# .gitlab-ci.yml
terraform-apply:
script:
- terraform apply -auto-approve
resource_group: terraform-prod # Only one job at a timeState Security
Encryption at Rest
S3 Backend:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "us-east-1"
encrypt = true # ✅ Always enable
kms_key_id = "arn:aws:kms:us-east-1:123456789012:key/..." # Optional: Use customer-managed key
}
}Encryption options:
| Method | Key Management | Cost | Use Case |
|---|---|---|---|
| SSE-S3 (AES-256) | AWS-managed | Included | Basic encryption |
| SSE-KMS | Customer-managed | $$$ per 10K requests | Compliance requirements |
| SSE-C | Client-managed | Included | Full control needed |
Encryption in Transit
All backends use TLS by default:
S3: HTTPS
Azure Storage: HTTPS
GCS: HTTPS
Terraform Cloud: HTTPS
Enforce TLS-only access:
// S3 bucket policy
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-terraform-state",
"arn:aws:s3:::my-terraform-state/*"
],
"Condition": {
"Bool": {
"aws:SecureTransport": "false"
}
}
}
]
}Access Control Patterns
IAM Policy for S3 Backend (AWS)
Minimal permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::my-terraform-state"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::my-terraform-state/prod/*"
},
{
"Effect": "Allow",
"Action": [
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:*:table/terraform-state-lock"
},
{
"Effect": "Allow",
"Action": [
"kms:DescribeKey",
"kms:Decrypt",
"kms:Encrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:us-east-1:*:key/*"
}
]
}Read-only policy (for auditing):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject"
],
"Resource": [
"arn:aws:s3:::my-terraform-state",
"arn:aws:s3:::my-terraform-state/*"
]
},
{
"Effect": "Allow",
"Action": "kms:Decrypt",
"Resource": "arn:aws:kms:us-east-1:*:key/*"
}
]
}Environment Isolation with IAM
Principle: Each environment gets its own IAM role with scoped access
# prod-role can only access prod state
resource "aws_iam_role" "terraform_prod" {
name = "terraform-prod"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam::123456789012:root"
}
Action = "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy" "terraform_prod_state" {
role = aws_iam_role.terraform_prod.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:ListBucket",
"s3:GetObject",
"s3:PutObject"
]
Resource = [
"arn:aws:s3:::my-terraform-state",
"arn:aws:s3:::my-terraform-state/prod/*" # Only prod path
]
}
]
})
}Sensitive Data in State Files
What gets stored in state:
Resource attributes (including computed values)
Variable values
Output values
Sensitive data like:
Database passwords
API keys
SSH keys
Certificate private keys
❌ DON'T: Store Secrets in Variables
# BAD: Secret visible in state
variable "database_password" {
type = string
sensitive = true
default = "SuperSecret123!" # ❌ Still stored in state
}✅ DO: Use Write-Only Arguments (Terraform 1.11+)
# Good: Password never stored in state
resource "aws_db_instance" "this" {
# ... other config ...
manage_master_user_password = true # AWS generates and stores password
lifecycle {
ignore_changes = [master_user_secret]
}
}
# Access password via Secrets Manager
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = aws_db_instance.this.master_user_secret[0].secret_arn
}Caveat: Reading a secret through
data "aws_secretsmanager_secret_version"pullssecret_stringinto the state file on every refresh. If the goal is to keep the raw secret out of state, use anephemeralresource/data source (Terraform 1.10+),manage_master_user_password, or inject the value via a CI-only environment variable instead of a data source.
✅ DO: Reference External Secrets
# Good: Fetch secret at runtime, not stored in state
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/database/master-password"
}
resource "aws_db_instance" "this" {
password = data.aws_secretsmanager_secret_version.db_password.secret_string
# Password still in state, but not hardcoded
}Caveat: The data source writes
secret_stringinto state on every refresh, so this pattern avoids hardcoding — it does not exclude the secret from state. For true state exclusion, use anephemeralresource/data source (Terraform 1.10+),manage_master_user_password, or a CI-injected env var.
Best Practice: Reconcile State After External Secret Rotation
# After rotation (handled outside Terraform), refresh state so it reflects
# the new value. This does not rotate the secret itself.
terraform apply -refresh-onlyState File Audit Logging
S3 Bucket logging:
resource "aws_s3_bucket_logging" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
target_bucket = aws_s3_bucket.logs.id
target_prefix = "terraform-state-access/"
}
# CloudTrail for API-level logging
resource "aws_cloudtrail" "terraform_state" {
name = "terraform-state-trail"
s3_bucket_name = aws_s3_bucket.cloudtrail_logs.id
event_selector {
read_write_type = "All"
include_management_events = true
data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3:::my-terraform-state/*"]
}
}
}Azure Storage logging:
azurerm_storage_account.blob_properties does NOT expose a logging sub-block.
Configure blob-service audit logs via azurerm_monitor_diagnostic_setting
targeting the storage account's blobServices resource.
What to monitor:
Who accessed state files
When state was modified
What changes were made
Failed access attempts
State Migration
Migrating Between Backends
Local → S3 Migration
Step 1: Set up S3 backend infrastructure
# In bootstrap directory
terraform init
terraform applyStep 2: Add backend config to your Terraform code
# backend.tf - Add this file
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/vpc/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}Step 3: Initialize with migration
# Backup local state first
cp terraform.tfstate terraform.tfstate.backup
# Migrate to S3
terraform init -migrate-state
# Terraform will ask: "Do you want to copy existing state to the new backend?"
# Answer: yes
# Verify migration
terraform plan # Should show no changes
# Verify state in S3
aws s3 ls s3://my-terraform-state/prod/vpc/Step 4: Clean up local state (AFTER verifying S3 works)
# Remove local state files
rm terraform.tfstate*
# Commit backend config
git add backend.tf
git commit -m "Migrate state to S3 backend"S3 → Terraform Cloud Migration
Step 1: Authenticate to Terraform Cloud
# Authenticate the CLI
terraform loginThe Terraform Cloud workspace is created automatically on first terraform init
against the cloud {} block below (provided the org permits auto-creation).
Alternatively, pre-create it in the TFC UI or with the tfe_workspace resource.
Do NOT use terraform workspace new here — CLI workspaces are a different
concept from Terraform Cloud workspaces.
Step 2: Update backend config
# backend.tf - Change from S3 to cloud
terraform {
cloud {
organization = "my-org"
workspaces {
name = "prod-infrastructure"
}
}
}
# Remove old S3 backend configStep 3: Migrate state
# Initialize with migration
terraform init -migrate-state
# Confirm migration
# State will be uploaded to Terraform CloudStep 4: Verify and clean up
# Verify in Terraform Cloud UI or CLI
terraform state list
# Old S3 state remains as backup - don't delete immediately
# Keep for 30-90 days, then removeBackend Change Without Migration
When recreating infrastructure is acceptable:
# Change backend config in backend.tf
# Re-initialize (will create new empty state)
terraform init -reconfigure
# Import existing resources
terraform import aws_vpc.this vpc-12345678
terraform import aws_subnet.private subnet-abcd1234
# ... import all resources ...
# Or destroy and recreate
terraform destroy # In old backend
terraform apply # In new backendState Refactoring with terraform state mv
Use cases:
Renaming resources
Moving resources between modules
Reorganizing state structure
Splitting monolithic state
Renaming a Resource
# Before: aws_instance.server
# After: aws_instance.web_server
terraform state mv aws_instance.server aws_instance.web_server
# Update code to match
# In main.tf: resource "aws_instance" "web_server" { ... }
# Verify
terraform plan # Should show no changesMoving Resource to Module
# Before: aws_s3_bucket.logs (in root module)
# After: module.logging.aws_s3_bucket.logs
# Step 1: Create module with resource
# Step 2: Move state
terraform state mv aws_s3_bucket.logs module.logging.aws_s3_bucket.logs
# Step 3: Remove old resource from root module
# Step 4: Add module call
# Step 5: Verify
terraform plan # Should show no changesMoving Resource Between Modules
# Move from module.old to module.new
terraform state mv \
module.old.aws_instance.app \
module.new.aws_instance.appMoving Resource to Different State File
Scenario: Splitting state into separate files
Note: terraform state mv only works within the same state file. To move resources between different state files, use the approach below.
Recommended approach: Use terraform state rm and import
# In source state - remove resource
terraform state rm aws_rds_cluster.main
# In destination state - import resource
terraform import aws_rds_cluster.main cluster-identifierState Push/Pull Operations
Pull state (download):
# View current state
terraform state pull
# Save to file
terraform state pull > terraform.tfstate.backup
# View specific resource
terraform state show aws_instance.webPush state (upload):
# Restore from backup
terraform state push terraform.tfstate.backup
# DANGEROUS: Overwrites remote state
# Only use for disaster recoveryWhen to use push/pull:
✅ Creating backups
✅ Disaster recovery
✅ Debugging state issues
✅ Manual state surgery (advanced)
❌ Regular operations (use terraform commands)
❌ Concurrent team access
State Backup Strategies
Automatic Backups
S3 versioning (automatic):
# List all versions
aws s3api list-object-versions \
--bucket my-terraform-state \
--prefix prod/vpc/terraform.tfstate
# Restore specific version
aws s3api get-object \
--bucket my-terraform-state \
--key prod/vpc/terraform.tfstate \
--version-id VERSION_ID \
terraform.tfstate.restoredPre-operation backup:
# Manual backup before major changes
terraform state pull > backup-$(date +%Y%m%d-%H%M%S).tfstate
# Or in automation
#!/bin/bash
BACKUP_DIR="./state-backups"
mkdir -p $BACKUP_DIR
terraform state pull > "$BACKUP_DIR/terraform.tfstate.$(date +%Y%m%d-%H%M%S)"
# Keep last 30 backups
ls -t $BACKUP_DIR/terraform.tfstate.* | tail -n +31 | xargs rm -fDisaster Recovery Plan
1. Backup checklist:
[ ] State file backed up (versioning enabled)
[ ] Backend config documented
[ ] IAM policies documented
[ ] Encryption keys accessible
[ ] Restore procedure tested
2. Recovery procedure:
# If state corrupted
# Step 1: Download last known good version
aws s3api get-object \
--bucket my-terraform-state \
--key prod/vpc/terraform.tfstate \
--version-id PREVIOUS_VERSION_ID \
terraform.tfstate.recovered
# Step 2: Push recovered state
terraform state push terraform.tfstate.recovered
# Step 3: Verify
terraform plan
# Step 4: If resources drifted, reconcile
terraform apply -refresh-onlyProvider Removal
Terraform calls the provider plugin's Destroy RPC during apply. Keep the provider installed until every resource for that provider is destroyed or removed from state.
| Goal | Use | Tradeoff |
|---|---|---|
| Remove provider and destroy the real resource | Two-phase removal (default) | Safe; requires apply |
| Remove provider and keep the real resource | removed block (Terraform 1.7+, OpenTofu 1.7+) | Declarative; real resource stays but becomes unmanaged |
| Remove from state manually | terraform state rm <addr> | Orphans the real resource; use only when intentionally abandoning |
Two-phase removal
Phase 1 — destroy resources, keep provider: Delete resource blocks from config (or mark for destruction). Keep the
providerblock andrequired_providersentry. Runterraform planand confirm target resources showdestroy. Runterraform apply. Runterraform state listand verify no resources remain for that provider.Phase 2 — remove provider: Remove the
providerblock and therequired_providersentry. Runterraform init. Runterraform planand expect no changes and no errors.
removed block
removed {
from = vault_policy.ops
lifecycle {
destroy = false
}
}Rules
❌ Remove the provider block first: plan cannot resolve the resource type → hard error.
✅ Same rule applies to provider aliases and multi-provider modules.
✅ Plain
terraform initafter removal;-upgradeis for bumping existing providers, not required here.
Multi-Team State Isolation
State Organization Patterns
Pattern 1: State Per Environment
Structure:
my-company-tf-state/
├── dev/
│ ├── networking/terraform.tfstate
│ ├── compute/terraform.tfstate
│ └── data/terraform.tfstate
├── staging/
│ ├── networking/terraform.tfstate
│ └── compute/terraform.tfstate
└── prod/
├── networking/terraform.tfstate
├── compute/terraform.tfstate
└── data/terraform.tfstateBenefits:
✅ Clear environment separation
✅ Different IAM roles per environment
✅ Blast radius limited to environment
✅ Easy to understand
Drawbacks:
⚠️ Duplicate code across environments
⚠️ Harder to keep environments in sync
Pattern 2: State Per Team/Component
Structure:
my-company-tf-state/
├── networking-team/
│ ├── prod-vpc/terraform.tfstate
│ ├── staging-vpc/terraform.tfstate
│ └── vpn/terraform.tfstate
├── platform-team/
│ ├── prod-eks/terraform.tfstate
│ └── staging-eks/terraform.tfstate
└── data-team/
├── prod-rds/terraform.tfstate
└── prod-redshift/terraform.tfstateBenefits:
✅ Team ownership clear
✅ Team-specific access control
✅ Independent release cycles
✅ Reduced coordination overhead
Drawbacks:
⚠️ Cross-team dependencies complex
⚠️ Need data sharing mechanisms
Pattern 3: Hybrid (Environment + Component)
Structure:
my-company-tf-state/
├── prod/
│ ├── 01-networking/terraform.tfstate # VPC, subnets
│ ├── 02-platform/terraform.tfstate # EKS, ALB
│ ├── 03-data/terraform.tfstate # RDS, Redis
│ └── 04-applications/terraform.tfstate
├── staging/
│ ├── 01-networking/terraform.tfstate
│ └── 02-platform/terraform.tfstateBenefits:
✅ Clear environment boundaries
✅ Component isolation within environment
✅ Numbered prefixes show dependencies
✅ Team ownership possible
Recommended: This pattern for most teams
Cross-State Data Sharing
Using terraform_remote_state
Producer module (networking):
# outputs.tf in networking module
output "vpc_id" {
description = "VPC ID for other modules"
value = aws_vpc.main.id
}
output "private_subnet_ids" {
description = "Private subnet IDs"
value = aws_subnet.private[*].id
}
output "database_security_group_id" {
description = "Security group for databases"
value = aws_security_group.database.id
}Consumer module (compute):
# data.tf in compute module
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "prod/networking/terraform.tfstate"
region = "us-east-1"
}
}
# Use networking outputs
resource "aws_instance" "app" {
subnet_id = data.terraform_remote_state.networking.outputs.private_subnet_ids[0]
vpc_security_group_ids = [data.terraform_remote_state.networking.outputs.database_security_group_id]
}Best practices:
✅ Document what outputs are for cross-module use
✅ Version outputs (add v2 suffix if breaking change)
✅ Keep outputs stable (don't rename casually)
✅ Use descriptive output names
Alternative: SSM Parameter Store
Producer:
# Store values in SSM
resource "aws_ssm_parameter" "vpc_id" {
name = "/terraform/prod/networking/vpc_id"
type = "String"
value = aws_vpc.main.id
}Consumer:
# Read from SSM
data "aws_ssm_parameter" "vpc_id" {
name = "/terraform/prod/networking/vpc_id"
}
resource "aws_instance" "app" {
subnet_id = data.aws_ssm_parameter.vpc_id.value
}Benefits over remote_state:
✅ No direct state dependency
✅ Can be read by non-Terraform tools
✅ Can be updated without Terraform
✅ Fine-grained IAM control
Drawbacks:
⚠️ Extra resources to manage
⚠️ Potential for values to be out of sync
When to Split vs Combine State
Split State When:
✅ Different lifecycles:
VPC (rarely changes) vs EC2 instances (frequently updated)
✅ Different teams own components:
Networking team manages VPC
Platform team manages Kubernetes
App teams manage applications
✅ Different risk profiles:
Critical infrastructure vs experimental features
✅ Large state files:
State operations becoming slow (>1000 resources — rough heuristic, depends on provider refresh time)
✅ Independent deployment cadence:
Database needs weekly updates
Application needs daily updates
Combine State When:
✅ Tightly coupled resources:
EC2 instance + EBS volume + ENI
✅ Same lifecycle:
All resources created/destroyed together
✅ Simple, small project:
< 100 resources
Single team ownership
✅ Resources reference each other frequently:
Security groups that reference each other
Decision Matrix
| Factor | Split State | Single State |
|---|---|---|
| Team size | Multiple teams | Single team |
| Resource count | >500 resources | <100 resources (rough heuristics — depends on provider refresh time) |
| Update frequency | Different cadences | Same cadence |
| Risk tolerance | Low (production) | High (dev/test) |
| Coupling | Loosely coupled | Tightly coupled |
| Ownership | Multiple owners | Single owner |
State Recovery & Troubleshooting
Recovering from State Corruption
Scenario 1: State File Corrupted
Symptoms:
Error: state snapshot was created by Terraform v1.8.0,
which is newer than current v1.6.0Solutions:
A) Restore from backup:
# S3 versioning
aws s3api list-object-versions \
--bucket my-terraform-state \
--prefix prod/terraform.tfstate
aws s3api get-object \
--bucket my-terraform-state \
--key prod/terraform.tfstate \
--version-id PREVIOUS_VERSION \
terraform.tfstate.restored
terraform state push terraform.tfstate.restoredB) Upgrade Terraform:
# Download newer version
tfenv install 1.8.0
tfenv use 1.8.0
# Verify
terraform versionScenario 2: State Completely Lost
If no backup exists:
Recreate state from existing infrastructure:
# List all resources to import
# (You'll need to know what was managed)
# Import resources one by one
terraform import aws_vpc.main vpc-12345678
terraform import aws_subnet.private[0] subnet-abcd1234
terraform import aws_subnet.private[1] subnet-efgh5678
# ... continue for all resources ...
# Verify
terraform plan # Should eventually show no changesUse import blocks (Terraform 1.5+):
# import.tf
import {
to = aws_vpc.main
id = "vpc-12345678"
}
import {
to = aws_subnet.private[0]
id = "subnet-abcd1234"
}terraform plan -generate-config-out=generated.tf
# Review generated.tf and merge with existing config
terraform applyHandling State Lock Stuck Issues
Issue: Lock Persists After Crash
Check lock status:
# DynamoDB (S3 backend)
aws dynamodb get-item \
--table-name terraform-state-lock \
--key '{"LockID":{"S":"my-terraform-state/prod/terraform.tfstate"}}'
# If item exists, lock is heldForce unlock:
# Get Lock ID from error message or DynamoDB
terraform force-unlock LOCK_ID
# Confirm when promptedPrevent future issues in CI/CD:
Use concurrency controls instead of automatic force-unlock (see CI/CD section below).
Issue: Cannot Acquire Lock in CI/CD
Problem: Parallel CI jobs trying to acquire lock
Solution 1: Use concurrency control
# GitHub Actions
concurrency:
group: terraform-${{ matrix.environment }}
cancel-in-progress: false
jobs:
terraform:
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, prod]Solution 2: Separate state per branch/PR
# backend.tf (partial configuration - key provided dynamically)
terraform {
backend "s3" {
bucket = "my-terraform-state"
region = "us-east-1"
# Note: Backend configuration does not support interpolation or env vars.
# Set the key dynamically during init in CI/CD:
}
}
# In CI/CD workflow (e.g., GitHub Actions):
# terraform init -backend-config="key=pr-${GITHUB_PR_NUMBER}/terraform.tfstate"State Refresh and Reconciliation
State drift: State doesn't match reality
Detect drift:
# Terraform 0.15.4+
terraform plan -refresh-only
# Shows what's changed in infrastructure vs stateReconcile drift:
# Update state to match reality (no infrastructure changes)
terraform apply -refresh-only
# Or during regular plan/apply
terraform plan # Includes refresh
terraform apply # Updates stateCommon drift causes:
Manual changes in AWS console
Changes by other tools (aws cli, CDK)
Resource deletion outside Terraform
Provider API changes
Prevent drift:
✅ Use CloudTrail to monitor manual changes
✅ Implement policy to block manual changes
✅ Use drift detection tools (Terraform Cloud drift detection, driftctl)
✅ Regular
terraform planin CI/CD✅ Enable termination protection on critical resources
Import for State Recovery
When to use import:
Resources created manually, now want Terraform to manage
Recovering from state loss
Adopting existing infrastructure
Migrating from other IaC tools
Import workflow:
1. Write resource configuration:
# main.tf
resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t3.micro"
# ... other attributes ...
}2. Import existing resource:
# Find resource ID
aws ec2 describe-instances --filters "Name=tag:Name,Values=web-server"
# Import
terraform import aws_instance.web i-1234567890abcdef03. Reconcile configuration:
# Plan will show attributes that don't match
terraform plan
# Update main.tf to match actual resource
# Or update resource to match main.tf4. Verify:
terraform plan # Should show no changesBulk import:
#!/bin/bash
# import-instances.sh
# Get all instance IDs
INSTANCE_IDS=$(aws ec2 describe-instances \
--query 'Reservations[].Instances[].InstanceId' \
--output text)
# Import each
for instance_id in $INSTANCE_IDS; do
terraform import "aws_instance.imported[\"$instance_id\"]" "$instance_id"
doneTerraform 1.5+ import blocks:
# Generate configuration from imports
import {
to = aws_instance.web
id = "i-1234567890abcdef0"
}
import {
to = aws_security_group.web
id = "sg-0123456789abcdef"
}# Generate configuration
terraform plan -generate-config-out=imported.tf
# Review and merge
terraform applyState Best Practices Summary
Decision Matrix: State Organization
| Scenario | Recommendation | Reasoning |
|---|---|---|
| Single team, <100 resources | Single state file | Simple, low overhead |
| Multiple teams | State per team | Clear ownership, independent deploys |
| Mixed update frequencies | State per lifecycle | Deploy VPC separately from apps |
| Production environment | State per component | Limit blast radius |
| Shared infrastructure | Separate state + remote_state | Core infra stable, apps change often |
| Development/testing | Combined state OK | Less critical, faster to rebuild |
Common Anti-Patterns to Avoid
❌ Anti-Pattern 1: Manual state editing
# DON'T manually edit state files
vim terraform.tfstate # ❌ Likely to corrupt✅ Instead: Use terraform state commands
terraform state mv
terraform state rm
terraform import❌ Anti-Pattern 2: Sharing state files via git
git add terraform.tfstate # ❌ No locking, merge conflicts✅ Instead: Use remote backend
❌ Anti-Pattern 3: Bypassing locks
rm .terraform/terraform.tfstate.lock.info # ❌ Dangerous✅ Instead: Investigate why lock exists, then force-unlock if safe
❌ Anti-Pattern 4: No backup strategy ✅ Instead: Enable versioning, regular backups, test restore
❌ Anti-Pattern 5: Monolithic state
5000+ resources in one state
Slow operations
High blast radius ✅ Instead: Split by component/team/environment
Safe Destroy Protocol
A targeted destroy can cascade far beyond its targets via implicit dependencies. Always follow this sequence:
Run
terraform plan -destroy [-target=...]- never skip straight to destroyRead every resource under "will be destroyed", not just the targeted ones
⚠️ Watch for
for_eachresources fed by a local that references a targeted resource - all instances become implicit dependents (e.g. targeting anaws_eipthat is referenced in alocals {}block used by afor_eachrecord resource will destroy all those records)Get explicit user confirmation of the full list before proceeding
❌ Never use
-auto-approveon destroy in production
LLM Mistake Checklist — State Management
Common model mistakes to correct before returning state-related recommendations:
recommends local state in team/production contexts
proposes one monolithic root state for "convenience"
suggests
rm .terraform.tfstate.lock.infoorforce-unlockwithout investigating why the lock existsedits
terraform.tfstatemanually instead of usingterraform state mv/rm/importcommits
*.tfstateto gitmixes prod and non-prod in the same backend key
recommends workspace-only isolation as a substitute for backend-level IAM separation
writes DynamoDB-lock configuration on Terraform 1.10+ instead of using
use_lockfile = trueon the S3 backendruns
terraform destroy -auto-approveor skipsplan -destroybefore a targeted destroy - missing implicit dependents pulled in via shared localsreads via
terraform_remote_statewithin a single team's stack instead of using module outputs (see module-patterns.md)omits the rollback/recovery note for destructive state operations
State Management Checklist
Setup: remote backend, locking, encryption at rest, IAM-scoped access, versioning/backup, audit logging.
Per environment: separate backend key, scoped IAM role, documented key naming convention, documented DR plan.
Ongoing: scheduled drift detection (plan -detailed-exitcode), tested state restore, review when a state exceeds ~500 resources, documented force-unlock policy.
Security: no secrets in variables/state (use write_only or external lookup), TLS enforced, audit log reviewed, encryption keys rotated.
Back to: Main Skill File
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Security
What our scanner noticed· Medium risk
- Instructions that use the network
- Downloads piped into a shell
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Anton Babenko
- Publisher
- Anton Babenko (Community)
- Source
- antonbabenko/terraform-skill · skills/terraform-skill · commit 9c1e92d
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Anton Babenko or Anton Babenko.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download terraform-skill.zip