Terraform State가 중요한 이유
Terraform State가 중요한 이유
Terraform state는 단순 실행 cache가 아니라 aws_instance.api 같은 resource address와 cloud의 실제 object ID를 연결하는 mapping database다. State를 잃으면 실제 resource가 사라지는 것이 아니라 Terraform이 소유 관계를 잃어 중복 생성이나 삭제 계획을 만들 수 있다. 팀에서는 locking·암호화·versioning·감사가 가능한 remote backend를 사용하고 production state의 읽기와 쓰기를 최소 권한으로 제한한다. State와 plan에는 sensitive 표시 여부와 상관없이 값이 들어갈 수 있다. Refactoring은 moved block이나 terraform state mv로 binding을 이동하고, force-unlock·state push·직접 JSON 편집은 사고 대응 절차와 백업 없이 사용하지 않는다.
목차
- #State는 실제 Resource의 소유권 Mapping이다
- #Plan은 Configuration State Remote를 비교한다
- #State를 잃거나 잘못 바꾸면 생기는 일
- #Local State가 팀 작업에 맞지 않는 이유
- #Remote Backend가 제공해야 하는 것
- #S3 Backend를 예로 구성하기
- #State Lock으로 동시 Writer 막기
- #force-unlock을 함부로 쓰면 안 되는 이유
- #State에는 민감정보가 들어갈 수 있다
- #Backend 접근 권한을 읽기와 쓰기로 분리하기
- #State Versioning과 복구 절차 만들기
- #Workspace와 환경 분리를 혼동하지 않기
- #State를 너무 크게 만들지 않기
- #다른 Stack에 값을 전달하는 방법
- #Resource 이름을 바꿀 때 moved Block 사용하기
- #기존 Resource를 Import하기
- #state rm과 state push의 위험성
- #Drift를 발견하고 처리하기
- #CI에서 Plan과 Apply를 같은 State에 연결하기
- #실패 시나리오를 포함한 검증
- #구현 체크리스트
- #마무리
- #관련 노트
- #참고 자료
State는 실제 Resource의 소유권 Mapping이다
Terraform configuration에는 cloud object ID가 아니라 논리적인 resource address가 있다.
resource "aws_instance" "api" {
ami = var.api_ami_id
instance_type = "t3.small"
tags = {
Name = "sample-api"
}
}
실제 cloud에는 i-0abc... 같은 provider ID가 있다. State가 둘을 연결한다.
aws_instance.api
↕
i-0123456789example
ID는 가상 값이다. Terraform은 이 binding을 사용해 다음 plan에서 aws_instance.api를 새로 만들지, 기존 object를 update할지, 제거할지 판단한다.
flowchart LR
A[HCL resource address]
B[Terraform state binding]
C[Cloud object identity]
A --- B --- CState에는 ID만 있는 것이 아니다. Resource attribute, dependency metadata, provider schema에 필요한 private data, output 등이 들어갈 수 있다. 그래서 “실제 infrastructure가 source of truth인데 state는 다시 만들면 된다”는 말은 절반만 맞다. 실제 object는 남지만 configuration과의 소유 관계를 다시 복원해야 한다.
Infrastructure 자체의 복사본이 아니라 어떤 configuration instance가 어떤 remote object를 관리하는지 기록한 mapping이다.
Plan은 Configuration State Remote를 비교한다
Terraform의 판단에는 세 가지 상태가 관여한다.
flowchart TD
A[Desired configuration]
B[Previous state]
C[Refreshed remote objects]
D[Execution plan]
A --> D
B --> D
C --> D- Configuration: 원하는 상태
- State: 이전 실행이 알고 있던 binding과 attribute
- Remote API: 지금 실제로 존재하는 상태
일반적으로 plan 전에 provider가 remote object를 refresh해 drift를 반영한다. 하지만 API 권한, refresh option, provider behavior에 따라 보이는 범위가 달라질 수 있다.
다음 변경은 configuration block 이름만 바꾼 것처럼 보인다.
# 변경 전
resource "aws_security_group" "api" {
name = "sample-api"
}
# 변경 후
resource "aws_security_group" "application" {
name = "sample-api"
}
State binding을 함께 옮기지 않으면 plan은 이전 address 삭제와 새 address 생성을 제안할 수 있다. Resource name refactoring도 infrastructure operation이 될 수 있는 이유다.
State를 잃거나 잘못 바꾸면 생기는 일
State file을 삭제해도 cloud instance는 삭제되지 않는다. Terraform이 모르게 된다.
Configuration: aws_instance.api 존재
State: binding 없음
Cloud: 기존 instance 존재
Plan: 새 instance 생성 가능
반대로 state에 binding이 있는데 실제 object가 수동 삭제됐다면 refresh 뒤 재생성 plan이 나올 수 있다.
잘못된 state 조작의 영향:
- 같은 실제 object를 두 address가 관리
- 기존 object를 새 resource로 오인해 중복 생성
- Refactor 중 destroy/create 발생
- 다른 environment의 state를 overwrite
- Secret과 network topology 노출
- 두 apply가 동시에 쓰며 마지막 write가 결과를 덮음
terraform plan 결과에 create 1, destroy 1이 나왔다고 기계적으로 승인하지 않는다. Address 변경, provider alias, workspace, backend key가 의도한 것인지 확인한다.
Local State가 팀 작업에 맞지 않는 이유
기본 local backend는 작업 directory에 terraform.tfstate와 backup을 둔다. 개인 실습에는 단순하지만 팀에서는 각자 다른 사본을 가질 수 있다.
개발자 A state serial 18
개발자 B state serial 17
CI state 없음
Git repository에 state를 commit해 공유하는 방식은 해결책이 아니다.
- Git은 apply 중 state lock을 제공하지 않는다.
- Merge conflict로 state JSON을 합칠 수 없다.
- 과거 commit 전체에 secret이 남는다.
- Apply와 commit 사이에 최신 state가 공유되지 않는다.
- CI와 local writer가 서로의 실행을 모른다.
.terraform/
*.tfstate
*.tfstate.*
*.tfplan
crash.log
Ignore는 local file의 실수 commit을 줄일 뿐 remote backend와 권한을 대신하지 않는다.
Remote Backend가 제공해야 하는 것
팀 state storage는 단순 object upload보다 다음 기능이 필요하다.
| 기능 | 필요한 이유 |
|---|---|
| 공유 storage | 모든 실행이 같은 최신 state 사용 |
| Locking | 동시에 두 apply가 쓰는 것 방지 |
| Encryption | State의 민감정보 보호 |
| Access control | 환경과 역할별 읽기·쓰기 제한 |
| Versioning | 잘못된 overwrite와 삭제 복구 |
| Audit log | 누가 언제 읽고 썼는지 조사 |
| Durability | Backend 장애와 data 손실 대응 |
Backend가 remote라고 자동으로 모두 충족되는 것은 아니다. Backend 종류와 option을 확인한다.
terraform {
backend "s3" {
bucket = "sample-terraform-state"
key = "commerce/production/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
use_lockfile = true
}
}
Bucket, key, region은 가상 예시다. Backend block의 값이 팀 표준과 맞는지 CI에서 검사할 수 있다.
S3 Backend를 예로 구성하기
State bucket은 관리 대상 workload와 별도의 bootstrap 단계에서 만든다. 같은 state가 있어야 만들 수 있는 bucket에 그 state를 저장하려 하면 순환한다.
flowchart LR
A[Bootstrap account]
B[State bucket]
C[Lock file]
D[Application Terraform]
A --> B
B --> C
B --> DS3 backend 설정에는 credential을 hardcode하지 않는다.
# 피해야 할 예
terraform {
backend "s3" {
access_key = "example"
secret_key = "example"
}
}
HashiCorp 문서가 설명하듯 backend configuration의 sensitive value는 .terraform directory와 plan에 저장될 수 있다. CI의 OIDC 또는 표준 credential chain으로 짧은 자격증명을 제공한다.
S3에서는 bucket versioning, public access block, encryption, TLS-only bucket policy, access logging 또는 cloud audit를 함께 설정한다. State와 lock object path 권한도 구분한다.
현재 HashiCorp S3 backend 문서는 use_lockfile = true로 S3 locking을 지원하며 DynamoDB 기반 locking은 deprecated로 안내한다. 오래된 예제를 그대로 복사하지 말고 팀의 Terraform version과 migration plan을 확인한다.
State Lock으로 동시 Writer 막기
두 pipeline이 같은 state에 동시에 apply한다고 하자.
sequenceDiagram
participant A as Apply A
participant L as State lock
participant B as Apply B
A->>L: acquire
L-->>A: granted
B->>L: acquire
L--xB: wait or fail
A->>L: write state and release
L-->>B: retryLock이 없으면 둘 다 같은 old state에서 plan하고 각자 remote object를 바꾼 뒤 마지막 state write가 다른 실행 결과를 잃게 할 수 있다.
Lock timeout은 일시적인 정상 실행을 기다리게 한다.
terraform apply \
-lock-timeout=5m \
reviewed.tfplan
시간은 가상 정책이다. -lock=false로 우회하는 것은 속도 해결이 아니라 동시 writer 보호 제거다. Read-only처럼 보이는 일부 명령도 state refresh나 write behavior를 확인한다.
CI pipeline의 concurrency group은 backend lock 위에 사용자 경험을 개선할 수 있지만 lock을 대체하지 않는다.
concurrency:
group: terraform-production-commerce
cancel-in-progress: false
force-unlock을 함부로 쓰면 안 되는 이유
실행이 비정상 종료되어 lock이 남을 수 있다. terraform force-unlock은 lock ID를 대상으로 제거한다.
terraform force-unlock LOCK_ID_FROM_BACKEND
다음 확인 없이 실행하면 실제 apply 중인 writer와 동시에 새 writer가 들어간다.
- Lock metadata의 operation, owner, created time 확인
- CI run과 local process가 실제 종료됐는지 확인
- Cloud API에서 진행 중 operation 확인
- 팀 channel에 unlock 의도 공유
- State snapshot과 version 확보
- 정확한 lock ID로 실행
대형 resource update가 오래 실행 중일 수 있다. Process와 pipeline의 실제 terminal 상태를 확인한다.
Unlock 뒤 바로 apply하지 말고 refresh-only plan으로 remote/state 관계를 확인한다.
terraform plan -refresh-only
실행 결과를 검토한 뒤 state를 정합화한다.
State에는 민감정보가 들어갈 수 있다
sensitive = true는 CLI 출력에서 값을 가리는 metadata이지 state에서 값을 제거하는 뜻이 아니다.
variable "database_password" {
type = string
sensitive = true
}
resource "example_database" "main" {
initial_password = var.database_password
}
Provider가 attribute를 state에 저장하면 실제 값이 포함될 수 있다. Plan file도 변경 전후 값을 포함할 수 있으므로 민감한 artifact다.
다음 자료를 같은 등급으로 보호한다.
- Remote state snapshot
- Local state와 backup
- Saved plan file
- Crash log와 debug log
terraform state pullstdout- CI artifact와 cache
Terraform과 provider의 최신 기능에는 ephemeral value나 write-only argument가 있을 수 있지만 모든 resource가 지원하는 것은 아니다. Provider schema를 확인하고 가능하면 secret의 본문 대신 secret manager의 reference를 관리한다.
resource "example_service" "api" {
database_secret_reference = var.database_secret_arn
}
Reference 자체도 infrastructure topology 정보이므로 공개해도 된다고 단정하지 않는다.
Backend 접근 권한을 읽기와 쓰기로 분리하기
State read 권한은 infrastructure ID와 sensitive attribute를 볼 수 있는 강한 권한이다. “Apply만 안 하면 안전”하지 않다.
역할을 나눈다.
| 역할 | State read | State write | Cloud change |
|---|---|---|---|
| PR plan | 제한적 | 아니오 | read-only refresh |
| Approved apply | 예 | 예 | 필요한 범위 |
| Developer | 환경별 | 보통 아니오 | sandbox만 |
| Audit/backup | 암호화된 복구 범위 | 아니오 | 아니오 |
S3 object path 기준으로 environment별 IAM을 제한할 수 있다. Lock file에는 acquire/release를 위한 별도 object 권한이 필요하다.
Production state를 읽는 모든 job에서 untrusted PR code를 실행하지 않는다. Plan 결과를 PR comment에 그대로 올리면 sensitive하지 않은 것으로 표시된 내부 endpoint와 ID가 공개될 수 있으므로 redaction과 접근 범위를 검토한다.
State Versioning과 복구 절차 만들기
Remote state object versioning은 잘못된 overwrite와 삭제에서 이전 snapshot을 찾게 한다. Version이 있다고 자동 복구되는 것은 아니다.
복구 절차:
flowchart TD
A[Stop all writers]
B[Capture current state and remote evidence]
C[Identify last known good version]
D[Compare lineage and serial]
E[Restore in isolated review]
F[Refresh-only plan]
G[Approved corrective apply]
A --> B --> C --> D --> E --> F --> G복구 snapshot을 production backend에 바로 덮지 않는다. 현재 remote infrastructure에 그 이후 변경이 있을 수 있다.
State에는 lineage와 monotonically increasing serial이 있어 다른 state 계보나 오래된 snapshot overwrite를 막는 보호가 있다. state push -force는 이 보호를 우회할 수 있으므로 최후 수단이다.
정기 restore drill에서 확인한다.
- 누가 object version을 조회할 수 있는가
- KMS key와 backup account가 살아 있는가
- 삭제 방지와 retention이 요구 기간을 만족하는가
- 복구 state를 안전한 임시 환경에서 inspect할 수 있는가
- RTO 안에 plan 정합성까지 확인 가능한가
Workspace와 환경 분리를 혼동하지 않기
Terraform workspace는 같은 configuration에서 여러 state instance를 선택하는 기능이다. Production 격리의 모든 경계를 자동으로 만들지는 않는다.
terraform workspace select production
terraform workspace show
잘못된 workspace에서 apply하는 사고를 막으려면 account, credential, backend path, approval을 함께 분리한다.
locals {
expected_account_id = "123456789012"
}
data "aws_caller_identity" "current" {}
check "expected_account" {
assert {
condition = (
data.aws_caller_identity.current.account_id
== local.expected_account_id
)
error_message = "unexpected cloud account"
}
}
Account ID는 가상 값이다. Production과 development가 blast radius와 접근 정책이 다르면 별도 root configuration과 backend를 사용하는 편이 명확할 수 있다.
Workspace name을 resource name에 붙이는 것만으로 security boundary가 생기지 않는다.
State를 너무 크게 만들지 않기
모든 infrastructure를 하나의 state에 넣으면 한 번의 lock과 plan이 전체 조직을 막는다.
network + database + cluster + application + monitoring
반대로 지나치게 잘게 나누면 output 전달과 실행 순서가 복잡해진다. 경계 기준:
- 변경 주기와 담당 team
- Blast radius
- 필요한 cloud 권한
- Lifecycle과 삭제 주기
- Dependency 방향
- Plan/apply 시간
- 장애 시 함께 복구할 단위
flowchart LR
A[Network state]
B[Platform state]
C[Application state]
A --> B --> CDependency가 순환하지 않게 상위 platform이 안정된 identifier를 명시적으로 publish한다.
State를 나누면 한 stack의 lock이 다른 stack 변경을 막지 않지만 cross-state transaction은 없다. Network output 변경과 application apply 사이의 호환 기간을 설계해야 한다.
다른 Stack에 값을 전달하는 방법
terraform_remote_state data source로 다른 root state의 output을 읽을 수 있다.
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "sample-terraform-state"
key = "network/production/terraform.tfstate"
region = "ap-northeast-2"
}
}
그러나 output만 사용하는 것처럼 보여도 backend snapshot을 읽을 credential은 전체 state object 접근을 가질 수 있다. Sensitive resource가 있는 state를 여러 consumer에게 읽히는 구조는 피한다.
대안:
- Cloud provider data source로 tag/ID 조회
- Parameter store에 필요한 값만 publish
- DNS와 service discovery
- HCP Terraform의 제한된 output 전용 방식
- CI artifact가 아닌 configuration registry
Published contract에는 version과 owner를 둔다.
{
"schemaVersion": 2,
"vpcId": "vpc-example",
"privateSubnetIds": ["subnet-a-example", "subnet-b-example"]
}
ID는 가상 값이다.
Resource 이름을 바꿀 때 moved Block 사용하기
Resource address refactor는 state binding 이동을 함께 표현해야 한다.
moved {
from = aws_security_group.api
to = aws_security_group.application
}
Terraform은 기존 object를 새 address에 연결하는 plan을 만든다.
aws_security_group.api
has moved to
aws_security_group.application
Module로 이동할 때도 사용할 수 있다.
moved {
from = aws_instance.api
to = module.compute.aws_instance.api
}
moved block은 code review와 이후 사용자가 refactoring 의도를 함께 볼 수 있다는 장점이 있다. 모든 consumer가 충분히 새 configuration을 적용한 뒤 제거 시점을 정한다.
Old Terraform version 지원이나 긴급 조작에는 terraform state mv를 사용할 수 있지만 apply와 같은 locking·review 절차가 필요하다.
terraform state mv \
'aws_instance.api' \
'module.compute.aws_instance.api'
실행 전후 terraform state list와 plan을 저장해 검토한다.
기존 Resource를 Import하기
수동으로 만든 object를 관리하려면 configuration을 먼저 작성하고 import로 binding한다.
import {
to = aws_s3_bucket.assets
id = "sample-assets-production"
}
Bucket 이름은 가상이다. Import는 remote object를 자동으로 원하는 configuration과 똑같이 바꾸는 작업이 아니다. Binding 뒤 plan에서 attribute 차이를 확인한다.
1. 실제 object와 owner 확인
2. Configuration 작성
3. Import plan review
4. Binding 생성
5. Refresh와 normal plan
6. 예상치 못한 replace/delete 수정
7. Approved apply
같은 object를 두 address에 중복 import하지 않는다. HashiCorp 문서는 remote object와 resource instance의 one-to-one mapping을 기대한다.
state rm과 state push의 위험성
terraform state rm은 remote object를 삭제하지 않고 Terraform 관리에서 binding만 제거한다.
terraform state rm 'aws_s3_bucket.legacy'
Configuration에 resource block이 남아 있으면 다음 plan에서 새 object를 만들 수 있다. 관리 이관, object 보존 후 Terraform 책임 제거처럼 명확한 목적과 후속 configuration 변경이 필요하다.
terraform state pull은 state JSON을 stdout으로 출력한다.
terraform state pull > restricted-state-backup.json
Shell history, CI log, terminal recording, local backup 파일을 보호한다. 임시 파일은 암호화된 제한 경로에 두고 retention 뒤 안전하게 폐기한다.
terraform state push는 remote state를 overwrite하는 위험한 명령이다. Provider resource command와 달리 실수를 plan으로 충분히 미리 보여 주지 못할 수 있다.
지원되는 moved, import, state mv, state rm으로 해결하고 직접 편집과 push -force는 복구 runbook의 최후 단계로 제한한다.
Drift를 발견하고 처리하기
Console에서 수동 변경하면 configuration과 remote가 달라진다.
terraform plan -refresh-only
Drift를 발견했을 때 선택지는 두 가지다.
- Configuration대로 remote를 되돌린다.
- 수동 변경이 올바르면 configuration에 반영한다.
State만 수동으로 맞춰 차이를 숨기지 않는다. Policy와 configuration이 실제 의도를 표현해야 한다.
정기 drift detection job은 read-only credential로 plan하고 변경을 자동 apply하지 않는다.
jobs:
drift:
permissions:
contents: read
id-token: write
steps:
- run: terraform init -input=false
- run: terraform plan -refresh-only -detailed-exitcode
Exit code를 정확히 해석한다. Plan에는 민감하거나 내부적인 정보가 포함될 수 있으므로 공개 PR comment에 전체 출력하지 않는다.
CI에서 Plan과 Apply를 같은 State에 연결하기
PR에서 만든 saved plan을 승인 후 apply할 수 있지만 그 사이 state와 configuration이 바뀌지 않았는지 보장해야 한다.
sequenceDiagram
participant P as Plan job
participant S as Remote state
participant A as Apply job
P->>S: refresh under lock
P->>P: create reviewed plan
A->>A: verify commit and artifact
A->>S: acquire lock
A->>S: apply reviewed plan확인할 계약:
- Plan artifact가 같은 commit과 tool/provider lock에서 생성됨
- Artifact 접근이 승인자와 apply job에 제한됨
- Apply 전에 remote state가 바뀌었다면 saved plan이 거부됨
- Production environment approval이 있음
- Apply job만 state write와 cloud write 권한을 가짐
- Plan과 state artifact retention이 제한됨
terraform plan \
-out=reviewed.tfplan \
-lock-timeout=5m
terraform apply \
-lock-timeout=5m \
reviewed.tfplan
Saved plan은 binary artifact이며 secret이 들어갈 수 있다. Git에 commit하거나 public artifact로 올리지 않는다.
실패 시나리오를 포함한 검증
구현 체크리스트
마무리
Terraform state는 결과를 빠르게 계산하기 위한 cache가 아니다. Configuration의 resource address와 cloud의 실제 object identity를 연결하는 소유권 mapping이다. State를 잃으면 resource가 사라지는 대신 Terraform이 무엇을 관리하는지 모르게 된다.
팀에서는 locking과 암호화, versioning, audit가 가능한 remote backend를 사용해야 한다. State read 자체가 민감한 권한이며 sensitive 표시는 state에서 값을 제거하지 않는다. Saved plan과 local backup도 같은 등급으로 보호한다.
Resource refactoring은 moved block으로 binding 이동을 code에 남기고, 기존 object는 import 뒤 normal plan으로 차이를 검증한다. force-unlock, state rm, state push는 일상적인 편의 명령이 아니라 잘못 사용하면 소유권을 끊거나 state를 덮는 복구 도구다.
좋은 state 운영은 backend 설정 한 블록보다 넓다. 동시에 한 writer만 실행되고, 어떤 역할이 읽고 쓸 수 있는지 제한되며, 잘못된 snapshot을 실제로 복구해 본 증거가 있어야 한다.
관련 노트
- Docker 이미지 레이어와 빌드 캐시 이해하기
- CI에서 의존성 캐시를 안전하게 사용하는 방법
- GitHub Actions에서 비밀값 노출 막기
- Blue Green 배포와 Rolling 배포 비교