Terraform을 처음 사용할 때는 별도 Backend 설정 없이도 잘 동작한다.
terraform init
terraform plan
terraform apply
그러면 같은 디렉터리에 terraform.tfstate가 생긴다.
처음에는 단순한 결과 파일처럼 보이지만, Terraform에서 state는 실제 Resource와 Terraform Code를 연결하는 핵심 데이터다.
flowchart TD
C["Terraform Code"] --> D["Difference 계산"]
S["Current State"] --> D
R["Cloud Resource"] --> D
D --> P["Plan"]
그래서 운영 환경에서는 state를 어디에 저장하고 어떻게 보호할지도 IaC 설계의 일부가 된다.
Terraform State는 단순 Cache가 아니다
Terraform은 Resource를 만든 뒤 실제 ID와 속성을 state에 기록한다.
State는 구성 주소 aws_instance.app과 실제 리소스 ID i-0123456789abcdef의 대응 관계를 기록한다.
다음 terraform plan에서는 Configuration, State, 실제 AWS Resource를 비교해 변경 사항을 계산한다.
flowchart TD
C["Configuration"] --> D["Drift / Change 비교"]
S["State"] --> D
A["AWS Resource"] --> D
D --> P["Plan"]
state를 잃어버리거나 여러 사람이 서로 다른 state를 사용하면 Terraform이 Infrastructure를 보는 기준도 달라진다.
Local State의 문제
혼자 테스트할 때는 local state도 충분하다. 하지만 Team 환경에서는 금방 문제가 생긴다.
Developer A와 Developer B가 각자 로컬의 terraform.tfstate를 관리한다.
각자 다른 state를 가지고 있다면 실제 Infrastructure는 하나인데 Terraform이 바라보는 상태는 두 개가 된다.
더 위험한 경우는 동시에 Apply하는 상황이다.
flowchart TD
A["Developer A"] --> AA["terraform apply"]
B["Developer B"] --> BA["terraform apply"]
AA --> I["같은 Infrastructure 수정"]
BA --> I
잘못하면 state가 서로 덮어써지거나 실제 Resource와 state가 어긋날 수 있다.
그래서 Team에서 Terraform을 운영할 때는 state를 중앙 저장소로 옮기는 것이 일반적이다.
Remote Backend
Terraform에서 state 저장 위치를 담당하는 것이 Backend다.
기본값은 local backend다.
기본 로컬 구성에서는 Terraform이 terraform.tfstate를 로컬 디스크에 저장한다.
S3 backend를 사용하면:
flowchart TD
D["Developer"] --> T["Terraform"]
G["GitHub Actions / CI"] --> T
T --> S["S3 Backend"]
S --> F["terraform.tfstate"]
모든 실행 주체가 같은 state를 바라보게 된다.
왜 S3를 많이 사용할까?
AWS 환경에서는 S3가 Remote Backend로 잘 맞는다.
중앙 저장
Developer A·B와 GitHub Actions가 동일한 S3 State를 공유한다.
Local File을 주고받을 필요가 없다.
Versioning
S3 Bucket Versioning을 켜두면 state가 잘못 변경되었을 때 이전 버전을 확인하거나 복구할 수 있다.
HashiCorp도 S3 backend를 사용할 때 Bucket Versioning을 강하게 권장한다.
IAM
State 접근 권한을 IAM으로 제한할 수 있다.
접근 권한 예시:
- Developer: Read
- Terraform CI Role: Read / Write
- Other Role: Deny
Encryption
Terraform state에는 Resource 정보뿐 아니라 상황에 따라 민감한 값도 들어갈 수 있다.
그래서 S3 Server-Side Encryption과 필요하면 KMS를 같이 사용하는 것이 좋다.
2026년에는 DynamoDB Locking보다 S3 Lockfile
예전 Terraform 예제를 보면 이런 Architecture가 자주 나온다.
기존 구성은 S3에 State, DynamoDB에 State Lock을 저장했다.
하지만 현재 Terraform S3 backend는 S3 자체 Lockfile 방식을 지원한다.
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "production/app/terraform.tfstate"
region = "ap-northeast-2"
use_lockfile = true
}
}
use_lockfile = true를 설정하면 Terraform이 state와 함께 Lockfile을 사용한다.
- terraform.tfstate
- terraform.tfstate.tflock
현재 HashiCorp 문서에서는 DynamoDB 기반 locking을 deprecated로 표시하고 있다.
새 환경이라면 예전 예제를 그대로 복사해서 S3 + DynamoDB부터 만드는 것보다 S3 native locking을 먼저 검토하는 편이 맞다.
Locking은 왜 필요할까?
Lock이 없다면 두 Pipeline이 같은 state를 동시에 수정할 수 있다.
sequenceDiagram
participant A as Pipeline A
participant S as Shared State
participant B as Pipeline B
A->>S: State Read
B->>S: State Read
Note over A,B: 잠금 없이 같은 State로 Apply
A->>S: State Write
B->>S: State Write
Note over A,B: 동시 변경으로 충돌 위험
마지막 Write가 앞선 변경을 덮으면 state corruption으로 이어질 수 있다.
Locking을 사용하면:
sequenceDiagram
participant A as Pipeline A
participant S as State Lock
participant B as Pipeline B
A->>S: Acquire Lock
S-->>A: 획득
B->>S: Acquire Lock
S-->>B: 이미 잠김
Note over B: lock-timeout 범위에서 재시도
A->>A: Apply / State Update
A->>S: Release Lock
B->>S: Acquire Lock 재시도
S-->>B: 획득 가능
처럼 한 번에 하나의 Writer만 state를 수정하게 된다.
Terraform은 backend가 locking을 지원하면 state를 쓰는 Operation에서 자동으로 Lock을 잡는다.
S3 Backend 구성 예시
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "production/network/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
use_lockfile = true
}
}
환경별로 key를 분리할 수도 있다.
company-terraform-state 버킷 안에서 환경·컴포넌트별 key를 분리한다.
development/network/terraform.tfstatedevelopment/application/terraform.tfstatestaging/network/terraform.tfstateproduction/network/terraform.tfstateproduction/application/terraform.tfstate
중요한 것은 단순히 Bucket 하나에 state를 몰아넣는 것이 아니라 경로와 IAM 권한을 Environment별로 나누는 것이다.
IAM 권한도 최소화한다
버킷 목록 조회에는 버킷 ARN을, state 읽기·쓰기에는 객체 ARN을 사용한다. 필요한 Action은 다음과 같다:
- s3:ListBucket
- s3:GetObject
- s3:PutObject
use_lockfile을 사용한다면 Lockfile에는:
- s3:GetObject
- s3:PutObject
- s3:DeleteObject
권한이 추가로 필요하다.
State 자체에 DeleteObject를 무조건 줄 필요는 없다.
- State 객체: Read / Write
- Lockfile 객체: Read / Write / Delete
처럼 최소 권한을 설계할 수 있다.
버킷·State·Lockfile의 Resource를 분리한다
아래는 default workspace의 한 state 경로만 사용하는 예시다. 버킷 이름과 경로를 실제 값으로 바꾼다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::company-terraform-state",
"Condition": {
"StringLike": {
"s3:prefix": ["production/network/*"]
}
}
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::company-terraform-state/production/network/terraform.tfstate"
},
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
"Resource": "arn:aws:s3:::company-terraform-state/production/network/terraform.tfstate.tflock"
}
]
}
SSE-KMS를 사용한다면 해당 KMS 키에 필요한 권한과 키 정책도 별도로 확인한다. 추가 workspace를 사용하면 실제 저장 경로와 workspace 관리에 필요한 권한이 달라지므로 이 정책을 그대로 복사하지 않는다.
잠금 충돌과 복구
앞의 잠금 대기 흐름은 개념도다. 다른 실행이 잠금을 잡았을 때 일정 시간 재시도하려면 대기 시간을 명시한다.
terraform plan -lock-timeout=5m
terraform apply -lock-timeout=5m
잠금 오류가 나면 먼저 같은 state를 사용하는 CI와 로컬 실행이 살아 있는지 확인한다. 실행이 끝나기를 기다리고, 종료된 실행의 잠금이 남았다는 근거가 있을 때만 해당 lock ID의 수동 해제를 검토한다. -lock=false를 일상적인 해결책으로 사용하지 않는다.
S3 이전 버전으로 state를 되돌리는 것은 실제 인프라를 되돌리는 작업이 아니다. 복구 전 현재 state를 보관하고 실제 리소스와 대조한 뒤 plan을 검토한다. 로컬과 CI의 Terraform 버전을 맞추고, use_lockfile 지원 여부를 확인한 버전을 프로젝트에서 고정한다. 이 글은 실제 AWS 계정에서 실행한 검증 기록을 포함하지 않는다.
State를 Git에 올리면 안 되는 이유
terraform.tfstate를 Git에 Commit하는 것은 권장하지 않는다.
State에는 상황에 따라 Resource ID, Endpoint, IP, ARN, Configuration, Sensitive Value 등이 들어갈 수 있다.
sensitive = true는 CLI 출력 등을 숨기는 데 도움을 주지만 state에 값이 저장되는 경우까지 없애주는 기능은 아니다.
그래서 terraform.tfstate, terraform.tfstate.backup, .terraform/ 등은 Git에서 제외하고 state는 별도의 Remote Backend로 관리하는 편이 좋다.
Local State를 S3로 옮기기
이미 Local State를 사용 중이라면 Backend 설정을 추가한 뒤 Migration할 수 있다.
terraform init -migrate-state
운영 환경에서는:
flowchart TD
N0["현재 State Backup"]
N1["S3 Bucket / IAM 확인"]
N2["Backend 설정"]
N3["terraform init -migrate-state"]
N4["terraform plan"]
N5["변경 없음 확인"]
N0 --> N1
N1 --> N2
N2 --> N3
N3 --> N4
N4 --> N5
순서로 진행하는 편이 안전하다.
내가 권장하는 기본 구조
flowchart TD
G["GitHub / Developer"] --> T["Terraform"]
T --> S["S3 Remote Backend"]
S --> V["Versioning"]
S --> E["Encryption"]
S --> L["Lockfile: use_lockfile"]
Environment가 커지면 Dev / Stage / Prod state와 IAM Role도 분리한다.
정리
Terraform State를 S3에 저장하는 이유는 단순히 파일을 Cloud에 백업하기 위해서가 아니다.
- Local State: 팀 공유·동시 실행·복구를 직접 해결해야 한다.
- Remote Backend: 중앙 관리에 Versioning·IAM·Encryption·Locking을 조합한다.
2026년 기준으로 새 S3 backend를 설계한다면 S3 + Bucket Versioning + Encryption + Least Privilege IAM + use_lockfile = true를 기본으로 보고 시작하는 것이 좋다.
예전에 많이 사용하던 DynamoDB 기반 locking은 현재 deprecated 상태이므로, 기존 시스템을 유지하는 경우와 새 시스템을 만드는 경우를 구분해서 볼 필요가 있다.