본문으로 건너뛰기
Lab 06: Remote State 설정

Lab 06: Remote State 설정

Lab 06

S3 + DynamoDB로 Remote Backend를 구성합니다. 로컬 terraform.tfstate 파일 대신 원격 저장소를 사용해 팀 협업의 기반을 마련하고, State Locking으로 동시 실행 충돌을 방지합니다.


왜 Remote State가 필요한가

    flowchart LR
    subgraph local["❌ 로컬 State (Lab 01~05)"]
        A1["개발자 A\nterraform.tfstate"] 
        A2["개발자 B\nterraform.tfstate"]
        A1 -. "서로 다른 State\n충돌 발생" .- A2
    end

    subgraph remote["✅ Remote State (Lab 06)"]
        B1["개발자 A"]
        B2["개발자 B"]
        S3["S3 버킷\n단일 State 저장소"]
        DDB["DynamoDB\nState Lock"]
        B1 -- "apply 시도" --> DDB
        B2 -- "apply 시도" --> DDB
        DDB -- "잠금 획득 후\nState 접근" --> S3
    end
  
로컬 StateRemote State
State 위치작업자 PCS3 버킷 (공유)
동시 실행충돌 위험DynamoDB Lock으로 방지
State 분실PC 손상 시 유실S3 버전 관리로 복구 가능
팀 공유수동 복사 필요자동 공유
암호화없음S3 SSE 암호화

전체 디렉터리 구조

lab06-remote-state/
├── bootstrap/          ← 1단계: S3·DynamoDB 생성 (로컬 State)
│   ├── versions.tf
│   ├── providers.tf
│   ├── main.tf
│   └── outputs.tf
└── main-app/           ← 2단계: 실제 앱 코드 (Remote State 사용)
    ├── versions.tf
    ├── providers.tf
    ├── backend.tf      ← S3 backend 설정
    ├── main.tf
    └── outputs.tf
닭이 먼저냐 달걀이 먼저냐: Remote State를 저장할 S3 버킷 자체는 어디서 만드나요? bootstrap/ 디렉터리에서 로컬 State로 먼저 S3와 DynamoDB를 만듭니다. 이 bootstrap은 한 번만 실행합니다.

1단계: Bootstrap — S3·DynamoDB 생성

bootstrap/versions.tf

terraform {
  required_version = ">= 1.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.0"
    }
  }
}

bootstrap/providers.tf

provider "aws" {
  region = "ap-northeast-2"
}

bootstrap/main.tf

# 버킷 이름 전역 유일성 보장
resource "random_string" "suffix" {
  length  = 8
  special = false
  upper   = false
}

# State 저장용 S3 버킷
resource "aws_s3_bucket" "terraform_state" {
  bucket = "terraform-state-${random_string.suffix.result}"

  tags = {
    Name      = "terraform-remote-state"
    ManagedBy = "terraform"
  }
}

# 버전 관리 — 실수로 State를 덮어써도 복구 가능
resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

# 서버 측 암호화
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# 퍼블릭 액세스 차단 — State에는 민감 정보가 포함될 수 있음
resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

# State Lock용 DynamoDB 테이블
resource "aws_dynamodb_table" "terraform_lock" {
  name         = "terraform-state-lock"
  billing_mode = "PAY_PER_REQUEST"   # 온디맨드 과금 (실습 비용 최소화)
  hash_key     = "LockID"            # Terraform이 요구하는 고정 키 이름

  attribute {
    name = "LockID"
    type = "S"
  }

  tags = {
    Name      = "terraform-state-lock"
    ManagedBy = "terraform"
  }
}

bootstrap/outputs.tf

output "state_bucket_name" {
  description = "State 저장용 S3 버킷 이름 — backend.tf에 사용"
  value       = aws_s3_bucket.terraform_state.bucket
}

output "dynamodb_table_name" {
  description = "Lock용 DynamoDB 테이블 이름 — backend.tf에 사용"
  value       = aws_dynamodb_table.terraform_lock.name
}

2단계: main-app — Remote State 사용

main-app/backend.tf

terraform {
  backend "s3" {
    bucket         = "terraform-state-xxxxxxxx"   # bootstrap output 값으로 교체
    key            = "lab06/main-app/terraform.tfstate"
    region         = "ap-northeast-2"
    dynamodb_table = "terraform-state-lock"
    encrypt        = true
  }
}
backend 블록에는 변수를 쓸 수 없습니다. var.bucket_name처럼 변수 참조가 불가능합니다. bootstrap에서 출력된 버킷 이름을 직접 복사해서 붙여넣어야 합니다. 또는 terraform init -backend-config 플래그를 사용합니다.

main-app/versions.tf

terraform {
  required_version = ">= 1.0.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

main-app/providers.tf

provider "aws" {
  region = "ap-northeast-2"
}

main-app/main.tf

# Remote State 테스트용 S3 버킷 (간단한 리소스)
resource "aws_s3_bucket" "app" {
  bucket = "lab06-app-bucket-${formatdate("YYYYMMDDhhmmss", timestamp())}"

  tags = {
    Name      = "lab06-app"
    ManagedBy = "terraform"
  }
}

main-app/outputs.tf

output "app_bucket_name" {
  value = aws_s3_bucket.app.bucket
}

실행 절차

Bootstrap — S3·DynamoDB 생성

cd lab06-remote-state/bootstrap

terraform init
terraform apply -auto-approve

출력에서 버킷 이름을 복사해 둡니다:

Outputs:
dynamodb_table_name = "terraform-state-lock"
state_bucket_name   = "terraform-state-a1b2c3d4"

backend.tf에 버킷 이름 입력

main-app/backend.tfbucket 값을 위 출력값으로 교체합니다:

backend "s3" {
  bucket = "terraform-state-a1b2c3d4"   # 실제 출력값으로 교체
  ...
}

main-app 초기화 — Remote Backend 연결

cd lab06-remote-state/main-app

terraform init

Remote backend에 연결됐다는 메시지가 출력됩니다:

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

배포 — State가 S3에 저장되는지 확인

terraform apply -auto-approve

완료 후 AWS 콘솔 또는 CLI로 S3에 State가 저장됐는지 확인합니다:

aws s3 ls s3://terraform-state-a1b2c3d4/lab06/main-app/
# 2026-06-30 ... terraform.tfstate

State Locking 체험 — 두 터미널 동시 실행

터미널 1 에서 apply를 실행합니다:

# 터미널 1
terraform apply -auto-approve

apply가 실행 중인 상태에서 터미널 2 에서도 apply를 시도합니다:

# 터미널 2 (동시에 실행)
terraform apply -auto-approve

터미널 2에서 Lock 오류가 발생합니다:

Error: Error acquiring the state lock

  lock Info:
    ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
    Path: lab06/main-app/terraform.tfstate
    Operation: OperationTypeApply
    Who: user@hostname
    Created: 2026-06-30 ...

  Terraform acquires a state lock to protect the state from being written
  by multiple users at the same time.

DynamoDB 콘솔에서 terraform-state-lock 테이블의 항목을 보면 LockID 레코드가 생성된 것을 확인할 수 있습니다.

정리 — 역순 삭제

# main-app 리소스 삭제
cd lab06-remote-state/main-app
terraform destroy -auto-approve

# bootstrap 리소스 삭제 (S3·DynamoDB)
cd lab06-remote-state/bootstrap
terraform destroy -auto-approve
bootstrap의 S3 버킷을 삭제하기 전에 버킷이 비어 있어야 합니다. State 파일이 남아 있으면 destroy가 실패합니다. aws s3 rm s3://버킷이름 --recursive로 먼저 비워야 합니다.

주의사항

backend 블록에 변수 불가: backend "s3" 안에서는 var.*, local.*, data.* 참조가 모두 금지됩니다. 버킷 이름 등 backend 설정값은 리터럴 문자열로 직접 입력하거나 terraform init -backend-config=backend.hcl 방식을 사용합니다.

-backend-config 활용: 버킷 이름을 코드에 하드코딩하기 싫다면 별도 파일로 분리합니다.

# backend.hcl (Git에서 제외하거나 공유 가능)
bucket         = "terraform-state-a1b2c3d4"
dynamodb_table = "terraform-state-lock"
region         = "ap-northeast-2"
encrypt        = true
terraform init -backend-config=backend.hcl

State Key 설계: key는 S3 내 파일 경로입니다. 프로젝트·환경별로 겹치지 않게 설계합니다.

# 권장 Key 패턴
{project}/{environment}/{component}/terraform.tfstate

# 예시
myapp/dev/network/terraform.tfstate
myapp/dev/compute/terraform.tfstate
myapp/prod/network/terraform.tfstate

핵심 학습 포인트

S3 = 저장, DynamoDB = 잠금: 두 서비스는 역할이 다릅니다. S3는 State 파일을 저장하고, DynamoDB는 동시에 두 명이 State를 수정하지 못하도록 잠금(Lock)을 관리합니다. 둘 다 있어야 완전한 Remote Backend입니다.

버킷 보안 3원칙: ① 버전 관리 활성화(실수 복구), ② 서버 측 암호화(민감 정보 보호), ③ 퍼블릭 액세스 차단(외부 노출 방지). State 파일에는 DB 비밀번호 등이 평문으로 들어갈 수 있습니다.

bootstrap은 한 번만: S3·DynamoDB 자체는 로컬 State로 관리합니다. 이 bootstrap 코드는 팀 공유 저장소에 보관하되, destroy는 신중하게 합니다. State 저장소가 사라지면 전체 인프라 State를 잃습니다.

State Key = 경계: 같은 S3 버킷이더라도 key가 다르면 완전히 독립된 State입니다. 하나의 버킷으로 여러 프로젝트·환경의 State를 관리할 수 있습니다.


실무에서의 교훈

이 실습은 “테라폼에서 리소스의 이름(인자)이 바뀌면 어떻게 동작하는가"를 보여주는 아주 좋은 예시입니다.

리소스 교체(Replace): S3 버킷처럼 이름이 곧 리소스 식별자가 되는 경우, 이름을 바꾸면 수정이 아니라 ‘삭제 후 재생성’이 일어납니다. main-app/main.tfaws_s3_bucket.app이 바로 그 예시입니다 — bucket 인자 값이 바뀔 때마다 terraform plan에는 -/+ destroy and re-create가 표시됩니다. 실무 운영 DB나 서버에서 이런 일이 벌어지면 대형 사고겠죠? 그래서 실무에서는 리소스 이름을 함부로 바꾸지 않습니다.
timestamp()의 위험성: 실제 운영 환경에서는 절대 timestamp() 같은 함수를 버킷 이름에 사용하지 않습니다. apply를 실행할 때마다 값이 바뀌어 매번 새 이름이 계산되고, 그 결과 terraform plan이 매번 리소스 교체를 제안합니다. 이름이 매번 바뀌면 인프라를 예측할 수 없기 때문이죠. 이 실습에서는 학습 목적으로만 사용했으며, 실제 프로젝트에서는 random_string/random_id(bootstrap에서 사용한 방식처럼 한 번만 생성되고 고정됨)나 고정된 명명 규칙을 사용해야 합니다.

→ 다음 실습: Lab 07 기존 리소스 Import — 콘솔에서 수동 생성한 리소스를 Terraform으로 편입