Published on

Terraform state 잠금 오류 - DynamoDB 412 해결

Authors

Terraform을 S3 backend + DynamoDB lock 조합으로 운영하다 보면, 어느 날 갑자기 terraform plan/apply가 잠금 오류로 멈추는 경우가 있습니다. 특히 CI에서 병렬 실행이 많거나, 로컬에서 작업하다가 중간에 프로세스가 죽었을 때 자주 터집니다.

대표적으로 아래처럼 DynamoDB 쪽에서 412(Precondition Failed) 또는 ConditionalCheckFailedException 류 메시지가 보입니다.

Error acquiring the state lock

Error message: ConditionalCheckFailedException: The conditional request failed
status code: 400, request id: ...

Lock Info:
  ID:        ...
  Path:      .../terraform.tfstate
  Operation: OperationTypeApply
  Who:       ...
  Version:   ...
  Created:   ...
  Info:      ...

이 글에서는 DynamoDB 412 잠금 오류가 왜 발생하는지, 안전하게 잠금을 해제하는 절차, 그리고 재발을 줄이는 운영 패턴을 실전 관점에서 정리합니다.

DynamoDB 412는 무슨 뜻인가

Terraform의 DynamoDB 잠금은 “잠금 레코드를 조건부로 넣는다”는 방식으로 구현됩니다. 즉, 잠금 키(보통 state 파일 경로)를 기준으로 DynamoDB에 아이템을 쓰되, 이미 존재하면 쓰기를 실패시키는 조건을 겁니다.

이때 DynamoDB는 조건이 만족되지 않으면 ConditionalCheckFailedException을 반환하고, 일부 도구/프록시/표기에서는 이를 412 Precondition Failed로 표현하기도 합니다.

정리하면 다음 중 하나입니다.

  • 정상적인 경쟁 상황: 다른 plan/apply가 이미 잠금을 잡고 있음
  • 고아 잠금(stale lock): 이전 작업이 죽거나 네트워크가 끊겨 잠금이 남아 있음
  • 잠금 테이블 구성 문제: 키 스키마/권한/테이블을 잘못 만들어 잠금 갱신이 기대대로 안 됨

가장 먼저 확인할 것: “진짜로 누가 잡고 있나”

잠금 해제부터 하기 전에, 먼저 동시에 돌아가는 파이프라인/작업자가 없는지 확인해야 합니다. 운영에서 가장 위험한 시나리오는 “실제로 다른 apply가 진행 중인데 lock을 강제로 풀어버리는 것”입니다.

1) Terraform 출력의 Lock Info 읽기

에러 메시지에 Who, Created, Operation, Path가 포함됩니다.

  • Who: 누가 실행했는지(대부분 user@host)
  • Created: 잠금 생성 시간
  • Operation: apply인지 plan인지
  • Path: 어떤 state를 잠갔는지

이 정보만으로도 “지금 CI에서 도는 작업인지” 감이 오는 경우가 많습니다.

2) DynamoDB에서 잠금 레코드 직접 확인

AWS CLI로 잠금 테이블을 조회합니다. 테이블명은 backend 설정의 dynamodb_table 값입니다.

잠금 키는 환경에 따라 다를 수 있지만, 일반적으로 LockID가 파티션 키이고 값이 state 경로인 경우가 많습니다.

aws dynamodb get-item \
  --table-name terraform-locks \
  --key '{"LockID": {"S": "my-bucket/path/to/terraform.tfstate"}}'

아이템이 존재한다면, Info 같은 필드에 JSON 문자열로 잠금 메타가 들어있습니다.

안전한 해결 1: 정상 경쟁이면 “기다리거나 직렬화”

가장 흔한 원인은 CI에서 같은 workspace/state를 동시에 건드리는 상황입니다.

  • PR 파이프라인과 main 배포 파이프라인이 동시에 실행
  • 모노리포에서 동일 state를 여러 job이 병렬로 apply
  • 사람이 로컬에서 apply 중인데 CI가 또 apply

이 경우 해결책은 “잠금을 풀기”가 아니라 동시 실행을 막는 것입니다.

  • GitHub Actions라면 concurrency로 직렬화
  • GitLab CI라면 resource_group 사용
  • Jenkins라면 lock 플러그인/뮤텍스 사용

이 패턴은 Kubernetes에서 동일 리소스를 동시에 갱신하다가 상태가 꼬이는 문제와도 비슷합니다. 운영에서 “경쟁을 막는 장치”가 결국 장애를 줄입니다. 관련해서는 Argo CD Sync 실패 - OutOfSync·Health Degraded 9가지에서 동기화/경쟁 관점의 장애 패턴도 함께 참고할 만합니다.

안전한 해결 2: 고아 잠금이면 force-unlock 사용

프로세스가 죽어서 lock이 남은 경우, Terraform이 제공하는 표준 해법은 force-unlock입니다.

1) Lock ID 확인

에러 메시지의 ID: 값을 복사합니다.

2) 강제 해제

terraform force-unlock <LOCK_ID>

MDX 렌더링 환경에서 부등호가 문제될 수 있어 위 명령의 LOCK_ID 표기는 인라인 코드로만 이해하면 됩니다.

-force 옵션

대화형 확인 없이 진행하려면:

terraform force-unlock -force <LOCK_ID>

3) 해제 후 재시도

terraform plan
terraform apply

언제 force-unlock을 쓰면 안 되나

  • 다른 apply가 실제로 진행 중인데, 단지 로그를 못 보고 있는 상황
  • 동일 state를 다른 팀/다른 리전에서 운영 중이라 현재 상황 파악이 안 되는 경우

이때는 먼저 CI 실행 내역, CloudTrail, DynamoDB 아이템의 Created 시간 등을 보고 “진짜 고아인지”를 판단하세요.

해결 3: DynamoDB 아이템을 직접 삭제(최후의 수단)

force-unlock이 실패하거나, Lock ID를 알 수 없거나, 상태가 비정상적으로 꼬인 경우에만 DynamoDB에서 아이템을 직접 지우는 방법을 씁니다.

aws dynamodb delete-item \
  --table-name terraform-locks \
  --key '{"LockID": {"S": "my-bucket/path/to/terraform.tfstate"}}'

주의사항:

  • 삭제 전 반드시 “현재 apply가 없다는 것”을 확인
  • 가능하면 변경 윈도우/승인 절차를 거칠 것
  • 삭제 후 즉시 plan으로 state 무결성 확인

재발 방지 1: backend 설정 점검(테이블/키/권한)

DynamoDB 테이블 키 스키마

Terraform이 기대하는 키 이름은 구성에 따라 다르지만, 일반적으로 다음 조건이 맞아야 합니다.

  • 파티션 키 1개(예: LockID)만 사용
  • 타입은 S(String)

Terraform이 쓰는 키와 테이블 정의가 다르면 잠금이 비정상 동작하거나, 해제가 안 되는 상황이 생길 수 있습니다.

IAM 권한

잠금에 필요한 DynamoDB 권한이 빠지면, 잠금 획득/해제가 불완전해져 “잠금은 남고 작업은 실패” 같은 형태가 나올 수 있습니다.

최소한 아래 액션이 필요합니다.

  • dynamodb:GetItem
  • dynamodb:PutItem
  • dynamodb:DeleteItem
  • dynamodb:UpdateItem

또한 S3 state에 대해서는 s3:GetObject, s3:PutObject, s3:ListBucket 등이 필요합니다.

재발 방지 2: CI에서 planapply 동시성 제어

동시성 제어는 “잠금 오류를 줄이는 것”뿐 아니라, 의도치 않은 순서 꼬임을 막습니다.

예: GitHub Actions의 concurrency

concurrency:
  group: terraform-prod
  cancel-in-progress: false

핵심은 “같은 state를 건드리는 작업은 한 번에 하나”입니다.

재발 방지 3: 작업 시간이 긴 apply를 줄이기

apply가 오래 걸릴수록 잠금이 오래 유지되고, 그 사이 다른 작업이 들어와 충돌할 확률이 커집니다.

  • 모듈을 쪼개 state를 분리(도메인 단위, 서비스 단위)
  • 한 state에서 너무 많은 리소스를 관리하지 않기
  • 외부 의존(예: Helm chart, 대규모 IAM 변경)을 분리

이건 네트워크 타임아웃으로 장애가 증폭되는 패턴과도 유사합니다. 트랜잭션(여기서는 apply) 시간을 줄이면 전체 안정성이 올라갑니다. 관련해서 인프라 관점의 타임아웃 점검은 AWS ALB 502/504 급증 - 타임아웃 7곳 점검도 함께 참고할 수 있습니다.

자주 묻는 질문(현업에서 많이 만나는 케이스)

Q1. -lock=false로 우회하면 안 되나

가능은 하지만, 운영에서는 권장하지 않습니다.

terraform apply -lock=false

-lock=false는 “잠금 메커니즘을 무시”하기 때문에, 동일 state에 동시 쓰기가 발생하면 state가 깨질 수 있습니다. 정말 긴급 복구가 아니라면 피하세요.

Q2. 고아 잠금인지 어떻게 더 확실히 판단하나

  • Created 시간이 충분히 오래되었는지(예: 1시간 이상)
  • CI 실행 내역에서 해당 시점 job이 실패/중단됐는지
  • CloudTrail에서 해당 IAM principal이 아직 작업 중인지 흔적 확인

Q3. 잠금이 자주 남는다면 근본 원인은

  • CI job 강제 종료(타임아웃)로 Terraform 프로세스가 정상 종료 못 함
  • 네트워크 끊김으로 backend 업데이트가 실패
  • provider hang으로 apply가 비정상 종료

이 경우 CI 타임아웃을 Terraform 작업 시간에 맞추고, 불안정한 provider 작업을 분리하는 게 효과적입니다.

결론: 412는 “잠금 경쟁” 또는 “고아 잠금” 신호다

DynamoDB 412는 Terraform이 잠금을 조건부로 획득하려다 실패했다는 뜻입니다. 해결은 단순히 lock을 지우는 게 아니라, 아래 순서로 접근하는 것이 안전합니다.

  1. Lock Info와 CI 실행을 보고 “진짜 경쟁인지” 확인
  2. 고아 잠금이면 terraform force-unlock으로 해제
  3. 최후의 수단으로 DynamoDB 아이템 직접 삭제
  4. 재발 방지를 위해 CI 직렬화와 state 분리로 동시성 자체를 줄이기

이 흐름을 팀 런북으로 만들어두면, 새벽에 412가 떠도 침착하게 복구할 수 있습니다.