Skip to content
Published on

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

Authors
  • Name
    Twitter
Terraform 모듈 설계 패턴과 State 관리 운영 플레이북 2026

개요

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 + DynamoDBGCS (Google Cloud Storage)Azure Blob Storage
State 저장소S3 BucketGCS BucketBlob Container
State LockingDynamoDB TableGCS 내장 잠금Azure Blob Lease
암호화 (저장 시)SSE-S3, SSE-KMSGoogle-managed, CMEKMicrosoft-managed, CMEK
암호화 (전송 시)TLS 1.2+TLS 1.2+TLS 1.2+
버전 관리S3 VersioningObject VersioningBlob Versioning
추가 비용DynamoDB 별도 과금추가 비용 없음추가 비용 없음
설정 복잡도높음 (2개 서비스)낮음 (1개 서비스)중간
IAM 통합AWS IAMGoogle IAMAzure 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 Values1.10+ 지원별도 구현
Write-Only Attributes1.11+ 지원1.11 동등 기능 (2025.07 릴리스)
Provider 호환성완전 호환99% 이상 호환
Registryregistry.terraform.ioregistry.opentofu.org + 호환
상용 지원HCP Terraform, Terraform EnterpriseSpacelift, 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 운영의 각 단계별로 반드시 확인해야 할 항목을 정리한다.

모듈 릴리스 전 체크리스트

  • 모든 변수에 descriptiontype이 정의되어 있는가
  • validation 블록으로 입력값을 검증하고 있는가
  • outputs.tf에 필요한 출력값만 노출하고 있는가
  • versions.tfrequired_versionrequired_providers가 명시되어 있는가
  • README.md에 사용 예시와 입출력 설명이 있는가
  • CHANGELOG.md에 변경 내역이 기록되어 있는가
  • Terratest 또는 terraform test가 통과하는가
  • terraform fmtterraform validate가 성공하는가

State 관리 체크리스트

  • 원격 백엔드가 구성되어 있는가 (로컬 State 사용 금지)
  • State Locking이 활성화되어 있는가
  • State 저장소에 버저닝이 활성화되어 있는가
  • State 저장소가 암호화(KMS/CMEK)되어 있는가
  • 퍼블릭 접근이 차단되어 있는가
  • 환경별 State가 완전히 분리되어 있는가 (Directory 방식 권장)
  • State 접근 IAM 정책이 최소 권한 원칙을 따르고 있는가
  • CI/CD에서 동시 실행 방지가 설정되어 있는가

변경 적용 전 체크리스트

  • terraform plan 출력을 반드시 리뷰했는가
  • destroy 대상 리소스가 의도한 것인가
  • 민감 정보가 코드에 하드코딩되어 있지 않은가
  • moved 블록 사용 시 fromto가 정확한가
  • 프로덕션 변경은 별도 승인 프로세스를 거쳤는가

실패 사례와 복구 절차

사례 1: 잘못된 terraform destroy

한 엔지니어가 dev 환경에서 작업하다가 prod workspace가 선택된 상태에서 terraform destroy를 실행한 사고다. Workspace 방식의 치명적 약점을 보여주는 사례다.

복구 절차:

  1. 즉시 Ctrl+C로 destroy 중단 (진행 중인 경우)
  2. S3 버저닝으로 이전 State 버전 확인
  3. 이전 State로 롤백
  4. terraform plan으로 삭제된 리소스 식별
  5. terraform apply로 삭제된 리소스 재생성
  6. 근본 대책: Workspace 방식에서 Directory 방식으로 전환

이 사례가 Directory 방식이 권장되는 가장 큰 이유다. prod 디렉터리에서 작업하려면 명시적으로 cd environments/prod를 실행해야 하고, CI/CD 파이프라인도 환경별로 분리된다.

사례 2: State Lock 데드락

DynamoDB Lock이 해제되지 않아 모든 팀원이 apply를 실행할 수 없는 상황이다.

복구 절차:

  1. DynamoDB에서 Lock 항목의 Info 컬럼 확인 (누가, 언제, 어떤 작업)
  2. 해당 엔지니어에게 작업 상태 확인 (슬랙, 메신저 등)
  3. 작업이 이미 종료된 것이 확인되면 terraform force-unlock 실행
  4. terraform plan으로 State 정합성 검증
  5. 근본 대책: CI/CD 파이프라인에 타임아웃 설정, graceful shutdown 핸들러 추가

사례 3: State 파일에 민감 정보 노출

RDS 패스워드가 State 파일에 평문으로 저장된 것이 감사에서 발견된 사례다.

복구 절차:

  1. S3 버킷의 접근 로그 확인으로 비인가 접근 여부 파악
  2. 노출된 패스워드 즉시 변경
  3. Terraform 1.10+ Ephemeral Resources 또는 1.11+ Write-Only Attributes로 마이그레이션
  4. State 파일의 이전 버전도 민감 정보를 포함하므로, S3 Lifecycle Policy로 이전 버전 삭제 또는 만료 설정
  5. 근본 대책: OpenTofu의 State 암호화 기능 도입 검토, 또는 Terraform 1.11 Write-Only Attributes 전면 적용

참고자료