[Step 1-2] Terraform: 변수/모듈화 & S3 Backend

2025. 8. 30. 00:09·클라우드

 

이번 단계에서는 기본 문법 → 변수 → 모듈화 → 원격 상태 관리(S3 백엔드)까지 한 번에 실습합니다.

 

요점:

  • AMI 하드코딩 금지: 동적 조회 사용
  • Bootstrap/Backend 분리: “치킨-달걀” 문제 해결
  • PowerShell 타겟 파싱 주의: 안전한 명령 패턴 제공
  • 버킷 비우기: 버저닝/삭제마커까지 정리법

0. 왜 S3 백엔드는 바로 terraform init/apply가 안 되나?

백엔드는 terraform init 시점에 먼저 로드됩니다.

하지만 S3 버킷/락 테이블(DynamoDB) 자체를 Terraform으로 만들고 싶다면… 아직 백엔드가 없죠.

  • → 첫 실행만 로컬 상태로 S3/DynamoDB를 만들고
  • → 그다음 상태를 S3로 이관해야 합니다.
  • 이게 흔히 말하는 bootstrap → migrate 흐름이에요.

대안 3가지:

    • 패턴 A (CLI 부트스트랩)
      : AWS CLI 3줄로 S3/DynamoDB를 먼저 만들고, backend.tf를 켠 채 바로 terraform init.
      -> S3/DDB는 Terraform이 아닌 CLI로 생성.
    • 패턴 B (폴더 분리 부트스트랩)
      : bootstrap/(S3/DDB만, backend 없음)에서 Terraform으로 생성 -> main/(backend.tf+본 인프라)에서 terraform init하여 S3 백엔드 사용(필요 시 migrate).
      -> S3/DDB를 Terraform으로 만들되, 폴더를 분리.
    • 패턴 C (단일 폴더 in-place 부트스트랩) ✔️
      같은 폴더에서 `backend.tf` 잠시 비활성화 → `init` → `apply --target`으로 S3/DDB 생성 ->`backend.tf` 복구 → `terraform init -migrate-state` → 본 인프라 `apply`.
      ->S3/DDB를 Terraform으로 만들되, 폴더는 분리 안 함.

아래는 패턴C 입니다.


1. 폴더 구조

앞에서 실습한 도커관련한 파일을 전부 docker-demo로, 앞으로 만들 파일은 terraform-infra-demo에 옮겨줍니다. 이렇게 분리하면 초기 backend 구성(bootstrap)과 실제 인프라 관리 코드를 따로 관리할 수 있어 협업 시 충돌이 줄어듭니다.

CLOUDSTUDY/
 ├── docker-demo/                # Step 1-1 Docker 실습
 │    ├── server.js
 │    ├── Dockerfile
 │    ├── favicon.ico
 │    ├── package.json
 │    └── package-lock.json
 │
 ├── terraform-infra-demo/       # Step 1-2 Terraform 실습
 │    ├── backend_setup.tf       # (bootstrap) S3 버킷 + DynamoDB 생성
 │    ├── backend.tf             # 이후 원격 상태 저장 설정
 │    ├── provider.tf
 │    ├── variables.tf
 │    ├── main.tf                # 실제 리소스 정의 (모듈 호출 포함)
 │    ├── outputs.tf
 │    └── modules/
 │         └── vpc/
 │              ├── main.tf
 │              ├── variables.tf
 │              └── outputs.tf
 │
 └── README.md

2. Provider & 변수

provider.tf

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

provider "aws" {
  region = var.region
  # (택1) profile = "zerry"
  # (택1) 터미널에 환경변수 코드입력 AWS_PROFILE=zerry
}

variables.tf

variable "region" {
  description = "AWS region"
  type        = string
  default     = "ap-northeast-2"
}

variable "project_name" {
  description = "Project name prefix"
  type        = string
  default     = "demo"
}

3. EC2(동적 AMI) + VPC 모듈 호출

main.tf

# ✅ 최신 Amazon Linux 2 AMI 동적 조회 (하드코딩 금지)
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["amzn2-ami-hvm-*-x86_64-gp2"]
  }
}

resource "aws_instance" "demo" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = "t2.micro"
  tags = { Name = "${var.project_name}-ec2" }
}

module "vpc" {
  source       = "./modules/vpc"
  cidr_block   = "10.0.0.0/16"
  project_name = var.project_name
}

modules/vpc/main.tf

resource "aws_vpc" "this" {
  cidr_block = var.cidr_block
  tags = { Name = "${var.project_name}-vpc" }
}

modules/vpc/variables.tf

variable "cidr_block"   { type = string }
variable "project_name" { type = string }

4. S3 Backend 리소스 (bootstrap 단계용)

backend_setup.tf

# S3 버킷 (tfstate 저장)
resource "aws_s3_bucket" "tf_state" {
  bucket = "my-terraform-state-bucket-250829"
  lifecycle { prevent_destroy = true } # 실수 삭제 방지
  tags = { Name = "tf-state-bucket" }
}

# 버전닝(별도 리소스)
resource "aws_s3_bucket_versioning" "tf_state" {
  bucket = aws_s3_bucket.tf_state.id
  versioning_configuration { status = "Enabled" }
}

# DynamoDB 락 테이블 (State Lock)
resource "aws_dynamodb_table" "tf_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"
  attribute { name = "LockID"; type = "S" }
  lifecycle { prevent_destroy = true }
  tags = { Name = "tf-locks" }
}

5. S3 Backend 연결 블록

backend.tf

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

⚠️ 부트스트랩할 때는 임시로 이 파일을 비활성화해야 합니다(이름 변경 또는 전체 주석).

이유: init이 backend 블록을 먼저 읽으므로 “아직 없는 S3에 연결”하려다 실패합니다.


6. 실행 순서 (PowerShell 안전 버전)

A) Bootstrap: 로컬 상태로 S3/DynamoDB 먼저 생성

  • backend.tf 잠시 비활성화 (예: backend.tf.off로 이름 변경) or 주석 
  • 깨끗이 초기화
# 필요시 초기화
Remove-Item -Recurse -Force .terraform, .terraform.lock.hcl -ErrorAction SilentlyContinue
Remove-Item -Force terraform.tfstate, terraform.tfstate.backup -ErrorAction SilentlyContinue

terraform init
  • 타겟 적용 (PowerShell 파싱 이슈 방지용으로 따옴표 사용)
terraform apply -auto-approve `
  "--target=aws_s3_bucket.tf_state" `
  "--target=aws_s3_bucket_versioning.tf_state" `
  "--target=aws_dynamodb_table.tf_locks"

# 한 줄로도 가능:
terraform apply -auto-approve "--target=aws_s3_bucket.tf_state" "--target=aws_s3_bucket_versioning.tf_state" "--target=aws_dynamodb_table.tf_locks"

 

B) Backend 연결 + 상태 이관

  1. backend.tf.off → backend.tf로 복구 or 주석해제
  2. 이관 초기화
terraform init -migrate-state

메시지 나오면 yes.

terraform state list로 리소스 목록이 그대로 보이면 성공.

C) 본 인프라 배포(EC2/VPC 등)

terraform plan
terraform apply -auto-approve

✅ 콘솔에서 생성확인

s3, vpc, dynameDB, ec2 생성확인


7. 자주 막히는 포인트

  • Too many command line arguments / Invalid target "aws_s3_bucket"
    terraform plan -out=tfplan `
      "--target=aws_s3_bucket.tf_state" `
      "--target=aws_s3_bucket_versioning.tf_state" `
      "--target=aws_dynamodb_table.tf_locks"
    terraform apply -auto-approve tfplan
    
    → PowerShell이 줄바꿈/공백을 깨먹은 것. 따옴표로 감싸거나, plan 파일을 먼저 만든 뒤 적용:
  • Backend initialization required
    →
    backend.tf가 살아있는데 S3가 아직 없음.
    → bootstrap 동안엔 backend 비활성화 → 생성 후 init -migrate-state.
  • 리전 불일치
    → provider.aws.region, backend.tf.region, 버킷 실제 리전을 모두 동일하게.
  • State lock
    → 작업 중단/충돌 시 terraform force-unlock <LOCK_ID>.

8. 삭제 방법

1. 업무 리소스(EC2/VPC 등)만 삭제

terraform destroy -auto-approve

백엔드 리소스(S3/DynamoDB)는 prevent_destroy = true 때문에 여기서 삭제되지 않습니다.

 

2. 백엔드(S3/DynamoDB)까지 없애기

왜 바로 안 지워지나?

S3 버킷에 버전닝이 켜져 있으면 aws s3 rm --recursive 는 최신 버전만 지우고, 과거 Versions/삭제마커(DeleteMarkers) 가 남아 있으면 delete-bucket 이 BucketNotEmpty로 실패합니다.

 

2-1) Terraform이 S3를 붙잡지 않도록 백엔드 끊기

terraform init -reconfigure -backend=false
# 또는 backend.tf 파일명을 backend.tf.off 로 잠시 변경

2-2) S3 버킷 “완전 비우기” → 버킷 삭제 (PowerShell 스크립트)

아래 코드를 통째로 복사붙여넣기

# === 설정 ===
$Bucket = "my-terraform-state-bucket-250829"
$Region = "ap-northeast-2"
# (선택) $Env:AWS_PROFILE = "zerry"

# 0) 추천: 테라폼이 S3 백엔드 붙잡지 않도록 잠시 끊기
# terraform init -reconfigure -backend=false
# 또는 backend.tf 파일명을 backend.tf.off 로 변경

Write-Host "Step1: 최신 객체 삭제"
aws s3 rm "s3://$Bucket" --recursive --region $Region | Out-Null

Write-Host "Step2: 모든 버전/삭제마커 반복 삭제 (임시폴더 + UTF-8 no-BOM + file://C:\\... 경로)"
while ($true) {
  $list = aws s3api list-object-versions --bucket $Bucket --region $Region --output json | ConvertFrom-Json

  $toDelete = @()
  if ($list.Versions)      { foreach ($v in $list.Versions)      { $toDelete += @{ Key = $v.Key; VersionId = $v.VersionId } } }
  if ($list.DeleteMarkers) { foreach ($m in $list.DeleteMarkers) { $toDelete += @{ Key = $m.Key; VersionId = $m.VersionId } } }

  if ($toDelete.Count -eq 0) { break }

  # 임시 폴더에 저장(경로 한글/공백 회피) + UTF-8(no-BOM)
  $tmp  = Join-Path $env:TEMP ("del-{0}.json" -f ([guid]::NewGuid()))
  $json = @{ Objects = $toDelete } | ConvertTo-Json -Depth 5 -Compress
  $utf8 = New-Object System.Text.UTF8Encoding($false)
  [System.IO.File]::WriteAllText($tmp, $json, $utf8)

  # AWS CLI는 file://C:\\... 또는 file://C:/... OK
  $uri = "file://$tmp"

  aws s3api delete-objects --bucket $Bucket --delete $uri --region $Region | Out-Null
  Remove-Item $tmp -Force -ErrorAction SilentlyContinue

  Write-Host (" - Deleted {0} versions in this batch..." -f $toDelete.Count)
}

Write-Host "Step3: 버킷 삭제"
aws s3api delete-bucket --bucket $Bucket --region $Region

Write-Host "Step4: 삭제 확인"
aws s3api head-bucket --bucket $Bucket --region $Region 2>$null
if ($LASTEXITCODE -eq 0) { "Bucket exists? True (still there)" } else { "Bucket successfully deleted." }

# (선택) DynamoDB 락 테이블 삭제
# aws dynamodb delete-table --table-name terraform-locks --region $Region

💡 콘솔에서도 “버킷 비우기(Empty)” 기능을 쓰면 위 2단계를 한 번에 처리해줍니다.

2-3) DynamoDB 락 테이블 삭제

aws dynamodb delete-table --table-name terraform-locks --region ap-northeast-2

 

3. 삭제 확인

# S3 버킷 존재 유무 (0이면 없음)
aws s3api list-buckets --query "length(Buckets[?Name=='$Bucket'])" --output text

# (남았다면) 남은 버전/삭제마커 총합
aws s3api list-object-versions --bucket $Bucket --region $Region `
  --query "length(Versions) + length(DeleteMarkers)" --output text

# DynamoDB 테이블 존재 유무 (0이면 없음)
aws dynamodb list-tables --region $Region --query "length(TableNames[?@=='terraform-locks'])" --output text

4. 참고 (Terraform으로 강제 삭제하고 싶다면)

  1. 위에서 백엔드 끊기 완료 후
  2. prevent_destroy 를 잠시 제거하고
  3. 아래처럼 타겟 파괴:
terraform destroy -auto-approve `
  -target=aws_s3_bucket_versioning.tf_state `
  -target=aws_s3_bucket.tf_state `
  -target=aws_dynamodb_table.tf_locks

그래도 S3는 완전 비우기가 선행되어야 합니다.


9. (선택) 더 쉬운 두 가지 운영 패턴

  • 패턴 A – AWS CLI로 부트스트랩(가장 단순)
    aws s3api create-bucket --bucket my-terraform-state-bucket-250829 --create-bucket-configuration LocationConstraint=ap-northeast-2 --region ap-northeast-2
    aws s3api put-bucket-versioning --bucket my-terraform-state-bucket-250829 --versioning-configuration Status=Enabled --region ap-northeast-2
    aws dynamodb create-table --table-name terraform-locks --attribute-definitions AttributeName=LockID,AttributeType=S --key-schema AttributeName=LockID,KeyType=HASH --billing-mode PAY_PER_REQUEST --region ap-northeast-2
    terraform init -reconfigure
    terraform apply -auto-approve
    
    
  • 패턴 B – 폴더 분리(bootstrap/ vs main/)
    • bootstrap/에는 backend 블록 없이 S3/DDB만
    • main/에는 backend.tf + 본 인프라
    • 처음 이후로는 항상 main/에서 plan/apply만

현재 우리가 진행했던 건 패턴 C(단일 폴더 In-place Bootstrap)입니다:

backend 임시 비활성화 → S3/DDB 생성 → init -migrate-state → 본 인프라 배포


10) GitHub 업로드

git init
git remote add origin <repo-url>
git add .
git commit -m "Step 1-2: variables, modules, S3 backend (bootstrap→migrate)"
git push -u origin main

✅ 요약

  • S3 백엔드는 init 시 먼저 필요 → 로컬로 Bootstrap → migrate-state가 정석
  • PowerShell은 -target=...를 따옴표로 감싸서 파싱 오류 방지
  • 버저닝 버킷은 비우기까지 해야 삭제 가능
  • 한 번 이관하고 나면 이후엔 그냥 plan/apply만 쓰면 된다

'클라우드' 카테고리의 다른 글

[Step 1-1] Docker 기초부터 운영까지  (1) 2025.08.29
인프라·아키텍처·프로토콜  (2) 2025.08.21
OSI 7계층 정리  (3) 2025.07.07
[실습] 인스턴스 생성하기  (0) 2025.05.17
AWS 보안 서비스 알아보기  (2) 2025.05.05
'클라우드' 카테고리의 다른 글
  • [Step 1-1] Docker 기초부터 운영까지
  • 인프라·아키텍처·프로토콜
  • OSI 7계층 정리
  • [실습] 인스턴스 생성하기
samsam031
samsam031
samsam031 님의 블로그 입니다.
  • samsam031
    samsam031 님의 블로그
    samsam031
  • 전체
    오늘
    어제
    • 분류 전체보기
      • 디지털포렌식
      • 드림핵 문제풀이
      • 대외활동
      • 개발 실습
      • 컴퓨터 보안
      • 클라우드
      • 자격증
      • 자연어처리
      • 백엔드
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
samsam031
[Step 1-2] Terraform: 변수/모듈화 & S3 Backend
상단으로

티스토리툴바