Split View: Terraform 모듈 설계 패턴과 State 관리 운영 플레이북 2026
Terraform 모듈 설계 패턴과 State 관리 운영 플레이북 2026

- 개요
- 모듈 설계 원칙
- 모듈 컴포지션 패턴
- State 관리 전략
- 원격 백엔드 구성
- State 잠금과 동시성
- State 마이그레이션
- Terraform vs OpenTofu 비교
- Terratest 테스트
- 트러블슈팅
- 운영 체크리스트
- 실패 사례와 복구 절차
- 참고자료
개요
2026년 현재 Terraform은 1.11 버전까지 릴리스되면서 Ephemeral Values, Write-Only Attributes 같은 보안 중심 기능이 대폭 강화되었고, OpenTofu는 State 암호화를 독자적으로 지원하며 누적 다운로드 1,000만 건에 근접하고 있다. 이 글에서는 Terraform 모듈을 실무에서 어떻게 설계하고 조합하는지, 그리고 State를 팀 단위로 안전하게 운영하기 위한 플레이북을 다룬다. 단순한 Remote Backend 설정 수준을 넘어, 모듈 컴포지션 아키텍처부터 Terratest 기반 검증, State 마이그레이션 시나리오별 복구 절차까지 IaC 운영의 전체 라이프사이클을 포괄한다.
이 글이 다루는 범위는 다음과 같다.
- 모듈 단일 책임 원칙과 인터페이스 설계
- Root Module, Child Module, Composition Module의 계층 구조
- S3, GCS, Azure Blob 등 원격 백엔드 비교와 선택 기준
- Workspace vs Directory 기반 환경 분리 전략
- moved, import, removed 블록을 활용한 State 마이그레이션
- Terratest와 terraform test를 병행하는 테스트 전략
- 실제 장애 사례 기반 트러블슈팅과 복구 절차
모듈 설계 원칙
Terraform 모듈을 올바르게 설계하려면 소프트웨어 엔지니어링의 핵심 원칙을 HCL 코드에 적용해야 한다. 가장 중요한 원칙은 **단일 책임 원칙(Single Responsibility Principle)**이다. 하나의 모듈은 네트워킹, 컴퓨트, 스토리지 중 하나의 도메인만 담당해야 한다.
모듈 디렉터리 구조
잘 설계된 Terraform 모듈은 표준 구조를 따른다. HashiCorp의 공식 가이드라인에서 권장하는 구조를 기반으로 실무에서 검증된 레이아웃을 소개한다.
# 표준 모듈 디렉터리 구조
modules/
networking/
main.tf # VPC, Subnet, Route Table 등 핵심 리소스
variables.tf # 입력 변수 정의 (CIDR, AZ, 태그 등)
outputs.tf # VPC ID, Subnet ID 등 출력값
versions.tf # required_providers, required_version
README.md # 모듈 사용법 문서
tests/ # terraform test 또는 Terratest 코드
networking_test.go
compute/
main.tf
variables.tf
outputs.tf
versions.tf
iam.tf # 컴퓨트 전용 IAM Role/Policy
userdata.tf # Launch Template, User Data
database/
main.tf
variables.tf
outputs.tf
versions.tf
security_group.tf # DB 전용 Security Group
변수 검증(Variable Validation)
Terraform 1.9 이상에서는 변수에 복잡한 검증 규칙을 설정할 수 있다. 잘못된 입력값이 plan 단계에서 조기에 차단되므로 운영 안정성이 크게 향상된다.
# variables.tf - 변수 검증 규칙 예시
variable "environment" {
type = string
description = "배포 환경 (dev, staging, prod)"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment는 dev, staging, prod 중 하나여야 합니다."
}
}
variable "vpc_cidr" {
type = string
description = "VPC CIDR 블록"
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "유효한 CIDR 형식이어야 합니다 (예: 10.0.0.0/16)."
}
validation {
condition = tonumber(split("/", var.vpc_cidr)[1]) >= 16 && tonumber(split("/", var.vpc_cidr)[1]) <= 24
error_message = "VPC CIDR의 프리픽스 길이는 /16에서 /24 사이여야 합니다."
}
}
variable "instance_type" {
type = string
default = "t3.medium"
description = "EC2 인스턴스 타입"
validation {
condition = can(regex("^(t3|t3a|m5|m6i|c5|c6i)\\.", var.instance_type))
error_message = "승인된 인스턴스 패밀리만 사용 가능합니다: t3, t3a, m5, m6i, c5, c6i."
}
}
모듈 인터페이스 설계 원칙
모듈의 입출력은 API 계약과 같다. 다음 원칙을 지키면 모듈의 재사용성과 유지보수성이 극대화된다.
| 원칙 | 설명 | 예시 |
|---|---|---|
| 최소 노출 | 꼭 필요한 출력만 내보낸다 | VPC ID는 출력하되 내부 Route Table ID는 숨긴다 |
| 명시적 의존성 | 암묵적 의존 대신 변수로 주입한다 | vpc_id = module.networking.vpc_id |
| 기본값 제공 | 선택적 변수에는 합리적 기본값을 설정한다 | default = "t3.medium" |
| 타입 제약 | object, map, list 등 구체적 타입을 사용한다 | type = map(object(...)) |
| 설명 필수 | 모든 변수와 출력에 description을 작성한다 | 신규 팀원의 온보딩 시간을 단축시킨다 |
모듈 컴포지션 패턴
실무에서 인프라는 단일 모듈로 구성되지 않는다. 여러 모듈을 조합하여 완성된 환경을 만드는 컴포지션(Composition) 패턴이 핵심이다.
계층별 모듈 구성
Terraform 모듈은 크게 세 가지 계층으로 나눌 수 있다.
Leaf Module(기본 모듈): 단일 리소스 또는 밀접하게 관련된 리소스 그룹을 관리한다. 예를 들어 VPC 모듈은 VPC, Subnet, Internet Gateway, NAT Gateway를 포함한다. 이 모듈은 다른 모듈에 의존하지 않고 독립적으로 테스트할 수 있어야 한다.
Composition Module(조합 모듈): 여러 Leaf Module을 조합하여 하나의 서비스 스택을 구성한다. 웹 서비스 스택이라면 networking + compute + database + monitoring 모듈을 조합한다. Composition Module 자체는 리소스를 직접 생성하지 않고, 하위 모듈의 출력을 다른 모듈의 입력으로 연결하는 역할만 한다.
Root Module(루트 모듈): 실제로 terraform apply를 실행하는 최상위 진입점이다. 환경별(dev, staging, prod) 디렉터리에 위치하며, Composition Module을 호출하고 환경 특화 변수를 주입한다.
# environments/prod/main.tf - Root Module 예시
module "web_service" {
source = "../../compositions/web-service"
environment = "prod"
vpc_cidr = "10.1.0.0/16"
instance_type = "m6i.xlarge"
min_size = 3
max_size = 10
db_instance_class = "db.r6g.xlarge"
multi_az = true
tags = {
Team = "platform"
CostCenter = "engineering"
ManagedBy = "terraform"
}
}
# compositions/web-service/main.tf - Composition Module 예시
module "networking" {
source = "../../modules/networking"
vpc_cidr = var.vpc_cidr
environment = var.environment
tags = var.tags
}
module "compute" {
source = "../../modules/compute"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.private_subnet_ids
instance_type = var.instance_type
min_size = var.min_size
max_size = var.max_size
environment = var.environment
tags = var.tags
}
module "database" {
source = "../../modules/database"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.database_subnet_ids
instance_class = var.db_instance_class
multi_az = var.multi_az
security_group_id = module.compute.app_security_group_id
environment = var.environment
tags = var.tags
}
Terraform 1.10/1.11 Ephemeral Values 활용
Terraform 1.10에서 도입된 Ephemeral Values와 1.11의 Write-Only Attributes는 모듈 간 민감 정보 전달 방식을 근본적으로 변경했다. 기존에는 데이터베이스 패스워드 같은 비밀 정보가 State 파일에 평문으로 저장되었지만, 이제는 State에 기록하지 않는 방식으로 전달할 수 있다.
# Terraform 1.10+ Ephemeral Resource 예시
ephemeral "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db/master-password"
}
resource "aws_db_instance" "main" {
identifier = "prod-primary"
engine = "postgres"
engine_version = "16.2"
instance_class = "db.r6g.xlarge"
# write_only 속성 - State에 저장되지 않음 (Terraform 1.11+)
password_wo = ephemeral.aws_secretsmanager_secret_version.db_password.secret_string
password_wo_version = 1
}
이 접근법의 핵심은 password_wo 값이 plan 파일이나 state 파일에 전혀 기록되지 않는다는 점이다. 패스워드를 변경해야 할 때는 password_wo_version 값을 증가시키면 Terraform이 업데이트를 트리거한다.
State 관리 전략
State 관리는 Terraform 운영의 가장 핵심적인 영역이다. 올바른 전략 없이는 팀 협업이 불가능하고, 잘못된 운영은 인프라 전체를 위험에 빠뜨릴 수 있다.
Workspace vs Directory 환경 분리
환경(dev, staging, prod)을 분리하는 두 가지 주요 전략이 있다. 각각의 장단점을 명확히 이해하고 선택해야 한다.
| 비교 항목 | Workspace 방식 | Directory 방식 |
|---|---|---|
| State 파일 위치 | 같은 백엔드, 다른 키 | 완전히 분리된 백엔드 |
| 코드 중복 | 없음 (하나의 코드베이스) | 있음 (환경별 디렉터리) |
| 환경별 차이 표현 | 조건문, tfvars 파일 | 각 디렉터리에서 직접 설정 |
| 실수로 인한 영향 범위 | prod workspace에서 dev 변경 가능 | 물리적으로 분리되어 안전 |
| 팀 규모 적합성 | 소규모 팀 (5명 이하) | 중대규모 팀 |
| CI/CD 파이프라인 | 단일 파이프라인 + workspace 변수 | 환경별 독립 파이프라인 |
| 권장 시나리오 | 개인 프로젝트, 소규모 서비스 | 프로덕션 운영, 엔터프라이즈 |
실무에서는 Directory 방식이 압도적으로 권장된다. Workspace 방식은 terraform workspace select prod를 실수로 실행한 상태에서 dev 변경을 apply하면 프로덕션에 영향이 갈 수 있기 때문이다. Directory 방식은 물리적 분리를 통해 이런 실수를 원천 차단한다.
# Directory 기반 환경 분리 구조
infrastructure/
modules/ # 재사용 모듈
environments/
dev/
main.tf # module source = "../../modules/..."
backend.tf # S3 key = "dev/terraform.tfstate"
terraform.tfvars
staging/
main.tf
backend.tf # S3 key = "staging/terraform.tfstate"
terraform.tfvars
prod/
main.tf
backend.tf # S3 key = "prod/terraform.tfstate"
terraform.tfvars
원격 백엔드 구성
클라우드별 원격 백엔드 비교
| 비교 항목 | AWS S3 + DynamoDB | GCS (Google Cloud Storage) | Azure Blob Storage |
|---|---|---|---|
| State 저장소 | S3 Bucket | GCS Bucket | Blob Container |
| State Locking | DynamoDB Table | GCS 내장 잠금 | Azure Blob Lease |
| 암호화 (저장 시) | SSE-S3, SSE-KMS | Google-managed, CMEK | Microsoft-managed, CMEK |
| 암호화 (전송 시) | TLS 1.2+ | TLS 1.2+ | TLS 1.2+ |
| 버전 관리 | S3 Versioning | Object Versioning | Blob Versioning |
| 추가 비용 | DynamoDB 별도 과금 | 추가 비용 없음 | 추가 비용 없음 |
| 설정 복잡도 | 높음 (2개 서비스) | 낮음 (1개 서비스) | 중간 |
| IAM 통합 | AWS IAM | Google IAM | Azure RBAC |
S3 + DynamoDB 백엔드 구성 (프로덕션 권장)
프로덕션 환경에서 가장 널리 사용되는 S3 백엔드의 완전한 부트스트랩 구성이다.
# bootstrap/main.tf - State 백엔드 인프라 부트스트랩
terraform {
required_version = ">= 1.9.0"
}
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_s3_bucket" "terraform_state" {
bucket = "mycompany-terraform-state-prod"
lifecycle {
prevent_destroy = true
}
tags = {
Name = "Terraform State"
ManagedBy = "bootstrap"
}
}
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
}
bucket_key_enabled = true
}
}
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
}
resource "aws_kms_key" "terraform_state" {
description = "KMS key for Terraform state encryption"
deletion_window_in_days = 30
enable_key_rotation = true
}
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
tags = {
Name = "Terraform State Lock"
ManagedBy = "bootstrap"
}
}
# 사용하는 측의 backend.tf
# terraform {
# backend "s3" {
# bucket = "mycompany-terraform-state-prod"
# key = "prod/networking/terraform.tfstate"
# region = "ap-northeast-2"
# encrypt = true
# kms_key_id = "arn:aws:kms:ap-northeast-2:123456789:key/xxx"
# dynamodb_table = "terraform-state-lock"
# }
# }
State 잠금과 동시성
State 잠금(Locking)은 여러 사용자가 동시에 terraform apply를 실행하는 상황에서 State 파일 손상을 방지하는 핵심 메커니즘이다. 잠금 없이 두 명의 엔지니어가 동시에 apply를 실행하면, 한 쪽의 변경이 다른 쪽에 의해 덮어씌워질 수 있다.
잠금 동작 원리
Terraform은 State 수정이 필요한 모든 작업(plan, apply, destroy, state 서브커맨드 등)에서 잠금을 획득한다. 잠금이 이미 존재하면 대기하거나 오류를 반환한다.
# 잠금 충돌 시 나타나는 메시지
Error: Error acquiring the state lock
Lock Info:
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Path: s3://mycompany-terraform-state-prod/prod/terraform.tfstate
Operation: OperationTypeApply
Who: engineer@workstation
Version: 1.11.0
Created: 2026-03-04 09:15:23.456789 +0000 UTC
# 잠금 강제 해제 (비상 시에만 사용)
terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890
# 잠금 해제 전 반드시 확인할 사항
# 1. 해당 Lock ID의 작업이 정말 종료되었는지 확인
# 2. 다른 팀원에게 현재 apply 중인지 슬랙으로 확인
# 3. DynamoDB 테이블에서 해당 Lock 항목 직접 확인
aws dynamodb get-item \
--table-name terraform-state-lock \
--key '{"LockID": {"S": "mycompany-terraform-state-prod/prod/terraform.tfstate"}}'
동시성 제어 모범 사례
State 잠금만으로는 완벽한 동시성 제어가 불가능하다. CI/CD 파이프라인에서는 다음과 같은 추가 조치가 필요하다.
첫째, 직렬 실행 보장이다. GitHub Actions에서는 concurrency 키를 사용하여 같은 환경에 대한 동시 배포를 방지한다. concurrency: group: terraform-prod와 같이 설정하면 하나의 워크플로우만 실행된다. 둘째, Plan과 Apply 분리이다. PR에서는 plan만 실행하고, 머지 후에 apply를 실행하는 패턴을 적용한다. 셋째, State 접근 권한 최소화이다. 프로덕션 State에 쓰기 권한이 있는 IAM 역할은 CI/CD 파이프라인의 apply 단계에서만 Assume할 수 있도록 제한한다.
State 마이그레이션
State 마이그레이션은 리소스 이름 변경, 모듈 재구성, 백엔드 전환 등의 상황에서 필수적으로 발생한다. Terraform 1.1부터 도입된 moved 블록과 Terraform 1.5부터 도입된 import 블록이 마이그레이션 작업을 대폭 간소화했다.
moved 블록을 활용한 리팩터링
moved 블록은 리소스의 주소가 변경될 때 State를 자동으로 업데이트해준다. 기존의 terraform state mv 명령어와 달리 코드에 선언적으로 마이그레이션 의도를 표현하므로, 팀 전체가 같은 마이그레이션 경로를 따르게 된다.
# 리소스 이름 변경: aws_instance.web -> aws_instance.web_server
moved {
from = aws_instance.web
to = aws_instance.web_server
}
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
}
# 모듈로 이동: aws_vpc.main -> module.networking.aws_vpc.main
moved {
from = aws_vpc.main
to = module.networking.aws_vpc.main
}
# 모듈 이름 변경
moved {
from = module.old_networking
to = module.networking
}
# for_each 키 변경
moved {
from = aws_subnet.private["az-a"]
to = aws_subnet.private["ap-northeast-2a"]
}
HashiCorp은 공유 모듈의 경우 moved 블록을 코드에 영구적으로 유지할 것을 권장한다. 이는 다양한 버전의 모듈을 사용하는 소비자들이 안전하게 업그레이드할 수 있도록 보장한다.
import 블록을 활용한 기존 리소스 가져오기
# import 블록 (Terraform 1.5+) - 선언적 임포트
import {
to = aws_s3_bucket.existing_logs
id = "my-existing-log-bucket"
}
resource "aws_s3_bucket" "existing_logs" {
bucket = "my-existing-log-bucket"
}
# terraform plan으로 차이점 미리 확인
# terraform apply로 State에 반영
백엔드 마이그레이션 절차
로컬 백엔드에서 S3 원격 백엔드로 전환하는 경우 다음 절차를 따른다.
# 1. 현재 State 백업
cp terraform.tfstate terraform.tfstate.backup.$(date +%Y%m%d_%H%M%S)
# 2. backend.tf 파일 생성/수정
cat > backend.tf << 'HEREDOC'
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "services/web/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
HEREDOC
# 3. terraform init으로 마이그레이션 실행
terraform init -migrate-state
# 4. 마이그레이션 확인
terraform state list
# 5. 원격 State 검증
terraform plan # No changes expected
# 6. 로컬 State 파일 정리 (검증 완료 후)
rm terraform.tfstate terraform.tfstate.backup
Terraform vs OpenTofu 비교
2024년 HashiCorp이 Terraform 라이선스를 BSL(Business Source License)로 변경한 이후, Linux Foundation이 관리하는 OpenTofu 포크가 빠르게 성장하고 있다. 2026년 초 기준 OpenTofu는 GitHub 릴리스만으로 누적 약 980만 건의 다운로드를 기록하며 실제 프로덕션 도입이 확대되고 있다.
| 비교 항목 | Terraform (HashiCorp) | OpenTofu (Linux Foundation) |
|---|---|---|
| 라이선스 | BSL 1.1 (상업적 제한 있음) | MPL 2.0 (완전한 오픈소스) |
| 거버넌스 | HashiCorp 단독 | 커뮤니티 투표 기반 |
| State 암호화 | 미지원 (외부 도구 필요) | 네이티브 지원 |
| Ephemeral Values | 1.10+ 지원 | 별도 구현 |
| Write-Only Attributes | 1.11+ 지원 | 1.11 동등 기능 (2025.07 릴리스) |
| Provider 호환성 | 완전 호환 | 99% 이상 호환 |
| Registry | registry.terraform.io | registry.opentofu.org + 호환 |
| 상용 지원 | HCP Terraform, Terraform Enterprise | Spacelift, env0, Scalr 등 |
| 성능 | 동일 아키텍처 | 동일 아키텍처 (차이 미미) |
선택 기준을 정리하면 다음과 같다. 라이선스 제약이 없고 기존 HashiCorp 에코시스템(Vault, Consul 등)을 사용 중이라면 Terraform이 안전한 선택이다. 반면 오픈소스 라이선스가 필수이거나, State 네이티브 암호화가 필요하거나, 커뮤니티 주도 개발을 선호한다면 OpenTofu가 적합하다. 두 도구의 HCL 구문과 CLI 워크플로우는 거의 동일하므로 전환 비용은 낮다.
Terratest 테스트
인프라 코드도 애플리케이션 코드와 마찬가지로 자동화된 테스트가 필수다. Terratest는 Gruntwork에서 개발한 Go 기반 테스트 라이브러리로, 실제 클라우드 리소스를 프로비저닝하고 검증한 후 정리하는 엔드투엔드 테스트를 지원한다.
Terratest vs terraform test 비교
Terraform 1.6부터 내장된 terraform test 명령어와 Terratest는 서로 다른 테스트 영역을 담당한다. 실무에서는 두 도구를 병행 사용하는 것이 효과적이다.
- terraform test: HCL로 작성하며, plan 수준의 단위 테스트에 적합하다. 별도 언어 학습이 불필요하고 실행이 빠르다.
- Terratest: Go로 작성하며, 실제 리소스를 배포하고 HTTP 요청, SSH 접속 등 통합 테스트를 수행한다. 테스트 범위가 넓지만 실행 시간이 길고 비용이 발생한다.
Terratest 코드 예시
// test/networking_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestNetworkingModule(t *testing.T) {
t.Parallel()
awsRegion := "ap-northeast-2"
terraformOptions := &terraform.Options{
TerraformDir: "../modules/networking",
Vars: map[string]interface{}{
"environment": "test",
"vpc_cidr": "10.99.0.0/16",
},
EnvVars: map[string]string{
"AWS_DEFAULT_REGION": awsRegion,
},
}
// 테스트 종료 시 리소스 정리
defer terraform.Destroy(t, terraformOptions)
// 인프라 배포
terraform.InitAndApply(t, terraformOptions)
// 출력값 검증
vpcID := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcID)
// AWS API로 실제 리소스 검증
vpc := aws.GetVpcById(t, vpcID, awsRegion)
assert.Equal(t, "10.99.0.0/16", vpc.CidrBlock)
// Subnet 수 검증
privateSubnetIDs := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
assert.Equal(t, 3, len(privateSubnetIDs))
// 태그 검증
actualTags := aws.GetTagsForVpc(t, vpcID, awsRegion)
assert.Equal(t, "test", actualTags["Environment"])
}
terraform test 예시
# tests/networking.tftest.hcl
provider "aws" {
region = "ap-northeast-2"
}
variables {
environment = "test"
vpc_cidr = "10.99.0.0/16"
}
run "vpc_creation" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.99.0.0/16"
error_message = "VPC CIDR 블록이 예상값과 다릅니다."
}
assert {
condition = aws_vpc.main.tags["Environment"] == "test"
error_message = "Environment 태그가 올바르지 않습니다."
}
}
run "subnet_count" {
command = plan
assert {
condition = length(aws_subnet.private) == 3
error_message = "Private Subnet은 3개여야 합니다."
}
}
트러블슈팅
Terraform 운영 중 가장 흔하게 발생하는 문제들과 해결 방법을 정리한다.
State Lock이 해제되지 않는 경우
CI/CD 파이프라인이 중간에 종료되거나, terraform apply 실행 중 네트워크 단절이 발생하면 State Lock이 남아있을 수 있다.
# 1. 현재 Lock 상태 확인
aws dynamodb scan \
--table-name terraform-state-lock \
--filter-expression "attribute_exists(LockID)"
# 2. Lock 소유자 확인 후 안전하게 해제
terraform force-unlock <LOCK_ID>
# 주의: force-unlock은 반드시 다른 apply가 실행 중이지 않음을 확인한 후 사용
State와 실제 인프라 불일치
수동으로 AWS Console에서 리소스를 변경하면 State와 실제 인프라 간 드리프트가 발생한다.
# 드리프트 감지
terraform plan -detailed-exitcode
# Exit code 0: 변경 없음
# Exit code 1: 오류
# Exit code 2: 변경 있음 (드리프트 감지)
# 특정 리소스 State 갱신 (실제 인프라 기준으로 State 업데이트)
terraform apply -refresh-only -target=aws_instance.web_server
# State에서 리소스 제거 (실제 인프라는 유지)
terraform state rm aws_instance.legacy_server
State 파일 손상
극히 드물지만 State 파일이 손상될 수 있다. S3 버저닝을 활성화해 두었다면 이전 버전으로 복구가 가능하다.
# S3에서 State 파일 이전 버전 목록 확인
aws s3api list-object-versions \
--bucket mycompany-terraform-state-prod \
--prefix prod/terraform.tfstate
# 특정 버전으로 복구
aws s3api get-object \
--bucket mycompany-terraform-state-prod \
--key prod/terraform.tfstate \
--version-id "abc123def456" \
terraform.tfstate.recovered
# 복구된 State 검증
terraform show terraform.tfstate.recovered
# State 파일 교체 (DynamoDB Lock 확인 후)
aws s3 cp terraform.tfstate.recovered \
s3://mycompany-terraform-state-prod/prod/terraform.tfstate
운영 체크리스트
Terraform IaC 운영의 각 단계별로 반드시 확인해야 할 항목을 정리한다.
모듈 릴리스 전 체크리스트
- 모든 변수에
description과type이 정의되어 있는가 validation블록으로 입력값을 검증하고 있는가outputs.tf에 필요한 출력값만 노출하고 있는가versions.tf에required_version과required_providers가 명시되어 있는가- README.md에 사용 예시와 입출력 설명이 있는가
- CHANGELOG.md에 변경 내역이 기록되어 있는가
- Terratest 또는 terraform test가 통과하는가
terraform fmt와terraform validate가 성공하는가
State 관리 체크리스트
- 원격 백엔드가 구성되어 있는가 (로컬 State 사용 금지)
- State Locking이 활성화되어 있는가
- State 저장소에 버저닝이 활성화되어 있는가
- State 저장소가 암호화(KMS/CMEK)되어 있는가
- 퍼블릭 접근이 차단되어 있는가
- 환경별 State가 완전히 분리되어 있는가 (Directory 방식 권장)
- State 접근 IAM 정책이 최소 권한 원칙을 따르고 있는가
- CI/CD에서 동시 실행 방지가 설정되어 있는가
변경 적용 전 체크리스트
terraform plan출력을 반드시 리뷰했는가- destroy 대상 리소스가 의도한 것인가
- 민감 정보가 코드에 하드코딩되어 있지 않은가
moved블록 사용 시from과to가 정확한가- 프로덕션 변경은 별도 승인 프로세스를 거쳤는가
실패 사례와 복구 절차
사례 1: 잘못된 terraform destroy
한 엔지니어가 dev 환경에서 작업하다가 prod workspace가 선택된 상태에서 terraform destroy를 실행한 사고다. Workspace 방식의 치명적 약점을 보여주는 사례다.
복구 절차:
- 즉시
Ctrl+C로 destroy 중단 (진행 중인 경우) - S3 버저닝으로 이전 State 버전 확인
- 이전 State로 롤백
terraform plan으로 삭제된 리소스 식별terraform apply로 삭제된 리소스 재생성- 근본 대책: Workspace 방식에서 Directory 방식으로 전환
이 사례가 Directory 방식이 권장되는 가장 큰 이유다. prod 디렉터리에서 작업하려면 명시적으로 cd environments/prod를 실행해야 하고, CI/CD 파이프라인도 환경별로 분리된다.
사례 2: State Lock 데드락
DynamoDB Lock이 해제되지 않아 모든 팀원이 apply를 실행할 수 없는 상황이다.
복구 절차:
- DynamoDB에서 Lock 항목의
Info컬럼 확인 (누가, 언제, 어떤 작업) - 해당 엔지니어에게 작업 상태 확인 (슬랙, 메신저 등)
- 작업이 이미 종료된 것이 확인되면
terraform force-unlock실행 terraform plan으로 State 정합성 검증- 근본 대책: CI/CD 파이프라인에 타임아웃 설정, graceful shutdown 핸들러 추가
사례 3: State 파일에 민감 정보 노출
RDS 패스워드가 State 파일에 평문으로 저장된 것이 감사에서 발견된 사례다.
복구 절차:
- S3 버킷의 접근 로그 확인으로 비인가 접근 여부 파악
- 노출된 패스워드 즉시 변경
- Terraform 1.10+ Ephemeral Resources 또는 1.11+ Write-Only Attributes로 마이그레이션
- State 파일의 이전 버전도 민감 정보를 포함하므로, S3 Lifecycle Policy로 이전 버전 삭제 또는 만료 설정
- 근본 대책: OpenTofu의 State 암호화 기능 도입 검토, 또는 Terraform 1.11 Write-Only Attributes 전면 적용
참고자료
- Terraform 1.10 - Ephemeral Values로 State의 비밀 관리 개선
- Terraform 1.11 - Write-Only Arguments로 Managed Resources에 Ephemeral Values 적용
- HashiCorp - Terraform 모듈 생성 권장 패턴
- Terraform moved 블록으로 리팩터링
- Terratest - 인프라 코드 자동화 테스트 라이브러리
- Spacelift - OpenTofu vs Terraform 비교
- AWS - Terraform Backend 모범 사례
- Google Cloud - Terraform 스타일 및 구조 모범 사례
- Terraform State 리팩터링 공식 문서
Terraform Module Design Patterns and State Management Operations Playbook 2026

- Overview
- Module Design Principles
- Module Composition Patterns
- State Management Strategy
- Remote Backend Configuration
- State Locking and Concurrency
- State Migration
- Terraform vs OpenTofu Comparison
- Terratest Testing
- Troubleshooting
- Operations Checklist
- Failure Cases and Recovery Procedures
- References
- Quiz
Overview
As of 2026, Terraform has reached version 1.11 with significantly enhanced security-focused features like Ephemeral Values and Write-Only Attributes, while OpenTofu independently supports State encryption and is approaching 10 million cumulative downloads. This article covers how to design and compose Terraform modules in practice, along with a playbook for safely operating State at the team level. Going beyond simple Remote Backend configuration, it encompasses the entire IaC operations lifecycle from module composition architecture to Terratest-based verification and State migration recovery procedures for each scenario.
The scope of this article is as follows:
- Module single responsibility principle and interface design
- Hierarchical structure of Root Module, Child Module, and Composition Module
- Remote backend comparison and selection criteria for S3, GCS, Azure Blob, etc.
- Workspace vs Directory-based environment isolation strategies
- State migration using moved, import, and removed blocks
- Testing strategies combining Terratest and terraform test
- Troubleshooting and recovery procedures based on real incident cases
Module Design Principles
To design Terraform modules correctly, you need to apply core software engineering principles to HCL code. The most important principle is the Single Responsibility Principle (SRP). A single module should handle only one domain: networking, compute, or storage.
Module Directory Structure
Well-designed Terraform modules follow a standard structure. Here is a layout verified in practice, based on the structure recommended by HashiCorp's official guidelines.
# Standard module directory structure
modules/
networking/
main.tf # Core resources: VPC, Subnet, Route Table, etc.
variables.tf # Input variable definitions (CIDR, AZ, tags, etc.)
outputs.tf # Output values: VPC ID, Subnet ID, etc.
versions.tf # required_providers, required_version
README.md # Module usage documentation
tests/ # terraform test or Terratest code
networking_test.go
compute/
main.tf
variables.tf
outputs.tf
versions.tf
iam.tf # Compute-specific IAM Role/Policy
userdata.tf # Launch Template, User Data
database/
main.tf
variables.tf
outputs.tf
versions.tf
security_group.tf # DB-specific Security Group
Variable Validation
From Terraform 1.9 onward, you can set complex validation rules on variables. Invalid input values are blocked early at the plan stage, greatly improving operational stability.
# variables.tf - Variable validation rule examples
variable "environment" {
type = string
description = "Deployment environment (dev, staging, prod)"
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "environment must be one of dev, staging, or prod."
}
}
variable "vpc_cidr" {
type = string
description = "VPC CIDR block"
validation {
condition = can(cidrhost(var.vpc_cidr, 0))
error_message = "Must be a valid CIDR format (e.g., 10.0.0.0/16)."
}
validation {
condition = tonumber(split("/", var.vpc_cidr)[1]) >= 16 && tonumber(split("/", var.vpc_cidr)[1]) <= 24
error_message = "VPC CIDR prefix length must be between /16 and /24."
}
}
variable "instance_type" {
type = string
default = "t3.medium"
description = "EC2 instance type"
validation {
condition = can(regex("^(t3|t3a|m5|m6i|c5|c6i)\\.", var.instance_type))
error_message = "Only approved instance families are allowed: t3, t3a, m5, m6i, c5, c6i."
}
}
Module Interface Design Principles
Module inputs and outputs are like API contracts. Following these principles maximizes reusability and maintainability.
| Principle | Description | Example |
|---|---|---|
| Minimal exposure | Export only necessary outputs | Export VPC ID but hide internal Route Table IDs |
| Explicit dependencies | Inject via variables instead of implicit dependencies | vpc_id = module.networking.vpc_id |
| Provide defaults | Set reasonable defaults for optional variables | default = "t3.medium" |
| Type constraints | Use specific types like object, map, list | type = map(object(...)) |
| Description required | Write descriptions for all variables and outputs | Reduces onboarding time for new team members |
Module Composition Patterns
In practice, infrastructure is never composed of a single module. The key is the Composition pattern that combines multiple modules to create a complete environment.
Module Hierarchy
Terraform modules can be broadly divided into three layers.
Leaf Module (Basic Module): Manages a single resource or a closely related group of resources. For example, a VPC module includes VPC, Subnet, Internet Gateway, and NAT Gateway. This module should be independently testable without depending on other modules.
Composition Module: Combines multiple Leaf Modules to form a single service stack. A web service stack would combine networking + compute + database + monitoring modules. The Composition Module itself does not create resources directly; it only connects outputs from child modules to inputs of other modules.
Root Module: The top-level entry point where terraform apply is actually executed. It is located in per-environment directories (dev, staging, prod) and calls Composition Modules while injecting environment-specific variables.
# environments/prod/main.tf - Root Module example
module "web_service" {
source = "../../compositions/web-service"
environment = "prod"
vpc_cidr = "10.1.0.0/16"
instance_type = "m6i.xlarge"
min_size = 3
max_size = 10
db_instance_class = "db.r6g.xlarge"
multi_az = true
tags = {
Team = "platform"
CostCenter = "engineering"
ManagedBy = "terraform"
}
}
# compositions/web-service/main.tf - Composition Module example
module "networking" {
source = "../../modules/networking"
vpc_cidr = var.vpc_cidr
environment = var.environment
tags = var.tags
}
module "compute" {
source = "../../modules/compute"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.private_subnet_ids
instance_type = var.instance_type
min_size = var.min_size
max_size = var.max_size
environment = var.environment
tags = var.tags
}
module "database" {
source = "../../modules/database"
vpc_id = module.networking.vpc_id
subnet_ids = module.networking.database_subnet_ids
instance_class = var.db_instance_class
multi_az = var.multi_az
security_group_id = module.compute.app_security_group_id
environment = var.environment
tags = var.tags
}
Leveraging Terraform 1.10/1.11 Ephemeral Values
Ephemeral Values introduced in Terraform 1.10 and Write-Only Attributes in 1.11 fundamentally changed how sensitive information is passed between modules. Previously, secrets like database passwords were stored in plaintext in State files, but now they can be passed without being recorded in State.
# Terraform 1.10+ Ephemeral Resource example
ephemeral "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db/master-password"
}
resource "aws_db_instance" "main" {
identifier = "prod-primary"
engine = "postgres"
engine_version = "16.2"
instance_class = "db.r6g.xlarge"
# write_only attribute - Not stored in State (Terraform 1.11+)
password_wo = ephemeral.aws_secretsmanager_secret_version.db_password.secret_string
password_wo_version = 1
}
The key point of this approach is that the password_wo value is never recorded in plan files or state files. When the password needs to be changed, incrementing the password_wo_version value triggers Terraform to perform an update.
State Management Strategy
State management is the most critical area of Terraform operations. Without a proper strategy, team collaboration is impossible, and poor management can put the entire infrastructure at risk.
Workspace vs Directory Environment Isolation
There are two main strategies for isolating environments (dev, staging, prod). You need to clearly understand the pros and cons of each before choosing.
| Comparison Item | Workspace Approach | Directory Approach |
|---|---|---|
| State file location | Same backend, different keys | Completely separated backends |
| Code duplication | None (single codebase) | Exists (per-environment dirs) |
| Expressing env diffs | Conditionals, tfvars files | Direct configuration per dir |
| Accidental blast radius | Can modify dev in prod workspace | Physically separated, safe |
| Team size suitability | Small teams (5 or fewer) | Medium to large teams |
| CI/CD pipeline | Single pipeline + workspace variable | Independent pipeline per env |
| Recommended scenario | Personal projects, small services | Production ops, enterprise |
In practice, the Directory approach is overwhelmingly recommended. With the Workspace approach, accidentally running terraform workspace select prod and then applying dev changes can impact production. The Directory approach physically prevents such mistakes through separation.
# Directory-based environment isolation structure
infrastructure/
modules/ # Reusable modules
environments/
dev/
main.tf # module source = "../../modules/..."
backend.tf # S3 key = "dev/terraform.tfstate"
terraform.tfvars
staging/
main.tf
backend.tf # S3 key = "staging/terraform.tfstate"
terraform.tfvars
prod/
main.tf
backend.tf # S3 key = "prod/terraform.tfstate"
terraform.tfvars
Remote Backend Configuration
Cloud-Specific Remote Backend Comparison
| Comparison Item | AWS S3 + DynamoDB | GCS (Google Cloud Storage) | Azure Blob Storage |
|---|---|---|---|
| State storage | S3 Bucket | GCS Bucket | Blob Container |
| State Locking | DynamoDB Table | Built-in GCS locking | Azure Blob Lease |
| Encryption (at rest) | SSE-S3, SSE-KMS | Google-managed, CMEK | Microsoft-managed, CMEK |
| Encryption (transit) | TLS 1.2+ | TLS 1.2+ | TLS 1.2+ |
| Versioning | S3 Versioning | Object Versioning | Blob Versioning |
| Additional cost | DynamoDB billed separately | No additional cost | No additional cost |
| Config complexity | High (2 services) | Low (1 service) | Medium |
| IAM integration | AWS IAM | Google IAM | Azure RBAC |
S3 + DynamoDB Backend Configuration (Production Recommended)
This is the complete bootstrap configuration for the S3 backend, the most widely used in production environments.
# bootstrap/main.tf - State backend infrastructure bootstrap
terraform {
required_version = ">= 1.9.0"
}
provider "aws" {
region = "ap-northeast-2"
}
resource "aws_s3_bucket" "terraform_state" {
bucket = "mycompany-terraform-state-prod"
lifecycle {
prevent_destroy = true
}
tags = {
Name = "Terraform State"
ManagedBy = "bootstrap"
}
}
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
}
bucket_key_enabled = true
}
}
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
}
resource "aws_kms_key" "terraform_state" {
description = "KMS key for Terraform state encryption"
deletion_window_in_days = 30
enable_key_rotation = true
}
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
tags = {
Name = "Terraform State Lock"
ManagedBy = "bootstrap"
}
}
# Consumer-side backend.tf
# terraform {
# backend "s3" {
# bucket = "mycompany-terraform-state-prod"
# key = "prod/networking/terraform.tfstate"
# region = "ap-northeast-2"
# encrypt = true
# kms_key_id = "arn:aws:kms:ap-northeast-2:123456789:key/xxx"
# dynamodb_table = "terraform-state-lock"
# }
# }
State Locking and Concurrency
State Locking is a critical mechanism that prevents State file corruption when multiple users run terraform apply simultaneously. Without locking, if two engineers run apply at the same time, one's changes can be overwritten by the other.
How Locking Works
Terraform acquires a lock on all operations that require State modification (plan, apply, destroy, state subcommands, etc.). If a lock already exists, it waits or returns an error.
# Message displayed during lock conflict
Error: Error acquiring the state lock
Lock Info:
ID: a1b2c3d4-e5f6-7890-abcd-ef1234567890
Path: s3://mycompany-terraform-state-prod/prod/terraform.tfstate
Operation: OperationTypeApply
Who: engineer@workstation
Version: 1.11.0
Created: 2026-03-04 09:15:23.456789 +0000 UTC
# Force unlock (use only in emergencies)
terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890
# Items to verify before force unlocking
# 1. Confirm that the operation for that Lock ID has truly ended
# 2. Check with teammates via Slack if anyone is currently running apply
# 3. Directly check the Lock entry in the DynamoDB table
aws dynamodb get-item \
--table-name terraform-state-lock \
--key '{"LockID": {"S": "mycompany-terraform-state-prod/prod/terraform.tfstate"}}'
Concurrency Control Best Practices
State locking alone does not provide complete concurrency control. Additional measures are needed in CI/CD pipelines.
First, ensure serial execution. In GitHub Actions, use the concurrency key to prevent simultaneous deployments to the same environment. Setting concurrency: group: terraform-prod ensures only one workflow runs at a time. Second, separate Plan and Apply. Run only plan on PRs, and apply after merge. Third, minimize State access permissions. Restrict IAM roles with write access to production State so they can only be assumed during the apply stage of the CI/CD pipeline.
State Migration
State migration inevitably occurs during resource renaming, module restructuring, backend transitions, and similar situations. The moved block introduced in Terraform 1.1 and the import block introduced in Terraform 1.5 have greatly simplified migration work.
Refactoring with moved Blocks
The moved block automatically updates State when a resource's address changes. Unlike the traditional terraform state mv command, it declaratively expresses migration intent in code, ensuring the entire team follows the same migration path.
# Resource rename: aws_instance.web -> aws_instance.web_server
moved {
from = aws_instance.web
to = aws_instance.web_server
}
resource "aws_instance" "web_server" {
ami = "ami-0c55b159cbfafe1f0"
instance_type = "t3.medium"
}
# Move to module: aws_vpc.main -> module.networking.aws_vpc.main
moved {
from = aws_vpc.main
to = module.networking.aws_vpc.main
}
# Module rename
moved {
from = module.old_networking
to = module.networking
}
# for_each key change
moved {
from = aws_subnet.private["az-a"]
to = aws_subnet.private["ap-northeast-2a"]
}
HashiCorp recommends keeping moved blocks permanently in the code for shared modules. This ensures consumers using various module versions can safely upgrade.
Importing Existing Resources with import Blocks
# import block (Terraform 1.5+) - Declarative import
import {
to = aws_s3_bucket.existing_logs
id = "my-existing-log-bucket"
}
resource "aws_s3_bucket" "existing_logs" {
bucket = "my-existing-log-bucket"
}
# Preview differences with terraform plan
# Apply to State with terraform apply
Backend Migration Procedure
Follow this procedure when transitioning from a local backend to an S3 remote backend.
# 1. Back up the current State
cp terraform.tfstate terraform.tfstate.backup.$(date +%Y%m%d_%H%M%S)
# 2. Create/modify backend.tf
cat > backend.tf << 'HEREDOC'
terraform {
backend "s3" {
bucket = "mycompany-terraform-state"
key = "services/web/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
HEREDOC
# 3. Execute migration with terraform init
terraform init -migrate-state
# 4. Confirm migration
terraform state list
# 5. Verify remote State
terraform plan # No changes expected
# 6. Clean up local State files (after verification)
rm terraform.tfstate terraform.tfstate.backup
Terraform vs OpenTofu Comparison
After HashiCorp changed the Terraform license to BSL (Business Source License) in 2024, the OpenTofu fork managed by the Linux Foundation has been growing rapidly. As of early 2026, OpenTofu has recorded approximately 9.8 million cumulative downloads from GitHub releases alone, and production adoption is expanding.
| Comparison Item | Terraform (HashiCorp) | OpenTofu (Linux Foundation) |
|---|---|---|
| License | BSL 1.1 (commercial restrictions) | MPL 2.0 (fully open source) |
| Governance | HashiCorp sole | Community vote-based |
| State encryption | Not supported (external tools needed) | Native support |
| Ephemeral Values | 1.10+ supported | Separate implementation |
| Write-Only Attributes | 1.11+ supported | 1.11 equivalent (July 2025) |
| Provider compatibility | Full compatibility | Over 99% compatible |
| Registry | registry.terraform.io | registry.opentofu.org + compat |
| Commercial support | HCP Terraform, Terraform Enterprise | Spacelift, env0, Scalr, etc. |
| Performance | Same architecture | Same architecture (minimal diff) |
To summarize the selection criteria: If there are no license constraints and you are using the existing HashiCorp ecosystem (Vault, Consul, etc.), Terraform is the safe choice. On the other hand, if an open-source license is mandatory, native State encryption is needed, or you prefer community-driven development, OpenTofu is a good fit. The HCL syntax and CLI workflows of both tools are nearly identical, so the transition cost is low.
Terratest Testing
Infrastructure code, like application code, requires automated testing. Terratest is a Go-based testing library developed by Gruntwork that supports end-to-end testing by provisioning actual cloud resources, verifying them, and then cleaning up.
Terratest vs terraform test Comparison
The built-in terraform test command from Terraform 1.6 and Terratest cover different testing domains. In practice, using both tools together is most effective.
- terraform test: Written in HCL, suitable for plan-level unit testing. No separate language learning required and fast execution.
- Terratest: Written in Go, deploys actual resources and performs integration tests including HTTP requests, SSH connections, etc. Wider test coverage but longer execution time and costs involved.
Terratest Code Example
// test/networking_test.go
package test
import (
"testing"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)
func TestNetworkingModule(t *testing.T) {
t.Parallel()
awsRegion := "ap-northeast-2"
terraformOptions := &terraform.Options{
TerraformDir: "../modules/networking",
Vars: map[string]interface{}{
"environment": "test",
"vpc_cidr": "10.99.0.0/16",
},
EnvVars: map[string]string{
"AWS_DEFAULT_REGION": awsRegion,
},
}
// Clean up resources when test ends
defer terraform.Destroy(t, terraformOptions)
// Deploy infrastructure
terraform.InitAndApply(t, terraformOptions)
// Verify output values
vpcID := terraform.Output(t, terraformOptions, "vpc_id")
assert.NotEmpty(t, vpcID)
// Verify actual resources via AWS API
vpc := aws.GetVpcById(t, vpcID, awsRegion)
assert.Equal(t, "10.99.0.0/16", vpc.CidrBlock)
// Verify Subnet count
privateSubnetIDs := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
assert.Equal(t, 3, len(privateSubnetIDs))
// Verify tags
actualTags := aws.GetTagsForVpc(t, vpcID, awsRegion)
assert.Equal(t, "test", actualTags["Environment"])
}
terraform test Example
# tests/networking.tftest.hcl
provider "aws" {
region = "ap-northeast-2"
}
variables {
environment = "test"
vpc_cidr = "10.99.0.0/16"
}
run "vpc_creation" {
command = plan
assert {
condition = aws_vpc.main.cidr_block == "10.99.0.0/16"
error_message = "VPC CIDR block does not match the expected value."
}
assert {
condition = aws_vpc.main.tags["Environment"] == "test"
error_message = "Environment tag is incorrect."
}
}
run "subnet_count" {
command = plan
assert {
condition = length(aws_subnet.private) == 3
error_message = "There must be 3 Private Subnets."
}
}
Troubleshooting
Here is a summary of the most common problems encountered during Terraform operations and their solutions.
When State Lock Is Not Released
If a CI/CD pipeline terminates midway, or a network disconnection occurs during terraform apply, the State Lock may remain.
# 1. Check current Lock status
aws dynamodb scan \
--table-name terraform-state-lock \
--filter-expression "attribute_exists(LockID)"
# 2. Safely release after confirming Lock owner
terraform force-unlock <LOCK_ID>
# Warning: Only use force-unlock after confirming no other apply is in progress
State and Actual Infrastructure Mismatch
Manually changing resources in the AWS Console causes drift between State and the actual infrastructure.
# Detect drift
terraform plan -detailed-exitcode
# Exit code 0: No changes
# Exit code 1: Error
# Exit code 2: Changes present (drift detected)
# Refresh specific resource State (update State based on actual infrastructure)
terraform apply -refresh-only -target=aws_instance.web_server
# Remove resource from State (actual infrastructure is preserved)
terraform state rm aws_instance.legacy_server
State File Corruption
Though extremely rare, State files can become corrupted. If you have S3 versioning enabled, recovery from a previous version is possible.
# List previous versions of State file in S3
aws s3api list-object-versions \
--bucket mycompany-terraform-state-prod \
--prefix prod/terraform.tfstate
# Recover a specific version
aws s3api get-object \
--bucket mycompany-terraform-state-prod \
--key prod/terraform.tfstate \
--version-id "abc123def456" \
terraform.tfstate.recovered
# Verify recovered State
terraform show terraform.tfstate.recovered
# Replace State file (after confirming DynamoDB Lock)
aws s3 cp terraform.tfstate.recovered \
s3://mycompany-terraform-state-prod/prod/terraform.tfstate
Operations Checklist
Here is a summary of items that must be verified at each stage of Terraform IaC operations.
Pre-Module Release Checklist
- Are
descriptionandtypedefined for all variables - Are input values validated with
validationblocks - Does
outputs.tfexpose only necessary output values - Are
required_versionandrequired_providersspecified inversions.tf - Does the README.md contain usage examples and input/output descriptions
- Are changes recorded in CHANGELOG.md
- Do Terratest or terraform test pass
- Do
terraform fmtandterraform validatesucceed
State Management Checklist
- Is a remote backend configured (no local State usage)
- Is State Locking enabled
- Is versioning enabled on the State storage
- Is the State storage encrypted (KMS/CMEK)
- Is public access blocked
- Is State fully isolated per environment (Directory approach recommended)
- Does the State access IAM policy follow the principle of least privilege
- Is concurrent execution prevention configured in CI/CD
Pre-Change Application Checklist
- Have you reviewed the
terraform planoutput - Are the resources targeted for destroy intentional
- Is sensitive information not hardcoded in the code
- When using
movedblocks, arefromandtoaccurate - Has the production change gone through a separate approval process
Failure Cases and Recovery Procedures
Case 1: Accidental terraform destroy
An engineer was working in the dev environment but ran terraform destroy while the prod workspace was selected. This case demonstrates the critical weakness of the Workspace approach.
Recovery Procedure:
- Immediately press
Ctrl+Cto stop destroy (if in progress) - Check previous State versions via S3 versioning
- Roll back to the previous State
- Identify deleted resources with
terraform plan - Recreate deleted resources with
terraform apply - Root cause fix: Migrate from Workspace approach to Directory approach
This case is the biggest reason the Directory approach is recommended. Working in the prod directory requires explicitly running cd environments/prod, and CI/CD pipelines are also separated per environment.
Case 2: State Lock Deadlock
A situation where the DynamoDB Lock is not released, preventing all team members from running apply.
Recovery Procedure:
- Check the
Infocolumn of the Lock entry in DynamoDB (who, when, what operation) - Confirm the operation status with the engineer (via Slack, messenger, etc.)
- If confirmed that the operation has already ended, run
terraform force-unlock - Verify State consistency with
terraform plan - Root cause fix: Set timeouts in CI/CD pipelines, add graceful shutdown handlers
Case 3: Sensitive Information Exposed in State File
A case where an RDS password was found stored in plaintext in the State file during an audit.
Recovery Procedure:
- Check S3 bucket access logs to determine if unauthorized access occurred
- Immediately change the exposed password
- Migrate to Terraform 1.10+ Ephemeral Resources or 1.11+ Write-Only Attributes
- Previous State file versions also contain sensitive information, so set S3 Lifecycle Policy to delete or expire old versions
- Root cause fix: Consider adopting OpenTofu's State encryption feature, or fully apply Terraform 1.11 Write-Only Attributes
References
- Terraform 1.10 - Improved Secret Management in State with Ephemeral Values
- Terraform 1.11 - Ephemeral Values for Managed Resources with Write-Only Arguments
- HashiCorp - Terraform Module Creation Recommended Patterns
- Terraform moved Block for Refactoring
- Terratest - Infrastructure Code Automated Testing Library
- Spacelift - OpenTofu vs Terraform Comparison
- AWS - Terraform Backend Best Practices
- Google Cloud - Terraform Style and Structure Best Practices
- Terraform State Refactoring Official Documentation
Quiz
Q1: What is the main topic covered in "Terraform Module Design Patterns and State Management
Operations Playbook 2026"?
Terraform module design patterns and state management operations playbook. Everything about IaC operations from module composition, remote backend configuration, state locking/isolation/migration, to Terratest testing.
Q2: Describe the Module Design Principles.
To design Terraform modules correctly, you need to apply core software engineering principles to
HCL code. The most important principle is the Single Responsibility Principle (SRP). A single
module should handle only one domain: networking, compute, or storage.
Q3: Explain the core concept of Module Composition Patterns.
In practice, infrastructure is never composed of a single module. The key is the Composition
pattern that combines multiple modules to create a complete environment. Module Hierarchy
Terraform modules can be broadly divided into three layers.
Q4: What are the key aspects of State Management Strategy?
State management is the most critical area of Terraform operations. Without a proper strategy,
team collaboration is impossible, and poor management can put the entire infrastructure at risk.
Q5: What are the key steps for Remote Backend Configuration?
Cloud-Specific Remote Backend Comparison S3 + DynamoDB Backend Configuration (Production
Recommended) This is the complete bootstrap configuration for the S3 backend, the most widely used
in production environments.