이번 단계에서는 기본 문법 → 변수 → 모듈화 → 원격 상태 관리(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 연결 + 상태 이관
- backend.tf.off → backend.tf로 복구 or 주석해제
- 이관 초기화
terraform init -migrate-state
메시지 나오면 yes.
terraform state list로 리소스 목록이 그대로 보이면 성공.
C) 본 인프라 배포(EC2/VPC 등)
terraform plan
terraform apply -auto-approve
✅ 콘솔에서 생성확인

7. 자주 막히는 포인트
- Too many command line arguments / Invalid target "aws_s3_bucket"
→ PowerShell이 줄바꿈/공백을 깨먹은 것. 따옴표로 감싸거나, plan 파일을 먼저 만든 뒤 적용: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 - 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으로 강제 삭제하고 싶다면)
- 위에서 백엔드 끊기 완료 후
- prevent_destroy 를 잠시 제거하고
- 아래처럼 타겟 파괴:
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 |