본문 바로가기

DevOps

Terraform S3 Backend와 State Lock 구성하기: 팀 협업을 위한 원격 상태 관리

반응형

팀원 3명이 같은 인프라를 Terraform으로 관리할 때, Local State로는 충돌을 피할 수 없습니다. S3 Backend와 DynamoDB Lock을 구성하면 이 문제를 구조적으로 해결할 수 있습니다.

핵심 요약

  • S3 Backend는 Terraform State를 팀 전체가 공유할 수 있는 원격 저장소에 보관합니다.
  • DynamoDB를 사용한 State Locking은 동시 작업으로 인한 State 충돌을 방지합니다.
  • Backend 리소스(S3 버킷, DynamoDB 테이블)는 Terraform이 아닌 별도 방법으로 먼저 생성하는 것이 일반적입니다.
  • State 파일에는 민감 정보가 포함될 수 있으므로 암호화와 접근 제어가 필수입니다.
  • 환경별(dev/staging/prod) State를 분리하면 blast radius를 줄일 수 있습니다.

1. 왜 S3 Backend가 필요한가

팀원 A가 EC2를 추가하고, 팀원 B가 Security Group을 수정하는 상황을 생각해봅니다.

Local State를 사용하면:

  • A의 노트북에 있는 State와 B의 노트북에 있는 State가 서로 다릅니다.
  • A가 만든 리소스를 B의 Terraform은 인식하지 못합니다.
  • 두 사람이 동시에 apply하면 State가 덮어써지면서 리소스 추적이 불가능해집니다.

이 문제를 해결하려면 State를 한 곳에 모아야 합니다. S3 Backend는 State 파일을 S3 버킷에 저장하고, 팀 전체가 같은 State를 참조하도록 만듭니다.

S3 Backend 구조
S3 Backend 구조

2. 전체 구성 흐름

S3 Backend를 구성하는 전체 흐름은 다음과 같습니다.

S3 Backend 구성 흐름
S3 Backend 구성 흐름
단계 작업 도구
1 S3 버킷 생성 AWS CLI 또는 콘솔
2 DynamoDB 테이블 생성 AWS CLI 또는 콘솔
3 Backend 설정 작성 Terraform .tf 파일
4 terraform init 실행 Terraform CLI
5 동작 확인 terraform plan + Lock 테스트

왜 Terraform으로 Backend 리소스를 만들지 않는가? Terraform이 State를 저장할 버킷을 Terraform으로 만들면 순환 의존이 생깁니다. 버킷이 없으면 State를 저장할 수 없고, State가 없으면 버킷을 관리할 수 없습니다. 이 문제를 "chicken-and-egg problem"이라고 부릅니다.

3. S3 버킷 생성

3.1 AWS CLI로 생성

# S3 버킷 생성
aws s3api create-bucket \
  --bucket my-team-terraform-state \
  --region ap-northeast-2 \
  --create-bucket-configuration LocationConstraint=ap-northeast-2

# 버전 관리 활성화 (State 복구를 위해 필수)
aws s3api put-bucket-versioning \
  --bucket my-team-terraform-state \
  --versioning-configuration Status=Enabled

# 서버 측 암호화 설정
aws s3api put-bucket-encryption \
  --bucket my-team-terraform-state \
  --server-side-encryption-configuration '{
    "Rules": [{
      "ApplyServerSideEncryptionByDefault": {
        "SSEAlgorithm": "aws:kms"
      },
      "BucketKeyEnabled": true
    }]
  }'

# 퍼블릭 액세스 차단
aws s3api put-public-access-block \
  --bucket my-team-terraform-state \
  --public-access-block-configuration \
    BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

3.2 각 설정의 이유

설정 이유
Versioning State가 손상되었을 때 이전 버전으로 복구 가능
KMS 암호화 State에 포함된 민감 정보(DB 비밀번호, API 키 등) 보호
BucketKey KMS API 호출 횟수를 줄여 비용 절감
Public Access Block State 파일이 외부에 노출되는 것을 원천 차단
Tip
버킷 이름은 전역적으로 고유해야 합니다. 팀명이나 프로젝트명을 접두사로 사용하면 충돌을 줄일 수 있습니다. 예: mycompany-prod-terraform-state

3.3 Versioning이 중요한 이유

실무에서 State가 손상되는 상황은 생각보다 자주 발생합니다.

  • terraform apply 중 네트워크가 끊겨 State가 절반만 기록된 경우
  • 실수로 terraform state rm을 잘못 실행한 경우
  • CI/CD 파이프라인에서 동시 실행이 발생한 경우

Versioning이 활성화되어 있으면 S3 콘솔에서 이전 버전의 State를 확인하고 복원할 수 있습니다.

# 버킷의 State 파일 버전 목록 확인
aws s3api list-object-versions \
  --bucket my-team-terraform-state \
  --prefix prod/terraform.tfstate

4. State Locking 구성

4.1 두 가지 Locking 방식

Terraform 1.10부터 S3 네이티브 Locking이 실험적으로 도입되었고, Terraform 1.11에서 GA(Generally Available)가 되었습니다. 이에 따라 DynamoDB 기반 Locking은 deprecated 상태입니다.

방식 Terraform 버전 상태 추가 리소스
S3 네이티브 Locking (use_lockfile) 1.10+ (실험), 1.11+ (GA) 권장 없음 (S3만 사용)
DynamoDB Locking (dynamodb_table) 모든 버전 Deprecated (향후 제거 예정) DynamoDB 테이블 필요

Terraform 1.11 이상을 사용한다면 S3 네이티브 Locking을 권장합니다. DynamoDB 테이블이 불필요하므로 관리할 리소스가 줄고, IAM 권한도 단순해집니다.

다만 기존 프로젝트에서 DynamoDB Locking을 사용 중이거나, Terraform 1.10 미만 버전을 사용하는 팀이라면 DynamoDB 방식이 여전히 유효합니다. 아래에서 두 가지 방식을 모두 설명합니다.

4.2 방식 A: S3 네이티브 Locking (Terraform 1.11+, 권장)

S3 네이티브 Locking은 S3의 조건부 쓰기(Conditional Write)를 사용하여 .tflock 파일을 생성하는 방식입니다. 별도 DynamoDB 테이블이 필요 없습니다.

terraform {
  backend "s3" {
    bucket       = "my-team-terraform-state"
    key          = "prod/network/terraform.tfstate"
    region       = "ap-northeast-2"
    encrypt      = true
    use_lockfile = true
  }
}

use_lockfile = true만 추가하면 됩니다. Lock 파일은 State 파일과 같은 경로에 .tflock 확장자로 생성됩니다.

4.3 방식 B: DynamoDB Locking (레거시, Terraform 1.10 미만)

Terraform 1.10 미만 버전을 사용하거나, 기존 DynamoDB 기반 설정을 유지해야 하는 경우입니다.

DynamoDB 테이블 생성 (AWS CLI)

aws dynamodb create-table \
  --table-name terraform-state-lock \
  --attribute-definitions AttributeName=LockID,AttributeType=S \
  --key-schema AttributeName=LockID,KeyType=HASH \
  --billing-mode PAY_PER_REQUEST \
  --region ap-northeast-2

DynamoDB 설정 상세

설정 이유
Partition Key LockID (String) Terraform이 Lock 생성 시 사용하는 키 이름
Billing Mode PAY_PER_REQUEST Lock 요청은 빈도가 낮으므로 온디맨드가 비용 효율적
Provisioned 대안 RCU 5 / WCU 5 팀 규모가 크고 요청이 빈번하면 Provisioned가 저렴할 수 있음

왜 PAY_PER_REQUEST인가? Terraform Lock은 plan이나 apply 실행 시에만 발생합니다. 하루에 수십 회 수준이므로 Provisioned 모드의 최소 비용보다 온디맨드가 저렴한 경우가 많습니다.

Tip
DynamoDB 테이블 하나로 여러 Terraform 프로젝트의 Lock을 관리할 수 있습니다. LockID에 State 파일 경로가 포함되므로 프로젝트 간 충돌이 발생하지 않습니다.

5. Backend 설정 작성

5.1 기본 설정 (S3 네이티브 Locking — 권장)

프로젝트의 backend.tf 파일에 다음을 작성합니다.

terraform {
  backend "s3" {
    bucket       = "my-team-terraform-state"
    key          = "prod/network/terraform.tfstate"
    region       = "ap-northeast-2"
    encrypt      = true
    use_lockfile = true
  }
}

5.2 기본 설정 (DynamoDB Locking — 레거시)

Terraform 1.10 미만 버전을 사용하는 경우:

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

5.3 key 설계 전략

key는 S3 버킷 내에서 State 파일의 경로입니다. 이 경로를 어떻게 설계하느냐에 따라 환경 분리와 blast radius가 결정됩니다.

# 권장 구조: 환경/계층/terraform.tfstate
s3://my-team-terraform-state/
├── prod/
│   ├── network/terraform.tfstate
│   ├── compute/terraform.tfstate
│   └── database/terraform.tfstate
├── staging/
│   ├── network/terraform.tfstate
│   └── compute/terraform.tfstate
└── shared/
    └── iam/terraform.tfstate

설계 기준:

  • 환경별 분리: prod에서 실험하다 staging을 건드리는 사고를 방지합니다.
  • 계층별 분리: network를 수정할 때 database State에 영향을 주지 않습니다.
  • shared 영역: IAM처럼 환경 공통 리소스는 별도로 관리합니다.

5.4 환경별 Backend 분리 방법

환경마다 다른 key를 사용하려면 두 가지 방법이 있습니다.

방법 1: -backend-config 옵션 사용

# backend.tf — key를 비워둠
terraform {
  backend "s3" {
    bucket         = "my-team-terraform-state"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
  }
}
# 환경별로 init 시 key를 지정
terraform init -backend-config="key=prod/network/terraform.tfstate"
terraform init -backend-config="key=staging/network/terraform.tfstate"

방법 2: Workspace 사용

terraform workspace new prod
terraform workspace new staging
terraform workspace select prod

이 경우 State는 env:/prod/terraform.tfstate 경로에 저장됩니다.

어떤 방법을 선택할까?

기준 -backend-config Workspace
환경별 완전 분리 O (버킷/테이블도 분리 가능) △ (같은 버킷 내 경로만 분리)
CI/CD 통합 환경 변수로 key 전달 workspace 명령 추가 필요
환경별 다른 provider 설정 O (디렉토리 분리와 조합) X (같은 코드 공유)
적합 상황 환경 간 차이가 큰 경우 환경 간 코드가 동일한 경우

운영 환경에서는 -backend-config와 디렉토리 분리를 조합하는 방식이 더 명확한 경우가 많습니다. Workspace는 코드가 완전히 동일한 dev/staging에서 유용할 수 있습니다.

6. terraform init 실행

Backend 설정을 작성한 후 terraform init을 실행합니다.

$ terraform init

Initializing the backend...

Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.

Initializing provider plugins...

Terraform has been successfully initialized!

6.1 기존 Local State를 Remote로 마이그레이션

이미 Local State로 리소스를 관리하고 있었다면, Backend 설정 추가 후 init을 실행하면 마이그레이션 여부를 묻습니다.

$ terraform init

Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend
  to the newly configured "s3" backend.

  Enter a value: yes

Successfully configured the backend "s3"!

yes를 입력하면 기존 terraform.tfstate 파일의 내용이 S3로 복사됩니다.

주의
마이그레이션 후 로컬 terraform.tfstate 파일은 삭제하거나 .gitignore에 추가해야 합니다. 두 곳에 State가 존재하면 혼란이 생깁니다.

6.2 init 실패 시 확인 사항

에러 메시지 원인 해결
NoSuchBucket S3 버킷이 존재하지 않음 버킷 생성 확인, 리전 확인
AccessDenied IAM 권한 부족 S3, DynamoDB 접근 권한 확인
ResourceNotFoundException DynamoDB 테이블 없음 테이블 생성 확인, 리전 확인
InvalidClientTokenId AWS 자격 증명 오류 aws configure 또는 환경 변수 확인

7. State Lock 동작 확인

7.1 Lock이 정상 동작하는지 테스트

터미널 두 개를 열고 동시에 terraform plan을 실행합니다.

# 터미널 1
$ terraform plan
Acquiring state lock. This may take a few moments...
# (정상 실행)

# 터미널 2 (동시 실행)
$ terraform plan
Error: Error acquiring the state lock

Lock Info:
  ID:        a1b2c3d4-e5f6-7890-abcd-ef1234567890
  Path:      my-team-terraform-state/prod/network/terraform.tfstate
  Operation: OperationTypePlan
  Who:       engineer-a@hostname
  Created:   2026-05-31 09:00:00 UTC

두 번째 터미널에서 Lock 에러가 발생하면 정상입니다. 첫 번째 작업이 끝나면 Lock이 해제되고 두 번째 작업을 실행할 수 있습니다.

7.2 Lock 강제 해제

비정상 종료로 Lock이 남아 있는 경우에만 사용합니다.

# Lock ID 확인 후 강제 해제
terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890
Security Note
force-unlock은 다른 사용자가 실제로 작업 중인 경우에도 Lock을 해제합니다. 반드시 해당 Lock을 건 사용자에게 확인한 후 사용해야 합니다. 확인 없이 사용하면 State 충돌이 발생할 수 있습니다.

7.3 DynamoDB에서 Lock 상태 직접 확인

aws dynamodb scan \
  --table-name terraform-state-lock \
  --region ap-northeast-2

Lock이 걸려 있으면 LockID와 함께 누가, 언제, 어떤 작업으로 Lock을 걸었는지 확인할 수 있습니다.

8. 보안 고려사항

8.1 IAM 권한 설계

Backend를 사용하려면 Terraform 실행 주체(사용자 또는 CI/CD Role)에 다음 권한이 필요합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": [
        "arn:aws:s3:::my-team-terraform-state",
        "arn:aws:s3:::my-team-terraform-state/*"
      ]
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:DeleteItem"
      ],
      "Resource": "arn:aws:dynamodb:ap-northeast-2:123456789012:table/terraform-state-lock"
    }
  ]
}

8.2 최소 권한 원칙 적용

역할 S3 권한 DynamoDB 권한 이유
인프라 관리자 Get, Put, Delete, List Get, Put, Delete 전체 작업 가능
개발자 (읽기 전용) Get, List Get plan만 실행, apply 불가
CI/CD 파이프라인 Get, Put, Delete, List Get, Put, Delete 자동 배포용

읽기 전용 개발자에게 PutObject를 주지 않으면 terraform apply를 실행해도 State를 갱신할 수 없으므로 실질적으로 apply가 차단됩니다.

8.3 추가 보안 설정

  • S3 버킷 정책: 특정 IAM Role만 접근 가능하도록 제한
  • KMS 키 정책: State 암호화에 사용하는 KMS 키의 접근 권한 분리
  • CloudTrail: S3 버킷에 대한 모든 접근을 로깅하여 감사 추적 가능
  • MFA Delete: 버킷 버전 삭제 시 MFA 인증 요구 (실수로 State 버전 삭제 방지)
Security Note
State 파일에는 데이터베이스 비밀번호, API 키, 인증서 등이 평문으로 저장될 수 있습니다. State 버킷에 대한 접근 권한은 인프라 관리자로 제한하고, 접근 로그를 반드시 활성화해야 합니다.

9. 비용/운영 고려사항

9.1 비용 구조

리소스 비용 항목 예상 비용 (월)
S3 버킷 저장 용량 + 요청 수 $0.01~0.10 (State 파일은 수 KB~MB)
S3 Versioning 버전별 저장 용량 $0.01~0.05 (오래된 버전 정리 시)
DynamoDB (On-Demand) 읽기/쓰기 요청 수 $0.01~0.05 (하루 수십 회 수준)
KMS API 호출 수 $1.00 (키 1개) + 요청당 $0.03/10,000건

대부분의 팀에서 S3 Backend 운영 비용은 월 $2 미만입니다.

9.2 운영 시 주의사항

  • State 파일 크기 관리: 리소스가 수백 개를 넘으면 State 파일이 수 MB까지 커질 수 있습니다. plan 속도가 느려지면 State를 계층별로 분리하는 것을 검토합니다.
  • S3 버전 정리: Versioning을 활성화하면 오래된 버전이 계속 쌓입니다. Lifecycle Rule로 90일 이상 된 비현재 버전을 자동 삭제하면 비용을 줄일 수 있습니다.
  • DynamoDB 테이블 공유: 하나의 DynamoDB 테이블로 여러 프로젝트의 Lock을 관리할 수 있습니다. 테이블을 프로젝트마다 만들 필요는 없습니다.
# S3 Lifecycle Rule 설정 예시 — 비현재 버전 90일 후 삭제
aws s3api put-bucket-lifecycle-configuration \
  --bucket my-team-terraform-state \
  --lifecycle-configuration '{
    "Rules": [{
      "ID": "cleanup-old-versions",
      "Status": "Enabled",
      "NoncurrentVersionExpiration": {
        "NoncurrentDays": 90
      }
    }]
  }'
주의
Lifecycle Rule로 비현재 버전을 삭제하면 해당 시점 이전의 State로는 복구할 수 없습니다. 90일은 대부분의 팀에서 충분한 보존 기간이지만, 컴플라이언스 요구사항이 있다면 보존 기간을 조정해야 합니다.

10. 실무 시나리오: CI/CD 파이프라인 통합

10.1 GitHub Actions에서 S3 Backend 사용

CI/CD 파이프라인에서 Terraform을 실행할 때도 같은 Backend를 사용합니다. 이때 AWS 자격 증명을 안전하게 전달하는 것이 핵심입니다.

# .github/workflows/terraform.yml
name: Terraform Plan

on:
  pull_request:
    paths:
      - 'infra/**'

jobs:
  plan:
    runs-on: ubuntu-latest
    permissions:
      id-token: write   # OIDC 인증용
      contents: read

    steps:
      - uses: actions/checkout@v4

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

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform Init
        run: terraform init
        working-directory: infra/prod/network

      - name: Terraform Plan
        run: terraform plan -no-color
        working-directory: infra/prod/network

10.2 OIDC를 사용하는 이유

GitHub Actions에서 AWS에 접근할 때 Access Key를 Secret에 저장하는 방식보다 OIDC(OpenID Connect)를 사용하는 것이 안전합니다.

방식 장점 단점
Access Key + Secret 설정이 간단 키 유출 위험, 주기적 교체 필요
OIDC 키 없음, 세션 기반 임시 자격 증명 IAM Role 설정 필요

OIDC를 사용하면 장기 자격 증명이 존재하지 않으므로 유출 위험이 구조적으로 제거됩니다.

11. 자주 하는 실수

실수 결과 예방 방법
Backend 리소스를 Terraform으로 관리 순환 의존, State 손실 시 복구 불가 AWS CLI 또는 별도 Bootstrap 프로젝트로 생성
Versioning 미활성화 State 손상 시 복구 불가 버킷 생성 시 반드시 Versioning 활성화
encrypt = true 누락 State 내 민감 정보가 평문 저장 Backend 설정 시 encrypt 필수 포함
환경별 key 미분리 prod와 staging이 같은 State 공유 key 경로에 환경명 포함
force-unlock 남용 다른 사용자 작업 중 State 충돌 Lock 소유자 확인 후에만 사용
.gitignore에 State 미추가 Git에 민감 정보 포함된 State 커밋 *.tfstate, *.tfstate.* 추가
DynamoDB 테이블 리전 불일치 Lock 실패, init 에러 Backend 설정의 region과 테이블 리전 일치 확인
Terraform 1.11+에서 DynamoDB 사용 deprecated 경고 발생 use_lockfile = true로 마이그레이션 검토

12. 정리

  • S3 Backend는 팀 환경에서 Terraform State를 안전하게 공유하기 위한 구조입니다.
  • DynamoDB State Lock은 동시 작업으로 인한 State 충돌을 방지합니다.
  • Backend 리소스(S3 버킷, DynamoDB 테이블)는 Terraform 외부에서 먼저 생성해야 합니다.
  • Versioning, 암호화, Public Access Block은 선택이 아니라 필수입니다.
  • 환경별 key 분리로 blast radius를 줄이고, IAM으로 접근 권한을 제어합니다.
  • CI/CD 통합 시 OIDC 기반 인증을 사용하면 자격 증명 유출 위험을 줄일 수 있습니다.

관련 글

참고 문서

반응형