Published on

Terraform apply 멈춤 - 상태잠금 해제·복구 가이드

Authors

서버리스, VPC, IAM 같은 리소스를 Terraform으로 운영하다 보면 어느 날 terraform apply가 멈춘 듯 보이거나, 다음 실행에서 상태 잠금 때문에 진행이 안 되는 상황을 겪습니다. 대부분은 백엔드 상태 잠금(lock)과 장시간 프로비저닝, 혹은 이전 실행의 비정상 종료로 인한 잠금 잔존이 원인입니다.

이 글에서는 “무작정 force-unlock부터 치지 않고”, 현재 상황을 분류한 뒤 안전하게 잠금을 해제하고 상태를 복구하는 순서를 다룹니다. 장애 복구 관점은 분산 시스템에서의 복구와 유사합니다. 예를 들어 보상 트랜잭션/복구 전략을 정리한 글인 MSA 사가 패턴 - 보상 트랜잭션 실패 복구 전략처럼, Terraform도 “어떤 단계에서 멈췄는지”를 먼저 확인해야 합니다.

1) 증상 분류: 진짜 멈춤 vs 잠금 대기

terraform apply가 멈춘 것처럼 보여도 원인은 크게 3가지로 나뉩니다.

1-1. 실제로는 오래 걸리는 프로비저닝

예: NAT Gateway, EKS, RDS, ACM 인증, Route53 검증 등은 생성 단계에서 몇 분에서 수십 분까지 걸릴 수 있습니다.

확인 포인트

  • 콘솔에서 해당 리소스가 생성 중인지 확인
  • TF_LOG=INFO 또는 TF_LOG=DEBUG로 진행 로그 확인
  • terraform apply -refresh=false로 리프레시 비용을 줄일 수 있는지 검토(상황에 따라)

1-2. 상태 잠금(lock) 대기

대부분 아래 메시지로 드러납니다.

  • Error acquiring the state lock
  • Lock Info: 와 함께 ID, Operation, Who, Version, Created 등이 표시

이 경우는 “다른 누군가가 apply 중”이거나 “이전 실행이 비정상 종료되어 잠금이 남아있는 상태”입니다.

1-3. 프로바이더/네트워크 문제로 apply가 멎은 상태

예: AWS API 타임아웃, 권한 문제, 네트워크 단절, 로컬 머신 슬립 등

이 경우는 터미널에서는 멈춘 것 같아도 실제로는 리소스가 부분 생성되었을 수 있습니다. 따라서 강제 잠금 해제 전에 “현재 리소스가 어떤 상태인지”부터 확인해야 합니다.

2) 잠금의 원리: 왜 Terraform은 잠금을 거는가

Terraform은 상태 파일을 단일 진실 소스(source of truth)로 사용합니다. 동시에 두 개의 apply가 같은 상태에 쓰기를 시도하면, 상태 파일이 꼬이거나(리소스 주소 충돌), 의도하지 않은 삭제/생성이 발생할 수 있습니다.

그래서 백엔드가 잠금을 지원하면 Terraform은 다음을 수행합니다.

  • 상태 읽기 전 잠금 획득
  • 플랜/적용 중 상태 변경을 독점
  • 종료 시 잠금 해제

대표적으로

  • S3 백엔드 + DynamoDB 테이블을 통한 잠금
  • Terraform Cloud/Enterprise의 원격 상태 잠금
  • 일부 백엔드는 잠금 미지원(이 경우 동시 실행 방지 장치가 약해짐)

3) 가장 먼저 할 일: “누가 잠금 잡고 있는지” 확인

잠금 오류 메시지에 Lock Info가 나오면, 그 정보를 그대로 기록해두세요.

특히 다음 항목이 중요합니다.

  • ID: 강제 해제 시 필요
  • Who: 실행 주체(사용자, CI)
  • Created: 잠금 생성 시각
  • Operation: apply 인지 plan 인지

잠금이 “정상 동작 중인 다른 apply”라면, 잠금을 해제하면 그 실행이 상태 기록을 못 하게 되어 더 큰 문제를 만들 수 있습니다.

4) 안전한 복구 절차(권장 순서)

여기부터는 “잠금이 남아있다”는 가정에서의 안전 절차입니다.

4-1. 실행 중인 Terraform 프로세스가 있는지 확인

로컬에서 실행했다면 먼저 프로세스가 살아있는지 확인합니다.

ps aux | grep terraform

CI에서 실행했다면

  • GitHub Actions, Jenkins, GitLab CI 등에서 해당 Job이 아직 실행 중인지
  • 원격 실행(Terraform Cloud)이라면 해당 Run이 진행 중인지

부터 확인합니다.

Jenkins에서 조건 누락으로 의도치 않은 병렬 실행이 생기는 경우도 있습니다. CI 병렬 실행이 잠금의 원인이라면 파이프라인부터 손봐야 합니다. 관련해서는 Jenkins Declarative Pipeline when+matrix 조건 누락 해결 같은 케이스가 실무에서 자주 연결됩니다.

4-2. 리소스가 실제로 생성 중인지 콘솔에서 확인

예를 들어 AWS라면

  • EC2 인스턴스 생성 중인지
  • EKS 클러스터가 CREATING인지
  • RDS가 modifying인지

를 확인합니다.

만약 리소스가 정상적으로 생성 중이면, 잠금은 건드리지 말고 기다리는 편이 안전합니다.

4-3. 상태 백업을 먼저 확보

강제 잠금 해제 전에는 항상 상태를 백업합니다.

원격 백엔드라도 state pull로 현재 상태를 로컬에 저장할 수 있습니다.

terraform state pull > state-backup-$(date +%Y%m%d%H%M%S).tfstate

이 파일은 복구의 마지막 보루입니다.

4-4. 강제 잠금 해제 force-unlock

잠금이 “유령 잠금”으로 판단되면, 잠금 ID를 이용해 해제합니다.

terraform force-unlock 3b1a2c4d-xxxx-xxxx-xxxx-xxxxxxxxxxxx

주의사항

  • 같은 워크스페이스/상태에 대해 누군가가 실제 apply 중이면 절대 실행하지 마세요.
  • 원격 실행 도중 네트워크가 끊겨 로컬에서는 멈춘 것처럼 보이지만, 원격에서는 계속 실행 중일 수 있습니다.

4-5. 다시 실행하기 전에 plan으로 드리프트 확인

잠금을 풀었다면 바로 apply를 치기보다, 현재 인프라와 상태가 얼마나 어긋났는지 먼저 봅니다.

terraform plan -out tfplan

만약 예상치 못한 대량 삭제/재생성이 보이면, 상태가 꼬였거나 구성 변경이 잘못 반영된 것입니다. 이때는 apply를 멈추고 아래 “상태 복구” 섹션으로 넘어가세요.

5) 상태가 꼬였을 때: 복구 전략 4가지

잠금만 풀면 끝나는 경우도 많지만, apply 중단으로 인해 “리소스는 만들어졌는데 상태에는 없거나”, “상태에는 있는데 실제는 없는” 불일치가 생길 수 있습니다.

5-1. 가장 흔한 케이스: 리소스는 존재하지만 상태에 없음 import

예: VPC는 이미 있는데 Terraform이 새로 만들려고 함

이때는 해당 리소스를 상태로 가져옵니다.

terraform import aws_vpc.main vpc-0abc1234def567890

가져온 뒤에는 반드시 plan으로 변경 사항이 안정적인지 확인합니다.

5-2. 상태에는 있는데 실제 리소스가 없음 state rm 후 재생성

예: 누군가 콘솔에서 리소스를 삭제했거나, 실패로 생성이 롤백되었는데 상태만 남음

이때는 상태에서 해당 주소를 제거하고, 다음 apply에서 재생성되도록 합니다.

terraform state rm aws_security_group.app

주의: state rm은 실제 리소스를 삭제하지 않습니다. “상태에서만 제거”합니다.

5-3. 주소 변경/모듈 구조 변경으로 인한 꼬임 moved 또는 state mv

모듈 리팩터링, 리소스 이름 변경 등으로 주소가 바뀌면 Terraform은 “삭제 후 생성”으로 해석할 수 있습니다.

가능하면 코드에 moved 블록을 선언해 안전하게 이동시키는 편이 좋고, 급한 경우 state mv를 사용합니다.

terraform state mv module.old.aws_s3_bucket.logs module.new.aws_s3_bucket.logs

5-4. 최후의 수단: 상태 롤백(버전 관리 활용)

  • S3 버킷 버저닝을 켜두었다면 이전 버전의 상태 파일로 롤백 가능
  • Terraform Cloud라면 State 버전 히스토리에서 복구 가능

상태는 데이터베이스처럼 다뤄야 합니다. 이벤트 소싱에서 스냅샷이 꼬이면 복구 전략이 필요하듯, Terraform도 상태 히스토리와 백업이 곧 복구력입니다. 관련 사고 대응 관점은 Event Sourcing 스냅샷 꼬임 - 중복·유실 복구 전략에서의 접근과 닮아 있습니다.

6) 백엔드별 잠금 트러블슈팅

6-1. S3 + DynamoDB 잠금

구성 예시(잠금 테이블 사용)

terraform {
  backend "s3" {
    bucket         = "my-tfstate-bucket"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

체크리스트

  • DynamoDB 테이블의 파티션 키가 LockID인지(일반적으로 Terraform 문서 권장 스키마)
  • 테이블에 TTL을 걸어 “영구 유령 잠금”을 줄일지(운영 정책에 따라)
  • IAM 권한에 dynamodb:PutItem, dynamodb:GetItem, dynamodb:DeleteItem 등이 포함되어 있는지

주의: DynamoDB에서 잠금 레코드를 콘솔로 직접 삭제하는 방식은 권장하지 않습니다. 반드시 terraform force-unlock을 1순위로 고려하세요. 직접 삭제는 “정말로 Terraform이 죽었는지” 확인이 어려워 사고를 키울 수 있습니다.

6-2. Terraform Cloud/Enterprise

  • Run이 실제로 진행 중인지 UI에서 확인
  • 취소(Cancel) 후 상태가 정상 반영되었는지 확인
  • 동일 워크스페이스에 대한 동시 실행 정책(Queueing) 설정 점검

7) 재발 방지 체크리스트

7-1. CI에서 동시 실행 방지

  • 브랜치별 워크스페이스 분리
  • 환경별 상태 키 분리
  • 파이프라인에서 mutex(락) 또는 단일 실행 정책 적용

7-2. apply 시간을 줄이는 설계

  • 너무 큰 단일 스택을 분리(네트워크, EKS, 앱, 데이터 계층)
  • 모듈/스택 경계 명확화
  • 장시간 걸리는 리소스는 변경 빈도 낮은 스택으로 이동

7-3. 장애 시 “멱등성”을 의식한 운영

Terraform은 선언형이지만, 프로바이더 API와 외부 시스템은 항상 멱등적이지 않을 수 있습니다. 특히 부분 생성, eventual consistency, API rate limit이 겹치면 apply가 늘어지고 잠금 충돌이 증가합니다.

8) 실전 복구 시나리오 예시

상황

  • 어제 밤 CI에서 terraform apply가 실패
  • 오늘 아침 로컬에서 실행하니 Error acquiring the state lock
  • Lock InfoWho는 CI 사용자, Created는 어제 밤

권장 절차

  1. CI에서 해당 Job이 정말 종료됐는지 확인
  2. 원격 실행이 아니라면, CI 워커에 남은 terraform 프로세스가 없는지 확인
  3. terraform state pull로 상태 백업
  4. terraform force-unlock으로 잠금 해제
  5. terraform plan으로 대량 변경 여부 확인
  6. 필요한 경우 import 또는 state rm로 상태 정합성 회복
  7. terraform apply 재시도

마무리

terraform apply 멈춤은 “잠금 문제”와 “프로비저닝 지연”이 섞여 보이는 경우가 많습니다. 그래서 복구의 핵심은

  • 누가 잠금을 잡았는지 확인
  • 상태 백업을 먼저 확보
  • 강제 해제는 마지막에
  • 상태 불일치가 보이면 import/state rm/state mv로 정합성 회복

입니다.

운영 환경에서는 특히 CI 동시 실행 방지와 상태 히스토리(버저닝/백업)가 복구 시간을 결정합니다. 잠금 해제는 버튼 한 번이지만, 안전하게 누르는 절차가 곧 신뢰성입니다.