학습 목표
Terraform state가 멱등·plan·팀 협업에 왜 필요한지 설명할 수 있다.
local state와 remote state(S3 등) ·state locking 차이를 설명할 수 있다.
module로 인프라를 재사용하고 root module에서 variable·output으로 연결할 수 있다.
terraform init 시 module source·backend 마이그레이션 흐름을 설명할 수 있다.
state 민감 정보·충돌 위험과 실무 운영 습관을 짚을 수 있다.
문제 상황
- 동료와 같은 VPC를 각자
terraform apply— 리소스 이중 생성·state 덮어쓰기 terraform.tfstate를 Git에 커밋 — RDS password가 평문 노출- laptop state만 있어 CI apply가 매번 전부 create 시도
- staging·prod 코드 복붙 — SG rule 하나 바꿀 때 두 repo 수정
- module
source를 로컬 경로로만 쓰다 팀원 PC에서 경로 not found terraform apply중 다른 사람 apply — state corruption (lock 없음)- 3편에서 로컬 state로 S3 하나 올렸는데 — 팀·환경으로 확장이 막힘
앞 편에서 HCL·init/plan/apply와 state 개념을 봤다. 이번 편은 remote state·locking과 module 조합으로 협업·재사용을 가능하게 한다. workspace·환경 디렉터리는 5편이다.
1. State란?
Terraform state는 실제 클라우드 리소스와 HCL의 매핑 테이블이다.

state에 담기는 것
- resource address → cloud ID (
aws_s3_bucket.artifacts→arn:...) - 속성 스냅샷 — plan 시 API refresh와 비교
- metadata — serial, lineage, (backend 설정 시) lock info
{
"resources": [
{
"type": "aws_s3_bucket",
"name": "artifacts",
"instances": [
{
"attributes": {
"id": "myapp-artifacts-staging",
"arn": "arn:aws:s3:::myapp-artifacts-staging"
}
}
]
}
]
}
- desired는 Git, ID 매핑은 state — 둘이 합쳐져 plan이 동작
- state 없으면 Terraform은 "이미 존재"를 모름 → 중복 create 시도
local vs remote
| Local state | Remote state | |
|---|---|---|
| 파일 | terraform.tfstate (워킹 디렉터리) |
S3·GCS·Terraform Cloud 등 |
| 협업 | 불가 (복사·충돌) | 공유 backend |
| CI | state 없음 | 동일 backend 참조 |
| locking | 없음 | DynamoDB·native lock |
| 보안 | .gitignore 필수 |
암호화·IAM·버전 |
- 솔로·학습 — local OK
- 팀·CI·prod — remote가 기본
2. Remote backend와 locking
AWS에서 흔한 패턴: S3 + DynamoDB.

# code/backend-s3.example.tf
terraform {
backend "s3" {
bucket = "my-org-tf-state"
key = "staging/app/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-locks"
encrypt = true
}
}
| 구성 | 역할 |
|---|---|
| S3 bucket | state 파일 저장, versioning 권장 |
| key | 스택별 경로 (env/service/terraform.tfstate) |
| encrypt | at-rest 암호화 (SSE-S3/KMS) |
| DynamoDB | lock 테이블 — 동시 apply 방지 |
lock 동작
User A: apply 시작 → lock 획득
User B: apply 시도 → Error: lock already held
User A: apply 완료 → lock 해제
User B: 재시도 → 성공
- plan도 lock 옵션 가능 (
-lock=false는 비상만) - stale lock — 중단된 apply 후 DynamoDB에 lock 잔류 →
force-unlock(신중)
backend 마이그레이션
로컬 → remote 전환:
# backend 블록 추가 후
terraform init -migrate-state
# 기존 local state를 S3로 복사할지 질문 → yes
- key 설계 — 환경·서비스·계층별 분리 (5편 workspace와 조합)
- state bucket은 별도 스택·계정 — 앱 리소스와 lifecycle 분리
state 보안
- 민감 값 —
password,private_key가 state에 평문 저장될 수 있음 - 조치
- state bucket IAM 최소 권한
- KMS·버전·접근 로그
- Git 커밋 금지 —
.gitignore에*.tfstate* - 가능하면 Sensitive·외부 secret store (Security·22편)
3. Module
module — .tf 묶음을 이름 붙여 재사용.

구조 예
4-terraform-state-module/code/stack/
├── main.tf # module 호출
├── variables.tf
├── outputs.tf
├── backend.tf
└── modules/
└── network/
├── main.tf
├── variables.tf
└── outputs.tf
child module (network)
# modules/network/variables.tf
variable "vpc_cidr" {
type = string
}
variable "environment" {
type = string
}
# modules/network/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
tags = {
Name = "vpc-${var.environment}"
}
}
# modules/network/outputs.tf
output "vpc_id" {
value = aws_vpc.main.id
}
root module에서 호출
# code/stack/main.tf
module "network" {
source = "./modules/network"
vpc_cidr = var.vpc_cidr
environment = var.environment
}
resource "aws_security_group" "app" {
name = "app-sg"
vpc_id = module.network.vpc_id
}
source— 로컬./modules/..., Gitgit::https://..., registryterraform-aws-modules/vpc/aws- module output —
module.network.vpc_id로 참조 - child 안 resource는 state에
module.network.aws_vpc.main주소로 기록
variable · output 흐름
| 방향 | 용도 |
|---|---|
| root → module | variable로 입력 |
| module → root | output으로 값 반환 |
| module → module | root가 중개 (형제 직접 참조 지양) |
# root outputs.tf — CI·다른 stack에 전달
output "vpc_id" {
value = module.network.vpc_id
}
- 버전 pin — registry module은
version = "~> 5.0" - 작은 module — VPC, SG, RDS 등 한 책임 — root는 조합만
4. init · plan · apply (module·backend)
| 단계 | module·state 관점 |
|---|---|
| init | module 다운로드·.terraform/modules, backend 설정 |
| plan | remote state 읽기, module 리소스 전개 후 diff |
| apply | lock → API → state 갱신 (S3) |
cd code/stack
terraform init # modules + backend
terraform plan
terraform apply
- module 코드 변경 →
terraform init -upgrade(provider·module 버전) - root module = apply 단위 — state 하나 per backend key
5. 실무 패턴
| 패턴 | 설명 |
|---|---|
| mono repo + multi key | repo 하나, backend key로 env·stack 분리 |
| module registry | 공통 VPC·EKS를 버전 태그 module로 |
| composition root | modules/network + modules/data + modules/app |
| plan in CI | PR에 plan comment, apply는 main·승인 후 |
| state 분리 | network state ≠ app state — blast radius·권한 분리 |
- anti-pattern — 거대 single module, state 한 파일에 전 계정
- drift·import — 20편; policy on plan — 21편
정리
- State — address → ID; plan·멱등·협업의 기준
- Remote + lock — S3·DynamoDB로 공유·동시성 제어
- Module — variable in · output out; root가 조합
- init — backend·module 준비; key·source 설계가 운영의 출발점
- 환경 분리 — workspace·directory·tfvars는 5편
다음에 다룰 것
- 환경과 Workspace
- dev/staging/prod 분리, workspace vs directory, tfvars
해당 내용은 HashiCorp Terraform Documentation (State, Modules), Brikman — Terraform: Up & Running, 3/E Ch 3~4 를 기반으로 합니다.
'Infrastructure as Code' 카테고리의 다른 글
| AWS CDK 개요 (0) | 2026.07.07 |
|---|---|
| 환경과 Workspace (0) | 2026.07.05 |
| Terraform 기초 (0) | 2026.07.01 |
| Infrastructure as Code란 (0) | 2026.06.30 |
| IaC 오리엔테이션 (0) | 2026.06.29 |