Terraform: Error acquiring the state lock
안녕하세요, BlueByte입니다. terraform apply를 실행했는데 plan 대신 Error: Error acquiring the state lock이 뜹니다. 무언가 고장 난 게 아닙니다 — Terraform은 상태를 쓸 수 있는 작업 전에 state를 잠그는데, 지금은 다른 누군가가 그 lock을 쥐고 있다고 판단한 것입니다. 증상은 명령이 곧바로 멈추면서 Lock Info: 블록과 Error message: 줄을 내는 것입니다. 오늘은 이 lock이 무엇인지, 그 블록을 읽어 진짜 충돌과 남은 lock을 구분하는 법, 백엔드별로 안전하게 푸는 법, 그리고 다시 걸리지 않게 하는 법까지 하나씩 짚어보겠습니다.
"Error acquiring the state lock"가 알려주는 것
Terraform은 잡지 못한 lock의 정보와 함께 실패를 출력합니다:
╷
│ Error: Error acquiring the state lock
│
│ Error message: operation error S3: PutObject, ... api error
│ PreconditionFailed
│ Lock Info:
│ ID: 4d1e3f0e-2b7a-9c31-8f0a-3b2c1d4e5f6a
│ Path: my-tf-states/prod/terraform.tfstate
│ Operation: OperationTypeApply
│ Who: jenkins@ci-runner-7
│ Version: 1.9.5
│ Created: 2026-09-05 09:14:22.301 +0000 UTC
│ Info:
│
│ Terraform acquires a state lock to protect the state from being written
│ by multiple users at the same time. Please resolve the issue above and try
│ again. For most commands, you can disable locking with the "-lock=false"
│ flag, but this is not recommended.
╵Error message: 줄은 백엔드에 따라 달라집니다. 새로운 S3 네이티브 lock에서는 .tflock 객체에 대한 S3 PreconditionFailed 조건부 쓰기 오류가 보이고, 예전 DynamoDB lock에서는 ConditionalCheckFailedException이 보입니다. 어느 쪽이든 Lock Info: 블록은 동일하며, 가장 먼저 읽어야 할 부분입니다.
lock이 왜 걸린 채로 남았나
lock이 잡혀 있다는 건 보통 다음 중 하나입니다:
- 다른 실행이 실제로 진행 중 — 동료나 CI job이 지금 apply하고 있습니다. lock이 제 역할을 하는 중입니다.
- 이전 실행이 중단됨 — 누군가 나쁜 타이밍에 Ctrl-C를 눌렀거나 apply 도중 네트워크가 끊겨서 Terraform이 lock을 풀지 못했습니다.
- CI job이 강제 종료됨 — 러너가 취소되거나 타임아웃돼서,
Who에 러너 신원이 남은 채 lock만 남았습니다. - 크래시 — Terraform이나 백엔드가 unlock 단계 전에 죽었습니다.
첫 번째만 진짜 충돌입니다. 나머지는 남은(stale) lock입니다 — 아무것도 실행되고 있지 않은데 lock 레코드만 남아 있는 것입니다.
손대기 전에 Lock Info 블록부터 읽기
반사적으로 force-unlock하지 말고 블록부터 읽으세요. Who와 Created가 거의 모든 걸 말해 줍니다. Who가 본인 CI 러너인데 아무 job도 안 돌고 Created가 한 시간 전이면 남은 lock입니다. Who가 동료이고 Created가 1분 전이면 누군가 apply 중이니 기다려야지 풀면 안 됩니다. CI 대시보드에서 활성 job이 없는지, 로컬에서 Terraform을 돌리는 사람이 없는지 확인하세요. unlock에 쓸 ID 값을 적어 둡니다.
그냥 기다려야 할 때, 그리고 -lock-timeout으로 경합 피하기
lock이 진짜라면 가장 단순한 해결은 기다리는 것입니다. 즉시 실패하는 대신 Terraform이 기다리게 만들 수 있습니다:
terraform apply -lock-timeout=120s-lock-timeout을 주면 Terraform이 지정한 시간 동안 lock 획득을 재시도한 뒤 포기하므로, 다른 실행과 몇 초 겹치는 실행도 오류 없이 성공합니다. 기본값은 0s로 즉시 실패입니다. 이걸 넘기려고 -lock=false에 손대지 마세요 — locking을 통째로 끄는 것이라 apply 두 개가 state를 동시에 망가뜨릴 수 있습니다.
terraform force-unlock으로 남은 lock 지우기
아무것도 실행되고 있지 않음을 확인했다면, ID로 남은 lock을 지웁니다:
terraform force-unlock 4d1e3f0e-2b7a-9c31-8f0a-3b2c1d4e5f6aTerraform이 확인을 요청하면 yes를 입력합니다. 파이프라인에서는 -force로 프롬프트를 건너뜁니다:
terraform force-unlock -force 4d1e3f0e-2b7a-9c31-8f0a-3b2c1d4e5f6aforce-unlock은 lock 레코드만 제거합니다 — state 내용이나 인프라는 건드리지 않습니다. 동작은 백엔드에 따라 다릅니다: 다른 프로세스가 쥔 순수 로컬 state 파일은 풀 수 없지만, 원격 백엔드(S3, DynamoDB, HCP Terraform)에서는 lock을 지워 다음 실행이 깔끔하게 획득하게 합니다.
실제 사례: apply 도중 죽은 CI job
파이프라인이 DynamoDB lock 테이블을 쓰는 S3 백엔드에 대해 terraform apply를 돌립니다. apply 중에 누군가 job을 취소합니다. 다음 실행이 Error acquiring the state lock, Error message: ConditionalCheckFailedException, Who: gitlab-runner@runner-3, Created: 2026-09-05 09:14로 실패합니다. GitLab을 보니 도는 job이 없어 남은 lock입니다. 블록에서 ID를 복사해 유지보수 셸에서 terraform force-unlock -force <ID>를 실행하고 파이프라인을 다시 돌리면 apply가 정상 진행됩니다. lock 테이블의 항목은 사라졌고, state는 손상되지 않았습니다 — 죽은 job이 쓰기를 끝내지 못했기 때문입니다.
lock이 사라지고 실행이 진행되는지 확인
작업을 다시 실행해 lock을 통과하는지 봅니다:
terraform planAcquiring state lock. This may take a few moments...
...
No changes. Your infrastructure matches the configuration.plan까지 도달하거나 — Acquiring state lock 줄 뒤에 정상 출력이 이어지면 — lock이 깔끔하게 획득·해제된 것입니다. S3 네이티브 lock이라면 실행 후 버킷에서 .tflock 객체가 사라져 있습니다.
lock이 걸린 채 남지 않게 예방하기
중단된 실행을 죽이기보다 끝까지 마치게 하세요 — Terraform은 오류가 나더라도 깔끔하게 종료하면 lock을 풉니다. CI에서는 concurrency group을 써서 Terraform job 두 개가 같은 state에 동시에 돌지 않게 하고, apply 도중 죽지 않도록 job에 넉넉한 타임아웃을 줍니다. 공유 state에는 완만한 -lock-timeout을 걸어 짧은 겹침은 실패 대신 기다리게 합니다. force-unlock은 남은 lock임이 확인된 경우에만 쓰세요.
state 버전 불일치와는 어떻게 다른가
lock 오류는 지금 누가 state를 쥐고 있는가에 대한 것입니다. state snapshot was created by Terraform vX, which is newer than current vY 같은 다른 오류는 state의 형식에 대한 것이라 unlock으로는 고쳐지지 않습니다 — Terraform을 맞는 버전으로 업그레이드해야 합니다. 메시지가 Lock Info: 블록이 아니라 버전을 언급한다면 lock이 아니라 버전 불일치입니다.
관련 질문
그냥 -lock=false로 넘기면 안 되나요?
안 됩니다. state locking을 통째로 끄는 것이라 동시에 도는 두 실행이 state를 같은 시점에 써서 망가뜨릴 수 있습니다. 진짜 실행이라면 기다리고, 남은 lock임이 확인되면 force-unlock을 쓰세요.
force-unlock이 state를 풀 수 없다고 합니다.
다른 프로세스가 쥔 순수 로컬 state 파일은 두 번째 Terraform이 풀 수 없습니다. 그 프로세스를 닫으세요. force-unlock은 원격 백엔드(S3, DynamoDB, HCP Terraform)용으로, 거기서 lock 레코드를 지웁니다.
force-unlock을 했는데도 다음 실행이 lock 획득에 실패합니다.
실제로 실행이 진행 중이거나(새 Lock Info의 ID·Created가 더 최신일 것입니다), apply하는 것과 다른 workspace나 백엔드를 unlock한 것입니다. workspace를 맞추고 블록을 다시 읽으세요.
LOCK_ID는 어디서 얻나요?
오류의 Lock Info 블록에 찍힌 ID 필드입니다. terraform force-unlock에 그대로 복사해 넣으세요; nonce 역할을 하므로 방금 본 그 lock만 풀 수 있습니다.
apply 도중 실행이 죽으면 state가 손상되나요?
대개는 아닙니다. Terraform은 state를 하나의 객체로 통째로 쓰므로, 쓰기 전에 죽은 job은 이전 state를 그대로 남기고 lock만 남깁니다. force-unlock은 state가 아니라 lock만 제거합니다.
참고 자료
Haneul Seo
Infrastructure engineer · 10+ years running Linux fleets
같은 카테고리 다른 글
Git: fatal: detected dubious ownership in repository
Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.
Kubernetes: Internal error occurred: failed calling webhook
admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.
MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.
Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk
읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.
Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)
V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.
Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트
컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.