How-to Guides¶
Scope
Task recipes for Terraform 1.16 (current stable, 2026-09): install and upgrade, state backends and locking, refactoring and import, workspaces, CI/CD and drift detection, security setup (backend hardening, dynamic credentials, keeping secrets out of state), tests, CDKTF migration, performance, and troubleshooting. Facts and tables are in Reference; background is in Explanation.
Install and Upgrade Terraform¶
Terraform ships as a single binary from releases.hashicorp.com and HashiCorp's package repositories.
# Debian / Ubuntu: HashiCorp apt repository
wget -O- https://apt.releases.hashicorp.com/gpg | \
sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | \
sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update && sudo apt install terraform
# macOS: HashiCorp tap (formula tracks current releases, 1.16.4 as of 2026-09-25)
brew tap hashicorp/tap
brew install hashicorp/tap/terraform
terraform version
Pin the version per configuration
Declare the supported range so CI and teammates fail fast on the wrong binary:
Upgrade one minor at a time, read the "UPGRADE NOTES" in each branch changelog (for example 1.16 changed bastion_host_key handling in provisioners), and run terraform plan to confirm no unexpected diffs before applying.
State Management¶
Backend Configuration¶
Use S3 native locking (use_lockfile, GA in 1.11). The DynamoDB arguments (dynamodb_table) are deprecated. Other backends are compared in Reference.
# Production S3 backend (Terraform 1.11+)
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "prod/network/terraform.tfstate"
region = "us-east-1"
encrypt = true
kms_key_id = "alias/terraform-state"
use_lockfile = true
}
}
Migrate S3 Locking from DynamoDB¶
- Upgrade all users and pipelines to Terraform 1.11 or later.
- Add
use_lockfile = truenext to the existingdynamodb_table. With both set, Terraform takes both locks, so older and newer runs stay safe during rollout. - Re-initialize:
terraform init -reconfigure. - After every pipeline uses the new config, remove
dynamodb_table, runterraform init -reconfigureagain, and delete the DynamoDB table.
The IAM principal needs s3:PutObject and s3:DeleteObject on the <key>.tflock object in addition to state access.
Refactor with Blocks Instead of State Commands¶
Configuration-driven refactoring is reviewable in pull requests and works in HCP Terraform, where you cannot run terraform state mv against remote runs.
# Rename a resource without destroy/create
moved {
from = aws_instance.old
to = aws_instance.new
}
# Stop managing a resource but keep the real object (1.7+)
removed {
from = aws_instance.decommissioned
lifecycle {
destroy = false
}
}
# Protect a resource from ever being destroyed by Terraform (1.16+)
resource "aws_s3_bucket" "audit" {
bucket = "company-audit-logs"
lifecycle {
destroy = false
}
}
The imperative equivalents (terraform state mv, terraform state rm) are in Commands & Recipes.
Import Existing Infrastructure¶
Import with import Blocks¶
# imports.tf (1.5+; for_each on import since 1.7; import inside modules since 1.16)
import {
to = aws_instance.web
id = "i-1234567890abcdef0"
}
# Generate HCL for imported resources that have no configuration yet
terraform plan -generate-config-out=generated.tf
# Review and clean generated.tf, then apply
terraform apply
Discover Resources in Bulk with terraform query¶
Since 1.14, providers that implement list resources can enumerate existing objects. Support varies by provider and resource type.
terraform query # print discovered resources
terraform query -generate-config-out=imported.tf # emit import blocks + config
terraform validate -query # offline check of .tfquery.hcl files
Workspace Patterns¶
Environment Isolation¶
# Per-environment CLI workspaces
terraform workspace new staging
terraform workspace new production
terraform workspace select production
terraform workspace list -json # machine-readable (1.16+)
# Use the workspace name in configuration
locals {
env = terraform.workspace
instance_type = {
staging = "t3.small"
production = "t3.xlarge"
}
}
Workspace vs directory vs Stacks
For environments that differ in structure, credentials, or approval flow, use separate root modules with shared modules instead of CLI workspaces, which share one backend and variable shape. On HCP Terraform, Stacks deployments model "same components, many environments/regions" natively.
CI/CD Integration¶
GitHub Actions Pattern¶
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.16.4"
- name: Terraform Plan
run: |
terraform init -backend-config=backend.hcl -input=false
terraform plan -out=tfplan -input=false
terraform show -json tfplan > plan.json
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -input=false tfplan
A saved plan applies without a prompt, so gate the apply job behind review (protected environment or manual approval). Use OIDC to the cloud rather than stored keys.
Drift Detection¶
# Detect drift without applying
terraform plan -detailed-exitcode
# Exit code: 0 = no changes, 1 = error, 2 = changes detected
# Accept out-of-band changes into state without touching infrastructure
terraform apply -refresh-only
Schedule the plan (cron in CI) and alert on exit code 2. HCP Terraform offers built-in drift detection (health assessments) on higher tiers; check the pricing page for availability.
Security Setup¶
Harden a Self-Managed Backend¶
terraform {
backend "s3" {
bucket = "terraform-state-prod"
key = "infra/terraform.tfstate"
region = "us-east-1"
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:111122223333:key/EXAMPLE-KEY-ID"
use_lockfile = true
}
}
- Encrypt with a customer-managed KMS key and restrict
kms:Decryptto the principals that run Terraform. - Enable bucket versioning to recover from bad writes; block public access.
- Restrict bucket access with least-privilege IAM; enable access logging or CloudTrail data events.
- Equivalent controls exist for GCS (CMEK, uniform bucket-level access) and Azure Blob (RBAC, soft delete).
Configure Dynamic Provider Credentials¶
Example for AWS on HCP Terraform (Azure, GCP, Vault, and Kubernetes follow the same pattern with their own variables):
- In AWS IAM, create an OIDC identity provider for
https://app.terraform.iowith audienceaws.workload.identity. - Create a role whose trust policy allows
sts:AssumeRoleWithWebIdentitywith a condition onapp.terraform.io:sub, for exampleorganization:my-org:project:infra:workspace:prod-network:run_phase:*. - Set workspace (or variable-set) environment variables:
- Remove any static
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYvariables. Use separate plan and apply roles (TFC_AWS_PLAN_ROLE_ARN,TFC_AWS_APPLY_ROLE_ARN) for a read-only plan phase.
When OIDC is unavailable: keep keys in environment variables or sensitive variable sets (never in .tf files), generate short-lived credentials from Vault, and rotate on a schedule.
Keep Secrets Out of State¶
Use ephemeral resources and write-only arguments (1.10+/1.11+) so the secret never lands in plan or state files. The provider must support them (for example random_password as an ephemeral resource, and password_wo on aws_db_instance in recent hashicorp/aws releases; check your provider docs).
ephemeral "random_password" "db" {
length = 24
special = true
}
resource "aws_db_instance" "main" {
identifier = "app-db"
engine = "postgres"
instance_class = "db.t4g.medium"
allocated_storage = 20
username = "app"
password_wo = ephemeral.random_password.db.result
password_wo_version = 1 # bump to rotate
}
variable "api_token" {
type = string
ephemeral = true # accepted at plan/apply, never persisted
}
For values that are not secrets but must not be shown, mark outputs and variables sensitive = true; this only redacts CLI output, it does not keep them out of state.
Pin Modules and Providers¶
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "5.5.0" # exact pin
}
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 6.0, < 7.0"
}
}
}
# Record checksums for every platform CI and developers use
terraform providers lock \
-platform=linux_amd64 -platform=linux_arm64 \
-platform=darwin_arm64 -platform=windows_amd64
git add .terraform.lock.hcl
Write a Sentinel Policy¶
import "tfplan/v2" as tfplan
import "version"
main = rule {
version.new(tfplan.terraform_version).greater_than("1.5.0")
}
Attach it to a policy set in HCP Terraform / TFE with an enforcement level (advisory, soft-mandatory, hard-mandatory). Categories and levels are listed in Reference.
Test Modules with terraform test¶
# tests/vpc.tftest.hcl
mock_provider "aws" {}
variables {
cidr = "10.0.0.0/16"
name = "test"
}
run "vpc_uses_requested_cidr" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == var.cidr
error_message = "VPC CIDR does not match input"
}
}
terraform test # all *.tftest.hcl files
terraform test -filter=tests/vpc.tftest.hcl
terraform test -junit-xml=test-results.xml # CI report (GA in 1.11)
mock_provider needs no credentials; drop it and use command = apply for integration tests against a sandbox account. Terraform destroys test-created resources at the end of the file.
Migrate from CDKTF¶
CDKTF was archived on 2025-12-10 and receives no updates. To move to plain HCL:
cdktf synth --hcl # writes .tf files under cdktf.out/stacks/<stack>
cd cdktf.out/stacks/my-stack
terraform init
terraform plan # expect no changes against the existing state
Keep the same backend and state; review and restructure the generated HCL into modules. Teams deeply tied to AWS CDK may migrate to AWS CDK instead (sunset notice).
Performance Optimization¶
| Strategy | Impact | When to use |
|---|---|---|
-parallelism=n |
More concurrent operations (default 10) | Large graphs; lower it if providers throttle |
-refresh=false |
Skip refresh | Short-lived runs when state is known to be current |
-minimal-refresh |
Refresh only resources with proposed changes | 1.17+ (beta as of 2026-09) |
-target |
Operate on specific resources | Emergency fixes only; not routine CI |
| State splitting | Smaller plans, smaller blast radius | 50+ resources per state or slow plans |
TF_PLUGIN_CACHE_DIR |
Reuse downloaded providers | CI pipelines, many root modules |
Common Issues¶
| Issue | Root cause | Resolution |
|---|---|---|
| State lock stuck | Crashed or cancelled apply | Confirm no run is active, then terraform force-unlock <ID> |
| Provider version conflict | Loose constraints or stale lock file | Constrain versions; terraform init -upgrade and commit the lock file |
| Cycle detected | Circular references | Restructure; split resources or use data sources |
| State drift | Manual changes | terraform plan to see it; terraform apply -refresh-only to accept, or apply to revert |
| Slow plan | Large state | Split state; -parallelism; -minimal-refresh (1.17) |
Saved plan is stale |
State changed after planning | Re-run terraform plan |
| Lock file missing platform hashes | Lock generated on a different OS/arch | terraform providers lock -platform=... |
Commands & Recipes¶
Core Workflow¶
terraform init # providers, modules, backend
terraform fmt -recursive
terraform validate
terraform plan -out=plan.tfplan
terraform apply plan.tfplan
terraform destroy
terraform graph -format=mermaid # 1.16+
terraform console -scope=module.vpc # 1.16+
State Commands¶
terraform state list
terraform state show aws_instance.web
terraform state show -json aws_instance.web # 1.16+
terraform state mv aws_instance.old aws_instance.new
terraform state rm aws_instance.orphan # forget, do not destroy
terraform import aws_instance.web i-1234567890abcdef0 # prefer import blocks
terraform force-unlock <LOCK_ID>
Module Pattern¶
# modules/vpc/main.tf
variable "cidr" {
type = string
default = "10.0.0.0/16"
}
variable "name" {
type = string
}
resource "aws_vpc" "main" {
cidr_block = var.cidr
enable_dns_hostnames = true
tags = { Name = var.name }
}
output "vpc_id" {
value = aws_vpc.main.id
}
# Root module usage
module "vpc" {
source = "./modules/vpc"
cidr = "10.0.0.0/16"
name = "production"
}
Workspace Commands¶
terraform workspace new staging
terraform workspace select staging
terraform workspace list
terraform workspace delete staging