본문으로 건너뛰기

Terraform 상태 파일 운영법: Remote Backend·State Locking·Drift Detection으로 인프라를 안전하게 관리하기

DevOps23분 읽기본문 11,469

핵심 요약

이 글에서 확인할 내용

인프라가 "코드"라면, 그 코드의 상태를 누가 책임지는가 Terraform을 처음 도입한 팀이 가장 빨리 맞닥뜨리는 위기는 terraform apply 를 두 사람이 동시에 실행하는 순간입니다. 우리 팀의 플랫폼 팀에서 정확히 이 상황이 벌어졌습니다. SRE 한 명이 EKS 노드 그룹 스케일아웃을 적용하는 동안, 다른 팀원이 보안 그룹 규칙을 추가했습니다.

  • 1. terraform.tfstate 구조와 의미
  • 2. 로컬 상태의 위험: 단일 장애 지점, 시크릿 평문 저장
  • 3. S3 + DynamoDB Remote Backend 설정

Terraform state remote backend locking drift detection

인프라가 "코드"라면, 그 코드의 상태를 누가 책임지는가

Terraform을 처음 도입한 팀이 가장 빨리 맞닥뜨리는 위기는 terraform apply를 두 사람이 동시에 실행하는 순간입니다. 우리 팀의 플랫폼 팀에서 정확히 이 상황이 벌어졌습니다. SRE 한 명이 EKS 노드 그룹 스케일아웃을 적용하는 동안, 다른 팀원이 보안 그룹 규칙을 추가했습니다. 두 작업 모두 성공한 것처럼 보였지만, 이후 terraform plan을 실행하자 누가 만든 것인지 모를 리소스 diff가 수십 줄씩 쏟아졌습니다.

이 글은 그 경험에서 출발합니다. terraform.tfstate가 무엇인지, 왜 로컬에 두면 안 되는지, S3와 DynamoDB를 결합한 Remote Backend가 어떻게 이 문제를 해결하는지 설명합니다. 여기에 더해 상태 파일이 실제로 꼬였을 때의 복구 절차, 로컬에서 Remote Backend로 전환하는 마이그레이션 체크리스트, 그리고 실무에서 반복적으로 받는 질문까지 다뤄 오늘 바로 팀 운영에 적용할 수 있도록 구성했습니다. GitOps 워크플로와 연결하는 부분은 ArgoCD App of Apps 구조 설계 글에서 더 자세히 다루고 있습니다.


1. terraform.tfstate 구조와 의미

terraform.tfstate는 Terraform이 관리하는 리소스의 현재 상태를 기록한 JSON 파일입니다. Terraform은 이 파일을 기준으로 실제 인프라와의 차이를 계산합니다.

상태 파일의 구조를 이해하면 왜 이것이 민감한 데이터인지 알 수 있습니다. 최상위 키는 version, terraform_version, serial, lineage, outputs, resources로 구성됩니다. serial은 상태 파일이 변경될 때마다 1씩 증가하는 정수로, 상태 충돌 감지에 사용됩니다.

resources 배열 안에는 각 리소스의 모든 속성 값이 그대로 저장됩니다. RDS 인스턴스라면 마스터 비밀번호, VPC ID, 서브넷 구성, 엔드포인트 주소까지 평문으로 들어갑니다. IAM 역할이라면 정책 내용이, ElastiCache라면 인증 토큰이 남습니다. Terraform 공식 문서는 이 파일을 민감 정보로 취급하고 안전하게 저장하도록 명시합니다.

실무에서 terraform state pull 명령으로 원격 상태를 그대로 받아 jq로 파싱해보면 이 구조가 손에 잡힙니다. 예를 들어 terraform state pull | jq '.resources[] | select(.type == "aws_db_instance")'를 실행하면 RDS 리소스의 속성이 모두 노출되는 것을 직접 확인할 수 있습니다. 리소스가 수백 개를 넘어가는 대형 프로젝트에서는 state 파일 자체가 수십 메가바이트로 불어나기도 하는데, 이 경우 planapply 실행 시마다 전체 파일을 읽고 직렬화하는 비용이 커져 체감 속도가 눈에 띄게 느려집니다. 우리 팀에서는 하나의 state 파일에 리소스가 200개를 넘어서자 plan 실행 시간이 40초를 넘어섰고, 이 경험이 뒤에서 다룰 모듈 단위 state 분리를 결정한 직접적인 계기가 되었습니다.


2. 로컬 상태의 위험: 단일 장애 지점, 시크릿 평문 저장

terraform.tfstate를 로컬 파일시스템에 두면 네 가지 위험이 동시에 생깁니다.

단일 장애 지점: 상태 파일이 특정 개발자의 노트북에만 있다면 그 사람이 휴가 중이거나 노트북이 고장나면 인프라 변경 자체가 불가능해집니다.

협업 불가능: 여러 명이 같은 .tfstate 파일을 각자 로컬에 가지면 어느 파일이 진실인지 알 수 없습니다.

시크릿 평문 저장: 상태 파일에는 리소스의 모든 속성이 그대로 담깁니다. 이 파일이 Git 저장소에 올라가는 순간, 마스터 DB 비밀번호와 API 키가 히스토리에 영구 기록됩니다. 시크릿 관리 전략이 궁금하다면 프로덕션 시크릿 관리와 Vault 로테이션 글을 참고하기 바랍니다.

동시 실행 충돌: 두 사람이 같은 상태 파일을 기반으로 동시에 apply를 실행하면 서로의 변경이 충돌합니다.

이 네 가지 위험 중에서도 실무에서 가장 자주 재현되는 사고는 Git 저장소에 실수로 커밋된 .tfstate 파일을 두 사람이 각자 브랜치에서 수정하고 병합하는 경우입니다. Git은 JSON 파일을 텍스트 기반으로 병합하려고 시도하지만, resources 배열 안의 중첩 구조가 조금만 어긋나도 병합 결과는 문법적으로는 유효한 JSON이지만 의미적으로는 모순된 상태가 됩니다. 이런 상태로 terraform apply를 실행하면 상태 스냅샷의 생성 버전이 현재 버전과 다르다는 버전 불일치 오류나, 심하면 lineage 값이 서로 다른 두 상태가 섞여 상태 데이터의 계보가 일치하지 않는다는 오류로 이어집니다. 이 오류가 뜨면 이미 상태 파일은 신뢰할 수 없는 상태이므로, 복구는 뒤에서 다룰 버전 관리 롤백 절차를 그대로 따라야 합니다.


3. S3 + DynamoDB Remote Backend 설정

AWS 환경에서 가장 많이 사용되는 Remote Backend 조합은 S3와 DynamoDB입니다. 공식 S3 Backend 문서에 두 서비스의 필수 권한과 권장 설정이 정리되어 있습니다.

terraform {
  backend "s3" {
    bucket         = "my-company-terraform-state"
    key            = "prod/eks-cluster/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    kms_key_id     = "arn:aws:kms:ap-northeast-2:123456789012:key/mrk-abc1234"

    dynamodb_table = "terraform-state-lock"
  }
}

S3 버킷은 반드시 버전 관리(Versioning)를 활성화해야 합니다. 상태 파일이 의도치 않게 손상되거나 잘못된 apply가 적용됐을 때 이전 버전으로 복구하는 유일한 방법입니다.

DynamoDB 테이블은 파티션 키를 LockID(String 타입)로 생성합니다. 상태 잠금 중 DynamoDB 쓰로틀링이 발생하면 apply가 실패하므로 온디맨드 과금 모드를 권장합니다.

2026년 현재는 이 구성에 중요한 변화가 하나 더 있습니다. Terraform 1.10부터 S3 백엔드가 DynamoDB 없이도 자체적으로 락을 거는 네이티브 State Locking을 실험적으로 지원하기 시작했고, 1.11부터는 정식(GA) 기능으로 승격됐습니다. use_lockfile = true 옵션 하나만 추가하면 Terraform이 상태 파일 옆에 잠금용 오브젝트를 S3의 조건부 쓰기(Conditional Write) 방식으로 생성해 잠금을 구현합니다. 신규로 백엔드를 구성한다면 DynamoDB 테이블 없이 다음과 같이 작성해도 충분합니다.

terraform {
  backend "s3" {
    bucket       = "my-company-terraform-state"
    key          = "prod/eks-cluster/terraform.tfstate"
    region       = "ap-northeast-2"
    encrypt      = true
    kms_key_id   = "arn:aws:kms:ap-northeast-2:123456789012:key/mrk-abc1234"
    use_lockfile = true
  }
}

기존에 DynamoDB 기반 잠금을 운영 중이라면 하루아침에 전환하기보다 dynamodb_tableuse_lockfile을 동시에 선언해 두 방식을 병행 검증한 뒤, 문제가 없다는 확신이 서면 dynamodb_table 항목을 제거하는 단계적 전환을 권장합니다. HashiCorp는 향후 버전에서 dynamodb_table 인자를 완전히 제거할 수 있다고 안내하고 있으므로, 새 프로젝트라면 처음부터 네이티브 락을 채택하는 편이 유지보수 부담을 줄입니다.

KMS 키를 사용할지 S3 기본 암호화(SSE-S3)로 충분할지도 팀마다 갈리는 지점입니다. 금융이나 의료처럼 규제 준수가 필요한 조직이라면 고객관리형 KMS 키로 CloudTrail에 접근 감사 로그를 남기고 키 정책으로 접근을 통제하는 편이 안전하고, 그 정도의 규제가 없는 내부 도구성 인프라라면 SSE-S3만으로도 실용적인 선택입니다. 버킷 정책은 반드시 특정 IAM 역할에서만 s3:GetObject, s3:PutObject를 허용하고, 퍼블릭 액세스 차단(Block Public Access)을 계정 전체에 강제하는 것이 최소 기준입니다.


4. State Locking 원리: 동시 수정 충돌 방지

State Locking은 terraform apply 또는 terraform plan이 실행되는 동안 다른 프로세스가 같은 상태 파일에 쓰지 못하도록 잠금을 거는 메커니즘입니다.

잠금 흐름: terraform apply가 시작되면 DynamoDB에 LockID가 상태 파일 경로인 항목을 조건부로 삽입합니다. 항목이 이미 존재하면 삽입이 실패하고 Terraform은 오류를 반환합니다. 성공하면 잠금 정보가 기록되고 apply가 진행됩니다.

잠금이 해제되지 않는 상황도 발생합니다. 프로세스가 비정상 종료되거나 네트워크가 끊기면 DynamoDB의 Lock 항목이 남습니다. 이 경우 terraform force-unlock <lock-id>로 수동 해제할 수 있습니다.

CI/CD 파이프라인에서도 Locking이 작동합니다. GitHub Actions나 GitLab CI에서 terraform apply를 실행하는 잡이 여러 개 동시에 트리거되면, 첫 번째 잡만 잠금을 획득하고 나머지는 대기하거나 실패합니다.

실제로 이 잠금이 말썽을 일으키는 경우는 생각보다 흔합니다. 우리 팀에서는 GitHub Actions 러너가 스팟 인스턴스 회수로 갑자기 종료되면서 terraform apply 도중 잠금 해제 신호를 보내지 못한 채 잡이 사라진 적이 있습니다. 이후 동일한 파이프라인이 재실행될 때마다 상태 잠금을 획득하지 못했다는 오류 메시지와 함께, 잠금 정보에는 이미 사라진 이전 잡의 Lock ID가 그대로 남아 있었습니다. 이럴 때는 절대 무작정 force-unlock을 실행하면 안 됩니다. 가장 먼저 확인해야 할 것은 그 Lock ID를 가진 프로세스가 정말로 죽었는지 여부입니다. DynamoDB 콘솔에서 해당 항목의 Info 필드에 담긴 사용자·호스트명과 잠금 생성 시각을 확인하고, 그 시각 이후로 해당 파이프라인이나 담당자가 실제로 작업 중인지 Slack이나 CI 로그로 교차 검증한 뒤에만 terraform force-unlock <lock-id>를 실행해야 합니다. 검증 없이 강제 해제했다가 실제로 동시에 실행 중이던 apply와 충돌해 상태 파일이 이중으로 쓰이는 사고를 방지하기 위한 최소한의 절차입니다.


5. Workspace vs 디렉터리 분리 전략

실무에서는 Workspace가 아닌 디렉터리 분리를 권장합니다.

infra/
├── modules/
│   ├── eks/
│   ├── rds/
│   └── vpc/
├── environments/
│   ├── dev/
│   │   ├── backend.tf
│   │   ├── main.tf
│   │   └── variables.tf
│   ├── staging/
│   └── prod/

이 구조에서 prod/ 디렉터리를 작업하려면 명시적으로 cd environments/prod를 실행해야 합니다. 실수할 여지가 줄어들고, 환경별 IAM 역할과 AWS 프로필을 다르게 가져갈 수 있습니다.

기준Workspace디렉터리 분리
실수로 잘못된 환경 조작발생 가능낮음
환경별 독립 설정변수 파일로 분리각 디렉터리가 독립
코드 재사용동일 코드 그대로모듈로 공유
환경별 접근 권한 분리어려움IAM 역할 분리 가능
대규모 팀 운영권장하지 않음권장

이 구조를 수동으로 유지하다 보면 dev, staging, prod 세 디렉터리에 사실상 동일한 backend.tfprovider.tf가 반복되는 문제가 생깁니다. 이 중복을 줄이기 위해 많은 팀이 Terragrunt를 도입해 설정 파일 하나로 백엔드 설정과 모듈 버전을 상속 구조로 관리합니다. 다만 Terragrunt는 학습 곡선과 추가 도구 의존성이라는 비용을 수반하므로, 환경 수가 서너 개를 넘지 않고 팀 규모가 작다면 순수 Terraform 디렉터리 분리만으로도 충분히 관리 가능하다는 점을 함께 고려해야 합니다.


6. 로컬 Backend에서 Remote Backend로 안전하게 마이그레이션하기

이미 로컬 상태로 운영 중인 프로젝트를 Remote Backend로 옮겨야 하는 상황은 신규 프로젝트보다 오히려 더 흔합니다. 팀 초기에는 인원이 한두 명뿐이라 로컬 상태로 시작했다가, 팀이 커지면서 뒤늦게 전환하는 경우가 대부분입니다. 마이그레이션 자체는 Terraform이 제공하는 절차를 따르면 위험도가 낮지만, 순서를 지키지 않으면 상태 유실로 이어질 수 있습니다.

# 1. 마이그레이션 전 반드시 로컬 상태를 백업합니다
cp terraform.tfstate terraform.tfstate.backup-$(date +%Y%m%d)

# 2. backend 블록을 S3로 변경한 뒤 init을 재실행합니다
terraform init -migrate-state

# 3. Terraform이 대화형으로 기존 상태를 새 backend로 복사할지 물어보면 yes로 응답합니다

# 4. 마이그레이션 후 반드시 plan으로 diff가 없는지 확인합니다
terraform plan

이 과정에서 실무자가 가장 자주 놓치는 지점은 팀 전체가 동시에 backend 블록 변경 커밋을 pull하지 않은 상태에서 작업을 이어가는 경우입니다. 한 사람만 마이그레이션을 완료하고 나머지 팀원이 여전히 이전 backend 설정으로 apply를 실행하면, 신·구 두 개의 상태 파일이 서로 다른 곳에서 독립적으로 갱신되는 최악의 시나리오가 벌어집니다. 마이그레이션은 반드시 팀 전체에 공지하고, 짧은 시간 동안 모든 apply 파이프라인을 일시 중지한 뒤 진행해야 합니다.

State 파일 하나에 리소스가 너무 많이 몰려 분리가 필요한 경우에는 terraform state mv로 리소스를 다른 state 파일로 옮기거나, removed 블록과 import 블록 조합을 사용합니다. HashiCorp 공식 리팩터링 가이드는 서로 다른 state 파일 간 리소스 이동에는 terraform state mv보다 removed + import 블록 조합을 권장하는데, 이 방식은 변경 이력이 코드에 그대로 남아 리뷰가 가능하기 때문입니다. 같은 state 안에서 이름만 바꾸거나 모듈 구조를 리팩터링하는 경우에는 moved 블록이 더 안전합니다. 코드에 다음과 같이 선언하면 리소스를 파괴·재생성하지 않고 상태만 갱신합니다.

moved {
  from = aws_security_group.old_sg
  to   = aws_security_group.new_sg
}

moved 블록의 장점은 terraform state mv와 달리 코드에 남아 CI 파이프라인에서 자동으로 검증되고, PR 리뷰 대상이 된다는 점입니다. 공유 모듈을 배포하는 경우 이 블록을 삭제하지 않고 누적해서 남겨 두는 것이 좋습니다. 이후 다른 사용자가 구버전에서 업그레이드하더라도 이전 리소스 주소를 자동으로 인식할 수 있기 때문입니다.


본문 중간 핵심 흐름을 시각화한 DevOps 일러스트

7. terraform import: 기존 리소스 온보딩

# 기존 S3 버킷을 Terraform 상태로 가져오기
terraform import aws_s3_bucket.terraform_state my-company-terraform-state

# RDS 인스턴스 import
terraform import aws_db_instance.primary mydb-prod-instance

# VPC import
terraform import aws_vpc.main vpc-0a1b2c3d4e5f67890

# import 후 반드시 plan으로 상태와 코드의 일치 여부를 확인합니다
terraform plan

Terraform 1.5부터는 import 블록을 코드 안에 선언하는 방식이 추가됐습니다. terraform plan이 import를 포함한 실행 계획을 미리 보여주므로 검토 후 apply를 결정할 수 있습니다. 이 방식은 대규모 클라우드 자원을 온보딩할 때 특히 유용합니다. 온프레미스에서 관리하다가 뒤늦게 코드화하는 경우, 수십~수백 개의 리소스를 하나씩 import 명령으로 실행하는 대신 import 블록을 여러 개 선언해 plan 한 번으로 일괄 검토할 수 있습니다.


8. State 수술: mv, rm, taint, replace

상태 파일을 직접 조작해야 하는 상황은 운영 중에 반드시 발생합니다. 모든 State 조작은 반드시 백업 후 실행해야 합니다.

# 리소스 이름 변경: 모듈 리팩터링 시 자주 사용
terraform state mv \
  'aws_security_group.old_sg' \
  'aws_security_group.new_sg'

# 모듈로 이동하는 경우
terraform state mv \
  'aws_iam_role.node_role' \
  'module.eks_cluster.aws_iam_role.node_role'

# State에서 리소스 제거 (실제 삭제하지 않음)
terraform state rm 'aws_instance.legacy_server'

# 리소스 강제 교체 (Terraform 1.2+ replace 옵션 권장)
terraform apply -replace='aws_instance.app_server'

# 모든 state 목록 확인
terraform state list

# 특정 리소스의 상세 상태 확인
terraform state show 'aws_db_instance.primary'

Terraform 0.x 시절 자주 쓰던 terraform taint는 1.2 버전부터 -replace 플래그로 대체됐습니다.

State 파일이 실제로 꼬였을 때의 복구 절차

이론적으로 위 명령어들을 알고 있어도, 막상 terraform plan 결과에 수십 개의 리소스가 삭제 후 재생성으로 표시되는 상황을 마주치면 당황하기 쉽습니다. 우리 팀이 겪은 사고 하나를 예로 들면, RDS 파라미터 그룹을 모듈로 리팩터링하는 과정에서 terraform state mv 명령의 리소스 주소를 오타 낸 채로 실행했고, 그 결과 기존 RDS 인스턴스가 상태에서 완전히 유실된 채 신규 리소스로 인식되는 사고가 발생했습니다. terraform plan은 즉시 프로덕션 RDS 인스턴스를 삭제하고 새로 생성하겠다는 계획을 출력했습니다.

이때 복구 절차는 다음과 같았습니다.

  1. 즉시 apply를 중단하고 S3 버킷의 버전 관리 탭에서 사고 발생 직전 시점의 상태 파일 버전을 확인합니다. S3 Versioning이 활성화되어 있었기 때문에 몇 분 전 상태로 롤백할 수 있는 객체가 남아 있었습니다.
  2. 이전 버전의 상태 파일을 별도 경로로 내려받아 terraform state list로 리소스 목록을 비교합니다. 사고 이후 상태와 사고 이전 상태에서 어떤 리소스 주소가 사라졌는지 diff로 확인했습니다.
  3. terraform state push로 이전 버전을 복구하기 전에, 반드시 정상적인 잠금 하에 실행하고, 팀 전체에 복구 작업 중임을 공지해 다른 apply가 끼어들지 않도록 합니다.
  4. 복구 후 terraform plan을 실행해 diff가 0인지 확인하고, 원래 의도였던 모듈 리팩터링은 moved 블록으로 다시 시도합니다.

이 경험 이후 우리 팀은 terraform state mvstate rm처럼 되돌리기 어려운 명령을 실행하기 전에 terraform state pull로 로컬 스냅샷을 먼저 남기는 것을 팀 규칙으로 못 박았습니다. S3 버전 관리가 있다고 해도, 로컬 스냅샷이 있으면 복구 시간을 수 분에서 수 초로 줄일 수 있기 때문입니다.


9. Drift Detection과 자동 알림

Drift는 Terraform이 관리하는 리소스가 상태 파일과 실제 클라우드 인프라 사이에서 의도치 않게 달라지는 현상입니다.

name: Terraform Drift Detection

on:
  schedule:
    - cron: '0 0 * * 1-5'
  workflow_dispatch:

jobs:
  drift-detect:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      contents: read

    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-terraform-ro
          aws-region: ap-northeast-2

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform Plan (Drift Check)
        working-directory: environments/prod
        run: |
          terraform init -input=false
          terraform plan -detailed-exitcode -input=false
        continue-on-error: true
        id: plan

      - name: Notify Slack on Drift
        if: steps.plan.outputs.exit_code == '2'
        run: |
          curl -X POST -H 'Content-type: application/json' \
            --data '{"text":":warning: Terraform Drift 감지"}' \
            ${{ secrets.SLACK_WEBHOOK }}

terraform plan의 종료 코드: 0은 변경사항 없음, 1은 오류, 2는 변경사항 있음(Drift 포함)을 의미합니다.

Drift가 실제로 발생하는 원인은 크게 세 가지로 나뉩니다. 콘솔에서 직접 리소스를 수정하는 ClickOps, AWS가 자동 패치나 백업 스냅샷처럼 스스로 갱신하는 속성, 그리고 오토스케일링 그룹의 인스턴스 수처럼 Terraform이 애초에 관리하지 않기로 한 필드입니다. 마지막 경우는 실제 drift가 아니라 lifecycle { ignore_changes = [...] }로 걸러야 할 대상이므로, Drift Detection 파이프라인에서 반복적으로 같은 리소스가 감지된다면 그 필드가 정말 Terraform이 소유해야 할 속성인지부터 재검토해야 합니다.


10. HCP Terraform vs OpenTofu 비교

2026년 현재 Terraform 생태계는 두 개의 주요 경로로 나뉩니다. HashiCorp가 2024년 4월 "Terraform Cloud"를 "HCP Terraform"으로 리브랜딩한 HCP Terraform과, 커뮤니티 주도로 갈라져 나온 OpenTofu입니다. 2025년 2월 IBM이 HashiCorp를 인수한 이후에도 제품명과 기능 자체는 유지되고 있지만, 라이선스 정책에 대한 커뮤니티의 우려는 여전히 OpenTofu 채택을 뒷받침하는 배경으로 작용하고 있습니다.

기준HCP TerraformOpenTofu
라이선스BSL 1.1MPL 2.0
State 관리내장자체 구성
Policy as CodeSentinelOPA 연동
커뮤니티HashiCorp(IBM 산하)CNCF
HCL 호환성기준Terraform 1.5+ 호환

두 도구 모두 .tf 코드는 거의 동일하게 동작합니다. 마이그레이션 비용보다 라이선스 정책과 운영 복잡도가 선택 기준이 됩니다. 관리형 플랫폼의 워크스페이스 관리, 정책 엔진, 감사 로그가 필요한 조직은 HCP Terraform 쪽이 초기 구축 비용을 아낄 수 있고, 라이선스 리스크를 최소화하면서 셀프 호스팅 백엔드를 직접 운영할 여력이 있는 조직은 OpenTofu가 대안이 됩니다.


11. 팀 협업 베스트 프랙티스

코드 리뷰에 Plan 출력을 포함합니다. Pull Request가 머지되기 전에 terraform plan 결과가 PR 코멘트에 자동으로 첨부되도록 CI를 구성합니다.

Apply는 CI/CD에서만 실행합니다. 개발자 로컬에서 terraform apply를 직접 실행하는 것을 팀 규칙으로 금지합니다.

IAM 최소 권한을 역할별로 분리합니다. Plan 역할은 읽기 권한만 부여하고, Apply 역할은 별도 승인 단계를 거쳐 사용됩니다.

모듈 버전을 고정합니다. source = "terraform-aws-modules/eks/aws" 처럼 버전을 생략하면 모듈 업데이트가 예고 없이 적용될 수 있습니다.

상태 파일을 직접 편집하지 않습니다. 가능하면 terraform state mv, terraform state rm, terraform import 명령으로 처리합니다.

실무에서는 이 원칙들을 문서로만 정리해 두어도 지켜지지 않는 경우가 많습니다. 예를 들어 두 개의 Pull Request가 동시에 열려 있고, 각각의 PR에 대한 CI가 서로 다른 시점에 terraform plan을 실행해 코멘트를 남겼다고 가정해 봅시다. 먼저 열린 PR이 머지되어 실제 인프라가 바뀌면, 나중에 열린 PR의 plan 결과는 이미 낡은 스냅샷을 기준으로 계산된 것이므로 신뢰할 수 없습니다. 이 문제를 방지하려면 머지 큐(Merge Queue)를 도입하거나, 최소한 머지 직전에 terraform plan을 재실행하도록 브랜치 보호 규칙에 강제해야 합니다. GitHub의 Merge Queue 기능이나 Atlantis 같은 도구의 PR 잠금 기능은 이런 낡은 plan 문제를 자동으로 방지해 줍니다.


12. 실무자가 자주 묻는 질문

Q. State 파일 크기가 계속 커지는데, 언제 분리해야 하나요?
정해진 리소스 개수 기준은 없지만, 경험적으로 terraform plan 실행 시간이 30초를 넘어가거나 리소스가 150~200개를 넘어서면 분리를 검토할 시점입니다. 서비스 도메인(네트워크, 컴퓨팅, 데이터 계층)이나 변경 빈도(자주 바뀌는 애플리케이션 리소스 vs 거의 바뀌지 않는 VPC)를 기준으로 쪼개는 것이 실무적으로 가장 무리가 없습니다.

Q. terraform state rm으로 리소스를 지웠는데 실제 인프라도 삭제되나요?
아닙니다. terraform state rm은 Terraform의 관리 대상에서만 제외할 뿐 실제 클라우드 리소스는 그대로 남습니다. 다만 이후 그 리소스를 참조하는 코드가 남아 있다면 다음 apply에서 신규 리소스로 다시 생성하려 시도할 수 있으므로, 코드에서도 함께 제거하거나 import 블록으로 다른 state에 재등록해야 합니다.

Q. 새 팀원이 합류하면 Remote Backend 권한을 어떻게 부여하나요?
IAM 사용자에게 직접 권한을 주지 않고, SSO로 연동된 권한 세트나 OIDC 기반 임시 자격 증명을 통해 역할을 위임하는 방식을 권장합니다. Plan 전용 읽기 역할과 Apply용 쓰기 역할을 분리해 두면, 신규 팀원에게는 우선 Plan 역할만 부여하고 온보딩이 끝난 뒤 Apply 권한을 확장하는 점진적 접근이 가능합니다.

Q. CI에서 매번 terraform init을 실행하는데 속도가 너무 느립니다.
프로바이더와 모듈 캐시를 CI 캐시 스토리지에 저장해 재사용하면 상당한 개선이 있습니다. GitHub Actions라면 actions/cache.terraform 디렉터리와 플러그인 캐시 경로를 캐싱하는 것만으로 init 시간을 수십 초 단위로 줄일 수 있습니다.


핵심 요약

Terraform 상태 파일 운영은 단순한 설정 작업이 아닙니다.

  • Remote Backend 구성 여부: terraform.tfstate가 로컬 또는 Git 저장소에 있다면 즉시 S3로 이전한다.
  • State Locking 활성화 여부: DynamoDB 또는 S3 네이티브 락(use_lockfile)으로 잠금이 구성되어 있는지 확인한다.
  • 환경별 상태 분리 여부: dev, staging, prod 상태 파일이 별도 경로에 있는지 확인한다.
  • Drift Detection 자동화 여부: 정기 스케줄로 terraform plan이 실행되는 파이프라인이 있는지 확인한다.
  • Apply 권한 분리 여부: CI/CD 외의 경로로 terraform apply가 실행 가능한지 점검한다.
  • State 백업 습관화 여부: state mv, state rm처럼 되돌리기 어려운 명령 실행 전 terraform state pull로 로컬 스냅샷을 남기는 습관이 팀에 자리잡았는지 점검한다.