Terraform State란? 인프라 상태 관리의 핵심
Terraform을 처음 사용하면 terraform.tfstate 파일이 자동 생성됩니다. 이 파일은 단순한 로그가 아닙니다 — Terraform이 관리하는 모든 인프라 리소스의 현재 상태를 기록한 데이터베이스입니다. State 파일 없이는 Terraform이 어떤 리소스가 이미 존재하는지, 무엇을 변경해야 하는지, 무엇을 삭제해야 하는지 알 수 없습니다.
팀 환경에서 State를 로컬 파일로 관리하면 동시 수정 충돌, State 유실, 민감 정보 노출 같은 치명적 문제가 발생합니다. 이 글에서는 State의 내부 구조부터 Remote Backend(S3, GCS) 설정, State Locking 메커니즘, terraform state 명령어 완전 정복, Workspace를 활용한 환경 분리, 그리고 State 분할(splitting) 전략까지 운영 수준에서 완전히 다룹니다.
State 파일의 내부 구조: 무엇이 저장되는가
{
"version": 4,
"terraform_version": "1.7.0",
"serial": 42,
"lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"outputs": {
"vpc_id": {
"value": "vpc-0abc123def456",
"type": "string"
}
},
"resources": [
{
"mode": "managed",
"type": "aws_instance",
"name": "web",
"provider": "provider["registry.terraform.io/hashicorp/aws"]",
"instances": [
{
"schema_version": 1,
"attributes": {
"id": "i-0abc123def456",
"ami": "ami-0abcdef1234567890",
"instance_type": "t3.micro",
"private_ip": "10.0.1.50",
"tags": { "Name": "web-server" }
}
}
]
}
]
}
핵심 필드 설명
| 필드 | 역할 | 중요도 |
|---|---|---|
serial |
State 버전 번호. 매 변경마다 증가 | 동시성 제어에 사용 |
lineage |
State의 고유 UUID. 다른 State와 구분 | 실수로 다른 State 덮어쓰기 방지 |
outputs |
output 값 저장. 다른 모듈에서 참조 가능 | 모듈 간 데이터 공유 |
resources |
관리 중인 모든 리소스의 속성 저장 | plan/apply의 diff 기준 |
⚠️ 보안 경고: State 파일에는 DB 비밀번호, API 키, 인증서 등 민감 정보가 평문으로 저장됩니다. aws_db_instance의 password, tls_private_key의 private_key_pem 등이 그대로 노출됩니다. 절대 Git에 커밋하면 안 됩니다.
Remote Backend: 팀 협업의 필수 조건
로컬 State의 문제점과 Remote Backend가 해결하는 것:
| 문제 | 로컬 State | Remote Backend |
|---|---|---|
| 팀원 간 공유 | ❌ 수동 복사 | ✅ 자동 공유 |
| 동시 수정 방지 | ❌ 없음 | ✅ State Locking |
| 민감 정보 보호 | ❌ 평문 파일 | ✅ 서버측 암호화 |
| 버전 이력 | ❌ 없음 | ✅ 버저닝 지원 |
| State 유실 | ❌ 디스크 고장 시 유실 | ✅ 고가용성 스토리지 |
AWS S3 + DynamoDB Backend 설정
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "production/vpc/terraform.tfstate"
region = "ap-northeast-2"
# State Locking (DynamoDB)
dynamodb_table = "terraform-locks"
# 서버측 암호화
encrypt = true
# KMS 키로 암호화 (선택)
kms_key_id = "arn:aws:kms:ap-northeast-2:123456789:key/abc-123"
}
}
# S3 버킷 설정 (별도 프로젝트로 관리)
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" # State 이력 보관 → 롤백 가능
}
}
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"
}
}
}
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
}
# DynamoDB Lock 테이블
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
GCS Backend (Google Cloud)
terraform {
backend "gcs" {
bucket = "my-terraform-state"
prefix = "production/vpc"
# GCS는 자체 Lock 메커니즘 내장 (별도 설정 불필요)
}
}
State Locking: 동시 수정 충돌 방지
두 명이 동시에 terraform apply를 실행하면 State가 손상될 수 있습니다. Locking이 이를 방지합니다:
# 정상 흐름
$ terraform plan
Acquiring state lock. This may take a few moments...
# Lock 획득 → plan 실행 → Lock 해제
# 다른 사람이 Lock을 잡고 있는 경우
$ terraform plan
Acquiring state lock. This may take a few moments...
Error: Error acquiring the state lock
Lock Info:
ID: a1b2c3d4-e5f6-7890
Path: my-terraform-state/production/vpc/terraform.tfstate
Operation: OperationTypeApply
Who: jane@laptop
Version: 1.7.0
Created: 2026-02-22 17:00:00.000000 +0000 UTC
# 비정상 종료로 Lock이 남아있는 경우 (강제 해제)
$ terraform force-unlock a1b2c3d4-e5f6-7890
Do you really want to force-unlock?
Terraform will remove the lock on the remote state.
This will allow local Terraform commands to modify this state,
even though it may still be in use. Only 'yes' will be accepted.
Enter a value: yes
Successfully force-unlocked!
⚠️ force-unlock 주의: 다른 사람이 실제로 apply 중인데 강제 해제하면 State가 손상됩니다. 반드시 Lock을 잡은 사람에게 확인한 후 실행하세요.
terraform state 명령어 완전 정복
state list: 관리 중인 리소스 목록
$ terraform state list
aws_vpc.main
aws_subnet.public[0]
aws_subnet.public[1]
aws_subnet.private[0]
aws_instance.web
module.rds.aws_db_instance.main
module.rds.aws_db_subnet_group.main
# 필터링
$ terraform state list module.rds
module.rds.aws_db_instance.main
module.rds.aws_db_subnet_group.main
state show: 리소스 상세 속성 확인
$ terraform state show aws_instance.web
# aws_instance.web:
resource "aws_instance" "web" {
ami = "ami-0abcdef1234567890"
arn = "arn:aws:ec2:ap-northeast-2:123456789:instance/i-0abc123"
id = "i-0abc123def456"
instance_state = "running"
instance_type = "t3.micro"
private_ip = "10.0.1.50"
public_ip = "54.180.1.100"
tags = { "Name" = "web-server" }
}
state mv: 리소스 리네임/리팩토링
# 리소스 이름 변경 (코드에서 이름을 바꾼 후)
# 변경 전: resource "aws_instance" "web" { ... }
# 변경 후: resource "aws_instance" "api_server" { ... }
$ terraform state mv aws_instance.web aws_instance.api_server
Move "aws_instance.web" to "aws_instance.api_server"
Successfully moved 1 object(s).
# 모듈로 이동
$ terraform state mv aws_instance.api_server module.compute.aws_instance.main
# for_each로 전환 시
# 변경 전: resource "aws_subnet" "public" { count = 2 }
# 변경 후: resource "aws_subnet" "public" { for_each = ... }
$ terraform state mv 'aws_subnet.public[0]' 'aws_subnet.public["ap-northeast-2a"]'
$ terraform state mv 'aws_subnet.public[1]' 'aws_subnet.public["ap-northeast-2c"]'
핵심: state mv 없이 코드에서만 이름을 바꾸면 Terraform은 기존 리소스를 삭제하고 새로 만들려고 합니다. state mv로 State의 주소만 변경하면 실제 인프라 변경 없이 리팩토링할 수 있습니다.
state rm: State에서 리소스 제거 (실제 인프라 유지)
# Terraform 관리에서 제외 (실제 리소스는 삭제 안 됨)
$ terraform state rm aws_instance.legacy
Removed aws_instance.legacy
Successfully removed 1 resource instance(s).
# 이후 terraform destroy해도 이 리소스는 영향받지 않음
import: 기존 인프라를 State에 추가
# 코드 먼저 작성
resource "aws_instance" "imported" {
ami = "ami-0abcdef1234567890"
instance_type = "t3.micro"
}
# 기존 인프라를 State에 연결
$ terraform import aws_instance.imported i-0abc123def456
aws_instance.imported: Importing from ID "i-0abc123def456"...
aws_instance.imported: Import prepared!
aws_instance.imported: Refreshing state...
Import successful!
# Terraform 1.5+ : import 블록 (코드 내 선언적 import)
import {
to = aws_instance.imported
id = "i-0abc123def456"
}
# plan으로 확인 후 apply
$ terraform plan
# aws_instance.imported will be imported
# Plan: 1 to import, 0 to add, 0 to change, 0 to destroy.
state pull / push: State 직접 조작 (비상용)
# State를 JSON으로 다운로드
$ terraform state pull > backup.tfstate
# State를 직접 업로드 (극도로 주의!)
$ terraform state push backup.tfstate
# 일반적으로 state pull/push는 비상 복구 시에만 사용
Workspace: 단일 코드로 멀티 환경 관리
Terraform Workspace는 같은 코드를 여러 환경(dev, staging, prod)에 적용할 때 State를 분리하는 기능입니다:
# Workspace 생성
$ terraform workspace new dev
Created and switched to workspace "dev"!
$ terraform workspace new staging
$ terraform workspace new prod
# Workspace 목록
$ terraform workspace list
default
dev
staging
* prod
# Workspace 전환
$ terraform workspace select dev
Switched to workspace "dev".
# 코드에서 현재 Workspace 참조
locals {
env = terraform.workspace # "dev", "staging", "prod"
instance_type = {
dev = "t3.micro"
staging = "t3.small"
prod = "t3.large"
}
}
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = local.instance_type[local.env]
tags = {
Name = "web-${local.env}"
Environment = local.env
}
}
S3 Backend에서 Workspace별 State 경로
# key = "vpc/terraform.tfstate" 설정 시
# dev → env:/dev/vpc/terraform.tfstate
# staging → env:/staging/vpc/terraform.tfstate
# prod → env:/prod/vpc/terraform.tfstate
Workspace vs 디렉토리 분리: 선택 기준
| 기준 | Workspace | 디렉토리 분리 |
|---|---|---|
| 코드 차이 | 거의 없음 (변수만 다름) | 환경별 구조가 다름 |
| 실수 위험 | ⚠️ workspace 전환 안 하고 apply 위험 | ✅ 디렉토리가 다르므로 안전 |
| 코드 중복 | ✅ 없음 | ⚠️ 모듈로 최소화 필요 |
| 권장 상황 | 소규모 팀, 환경 차이 적음 | 대규모 팀, 프로덕션 격리 필수 |
실무 권장: 프로덕션 환경은 별도 디렉토리(또는 별도 저장소)로 분리하는 것이 안전합니다. Workspace 전환을 깜빡하고 terraform apply하면 프로덕션이 날아갈 수 있습니다.
State 분할(Splitting): 대규모 인프라 관리 전략
모든 인프라를 하나의 State에 관리하면 plan이 느려지고, 변경 영향 범위가 커지며, 팀 간 충돌이 증가합니다. 도메인별로 State를 분리하는 것이 핵심입니다:
infrastructure/
├── network/ # VPC, 서브넷, NAT Gateway
│ ├── main.tf
│ ├── outputs.tf # vpc_id, subnet_ids 출력
│ └── backend.tf # key = "network/terraform.tfstate"
├── database/ # RDS, ElastiCache
│ ├── main.tf
│ ├── data.tf # network State 참조
│ └── backend.tf # key = "database/terraform.tfstate"
├── compute/ # ECS, EC2, ASG
│ ├── main.tf
│ └── backend.tf # key = "compute/terraform.tfstate"
└── monitoring/ # CloudWatch, SNS
├── main.tf
└── backend.tf # key = "monitoring/terraform.tfstate"
terraform_remote_state로 State 간 데이터 공유
# network/outputs.tf — VPC 정보를 output으로 노출
output "vpc_id" {
value = aws_vpc.main.id
}
output "private_subnet_ids" {
value = aws_subnet.private[*].id
}
output "database_security_group_id" {
value = aws_security_group.database.id
}
# database/data.tf — network State 참조
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "my-terraform-state"
key = "network/terraform.tfstate"
region = "ap-northeast-2"
}
}
# database/main.tf — 참조한 값 사용
resource "aws_db_instance" "main" {
identifier = "production-db"
engine = "postgres"
instance_class = "db.r6g.large"
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [
data.terraform_remote_state.network.outputs.database_security_group_id
]
}
resource "aws_db_subnet_group" "main" {
name = "production-db"
subnet_ids = data.terraform_remote_state.network.outputs.private_subnet_ids
}
이 패턴은 Terraform Modules와 결합하여, 모듈은 재사용 가능한 코드를, terraform_remote_state는 프로젝트 간 데이터 연결을 담당하는 구조를 만듭니다.
State 마이그레이션: Backend 변경
# 1. 로컬 → S3로 마이그레이션
# backend.tf를 수정한 후:
$ terraform init -migrate-state
Initializing the backend...
Terraform has detected you're unconfiguring your previously set "local" backend.
Do you want to copy existing state to the new backend?
Enter a value: yes
Successfully configured the backend "s3"!
# 2. S3 키 변경 (경로 리팩토링)
# backend.tf에서 key를 변경한 후:
$ terraform init -migrate-state
# 기존 State가 새 키로 복사됨
# 3. 다른 Backend 종류로 이전 (S3 → GCS)
# backend.tf를 gcs로 변경한 후:
$ terraform init -migrate-state
State 보안: 민감 정보 보호 전략
| 대책 | 방법 | 효과 |
|---|---|---|
| 서버측 암호화 | S3 SSE-KMS / GCS CMEK | 저장 시 암호화 |
| 접근 제어 | IAM 정책으로 State 버킷 접근 제한 | 권한 없는 사용자 차단 |
| 퍼블릭 차단 | S3 Block Public Access | 외부 노출 방지 |
| 버저닝 | S3 Versioning / GCS Object Versioning | 실수 시 복구 가능 |
| .gitignore | *.tfstate, *.tfstate.backup | Git 커밋 방지 |
| sensitive output | sensitive = true |
CLI 출력 마스킹 (State에는 여전히 저장) |
# .gitignore (필수)
*.tfstate
*.tfstate.*
*.tfstate.backup
.terraform/
# IAM 정책: State 버킷 접근 제한
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::my-terraform-state/*",
"Condition": {
"StringEquals": {
"aws:PrincipalTag/Team": "platform"
}
}
}
]
}
State 복구: 비상 시나리오별 대응
시나리오 1: State 파일 삭제/손상
# S3 버저닝이 활성화되어 있다면:
# AWS Console → S3 → 해당 버킷 → 이전 버전 복원
# 로컬 백업이 있다면:
$ terraform state push backup.tfstate
시나리오 2: State와 실제 인프라 불일치
# State를 실제 인프라와 동기화 (refresh)
$ terraform apply -refresh-only
# State만 업데이트하고 인프라는 변경하지 않음
# 특정 리소스만 refresh
$ terraform apply -refresh-only -target=aws_instance.web
시나리오 3: 리소스가 콘솔에서 수동 삭제됨
# State에서만 제거 (재생성 방지)
$ terraform state rm aws_instance.manually_deleted
# 또는 코드에서 해당 리소스 블록 삭제 후
$ terraform plan
# 1 to destroy → State에서 제거될 뿐 이미 없으므로 에러
moved 블록: Terraform 1.1+ 코드 내 리팩토링
# state mv 대신 코드에서 선언적으로 리팩토링
moved {
from = aws_instance.web
to = aws_instance.api_server
}
moved {
from = aws_instance.app
to = module.compute.aws_instance.app
}
# terraform plan 실행 시:
# aws_instance.web has moved to aws_instance.api_server
# Plan: 0 to add, 0 to change, 0 to destroy.
# apply 후 moved 블록은 제거해도 됨
moved 블록은 state mv보다 안전합니다 — 코드 리뷰에서 리팩토링을 확인할 수 있고, plan으로 미리 검증할 수 있으며, 팀원 모두에게 자동 적용됩니다. Terraform Modules 리팩토링 시 특히 유용합니다.
정리: State 관리 체크리스트
| 항목 | 체크 |
|---|---|
| Remote Backend 사용 (S3/GCS/Terraform Cloud) | ☐ |
| State Locking 활성화 (DynamoDB/GCS 내장) | ☐ |
| 서버측 암호화 (SSE-KMS) 설정 | ☐ |
| S3 버저닝 활성화 (State 이력 보관) | ☐ |
| 퍼블릭 접근 차단 | ☐ |
| .gitignore에 *.tfstate 추가 | ☐ |
| 도메인별 State 분할 (network/database/compute) | ☐ |
| 리팩토링 시 state mv 또는 moved 블록 사용 | ☐ |
| import는 코드 작성 → import → plan 검증 순서 | ☐ |
| force-unlock 전 Lock 소유자 확인 | ☐ |
Terraform State는 인프라의 단일 진실 공급원(Single Source of Truth)입니다. State가 손상되면 Terraform은 인프라를 관리할 수 없게 됩니다. Remote Backend로 안전하게 보관하고, Locking으로 동시 수정을 방지하며, 도메인별로 분할하여 변경 영향을 최소화하는 것이 대규모 인프라 운영의 핵심입니다.