Slack 메시지로 제 월요일이 망쳐졌다
"Hey, 이전 플랫폼 팀이 떠났어. 리포지토리를 여기에 두고. 행운을 빌어 🫡"
나는 Git 리포지토리를 바라보았다.
47,000 줄의 Terraform.
하나의 상태 파일. 모듈은 없었다.
x 이름이 __JHSNS_SEG_3fec2966_5__, temp2, 그리고 제가 가장 좋아하는 것 - DO_NOT_TOUCH_ask_raj.
__JHSNS_SEG_3fec2966_8__ Raj는 두 년 전에 회사를 떠났었다.
한 해 이상 Senior DevOps Engineer로 일해왔다면, 그런 것들을 상속받았을 거예요. 아마 47K 줄은 아니지만, main.tf 당신의 경력 선택에 대해 의문을 제기하는 문제를 열었습니다.
이것은 "Terraform 최선의 관행" 기사가 아닙니다. 그런 것들은 Terraform을 운영해야 했던 사람들에 의해 작성되지 않았습니다.terraform plan3,000개 자원 상태 파일에 2시에 있을 때 엔지니어링 부사장이 지켜보고 있었다.
이것은 생존 가이드입니다.
반패턴 #1: 모노리스 상태 파일 (또는 "직업 실패의 단일 지점")
나가서 발견한 것
# main.tf — 8,400 lines
# "Managed" networking, compute, databases, DNS, IAM, monitoring,
# and somehow... a CloudFront distribution for a marketing site
# that was decommissioned in 2023.
resource "aws_vpc" "main" { ... }
resource "aws_instance" "api_server_1" { ... }
resource "aws_instance" "api_server_2" { ... }
# ... 200 more instances ...
resource "aws_rds_instance" "prod_db" { ... }
resource "aws_iam_role" "god_mode" { ... } # yes, really
하나의terraform apply촉촉했다모든 것이 네트워킹, 데이터베이스, 컴퓨트, DNS — 모두 1월의 크리스마스 조명처럼 뒤얽혀 있습니다. 보안 그룹 규칙에 오타 하나라도 있으면? 축하합니다, 당신의 plan이 847개의 자원을 평가해야 하는데, Terraform은 당신의 RDS 인스턴스를 교체해야 한다고 결정했습니다.
실제 위험
이건 단순히 엉망이 아니라 — 운영적으로 치명적입니다. 이것이 일어나는 것은 다음과 같습니다:
-
terraform plan는 14분을 소요합니다. - State 파일 잠금은 한 번에 한 사람만 작업할 수 있음을 의미합니다.
- 오류의 폭발 반경 = 전체 인프라.
- 새로운 팀 멤버들은 아무것도 건드리지 두려워합니다 (당연히 그럴 만합니다).
어떻게 해결했는지 (운영 중단 없이)
단계 1: State 수술 시작 vớiterraform state mv
# First, I mapped resource dependencies visually
terraform graph | dot -Tsvg > infra-dependency-map.svg
# Then, split by domain boundaries
terraform state mv 'aws_vpc.main' -state-out=networking/terraform.tfstate
terraform state mv 'aws_subnet.public[0]' -state-out=networking/terraform.tfstate
terraform state mv 'aws_subnet.public[1]' -state-out=networking/terraform.tfstate
단계 2: 블래스트 반경으로 상태 경계 giới thiệu
나는 다섯 개의 상태 파일로 나뉘었습니다.주파수 변경그리고폭발 반경:
| 레이어 | 콘텐츠 | 주파수 변경 | 폭발 반경 |
|---|---|---|---|
foundation |
VPC, 서브넷, 경로 테이블 | 월별 | 심각 |
security |
IAM, KMS, 보안 그룹 | 주간 | 심각 |
data |
RDS, ElastiCache, S3 | 희귀 | 대단 |
compute |
ECS/EKS, ASGs, ALBs | 일별 | 높음 |
edge |
CloudFront, Route53, WAF | 주간 | 중간 |
3단계: 원격 상태 데이터 소스로 함께 연결합니다
# In compute/main.tf
data "terraform_remote_state" "networking" {
backend = "s3"
config = {
bucket = "company-terraform-state"
key = "foundation/terraform.tfstate"
region = "us-east-1"
}
}
resource "aws_ecs_service" "api" {
# Reference networking outputs safely
network_configuration {
subnets = data.terraform_remote_state.networking.outputs.private_subnet_ids
}
}
결과: terraform plan 14분에서 45초로 줄었습니다. 팀 속도가 삼배로 증가했습니다. 2시에 알림을 받지 않습니다.
반패턴 #2: 사본 붙여넣기 제국 (또는 "집에서 모듈")
나가서 발견한 것
environments/
├── dev/
│ └── main.tf # 1,200 lines
├── staging/
│ └── main.tf # 1,200 lines (95% identical to dev)
├── prod/
│ └── main.tf # 1,200 lines (90% identical... with 47 "hotfixes")
└── dr/
└── main.tf # 1,200 lines (copied from prod 8 months ago, never updated)
동일한 인프라의 사본 4개와 미묘한 차이. 스테이징에는 보안 그룹 규칙이 있었는데, 프로덕션에는 없었음. DR은 3개의 서비스가 전혀 없었음. 누구도 어떤 차이가 의도된 것인지 알지 못함.
왜 이것이 경력 공학자들을 죽인다?
너는 못해diff이 문제를 해결하는 당신의 방법. 파일들은 의도적(프로덕션은 더 큰 인스턴스를 가짐)과 우발적(개발자가 버그를 수정했지만 전파를 깜빡했음)한 방식으로 분열되었습니다. 당신은진실의 출처가 없음.
실제로 작동하는 리팩토링 전략
모든 것을 한 번에 통합하려 하지 마세요. 나는 3 스프린트 동안 지속된 "큰 폭" 리팩토링이 실패하여 스테이징 환경을 1 주일 동안 망가뜨린 후에 어렵게 이를 배웠습니다.
대신, Strangler Fig 패턴을 사용하세요:
# modules/api-platform/main.tf
variable "environment" {
type = string
validation {
condition = contains(["dev", "staging", "prod", "dr"], var.environment)
error_message = "Environment must be dev, staging, prod, or dr."
}
}
variable "config" {
type = object({
instance_type = string
min_capacity = number
max_capacity = number
enable_waf = bool
multi_az = bool
backup_retention = number
})
}
locals {
# Environment-specific defaults that document WHY they differ
env_config = {
dev = {
instance_type = "t3.medium"
min_capacity = 1
max_capacity = 2
enable_waf = false
multi_az = false
backup_retention = 1
}
prod = {
instance_type = "m5.xlarge"
min_capacity = 3
max_capacity = 20
enable_waf = true
multi_az = true
backup_retention = 35
}
}
}
핵심 통찰: 모든 환경 차이는코드에 의식적인 결정으로 문서화되었습니다가 아닌, 1,200줄의 파일에 우연한 분기로 숨겨져 있지 않습니다
반패턴 #3: terraform apply -auto-approve YOLO 파이프라인
.gitlab-ci.yml
deploy_prod:
stage: deploy
script:
- terraform init
- terraform apply -auto-approve # 🚨 WHAT
only:
- main
계획 아티팩트 없음. 승인 게이트 없음. 차이 검토 없음. main으로 푸시 → 생산 환경에서 인프라 변경. 커밋 히스토리는 공포 이야기를 말해줌:
fix: revert the revert of the fix
fix: actually fix prod this time
fix: ok THIS one fixes it
revert: revert everything from today
실제로 선임 엔지니어가 필요한 것은 무엇인가
# .github/workflows/terraform.yml
name: "Terraform"
on:
pull_request:
paths: ['infrastructure/**']
push:
branches: [main]
paths: ['infrastructure/**']
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Terraform Plan
id: plan
run: |
terraform init
terraform plan -no-color -out=tfplan \
-detailed-exitcode 2>&1 | tee plan_output.txt
continue-on-error: true
- name: Comment Plan on PR
uses: actions/github-script@v7
if: github.event_name == 'pull_request'
with:
script: |
const fs = require('fs');
const plan = fs.readFileSync('plan_output.txt', 'utf8');
const truncated = plan.length > 60000
? plan.substring(0, 60000) + '\n\n... truncated ...'
: plan;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `## Terraform Plan Output\n\`\`\`\n${truncated}\n\`\`\``
});
- name: Upload Plan Artifact
uses: actions/upload-artifact@v4
with:
name: tfplan
path: tfplan
apply:
needs: plan
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: production # Requires manual approval
steps:
- uses: actions/checkout@v4
- name: Download Plan
uses: actions/download-artifact@v4
with:
name: tfplan
- name: Terraform Apply
run: terraform apply tfplan # Apply ONLY the reviewed plan
타협할 수 없는 규칙:
- PR에 계획이 생성되고 아티팩트로 첨부됩니다.
- 인간이 생산 적용 전에 차이점을 검토합니다.
- Apply는 사용합니다정확히리뷰된 계획(새로운 계획은 아님).
- 더욱
production환경은 고급 엔지니어의 수동 승인이 필요합니다.
반패턴 #4: 비밀 정보 (The Ticking Compliance Bomb)
나는 발견한 것
resource "aws_db_instance" "prod" {
engine = "postgres"
instance_class = "db.r5.2xlarge"
username = "admin"
password = "Pr0d_P@ssw0rd_2022!" # I wish I was joking
publicly_accessible = true # I really wish I was joking
}
비밀번호는 .tf 파일에, 상태 파일에, 계획 출력에, 와 Git 이력에 있었습니다. 네 곳에서 유출될 수 있는 곳입니다. 그리고publicly_accessible = true는 이 쓰레기 손담이 아이스크림 위에 올린 딸기였습니다.
The Fix (That Also Passes Audit)
# Use a data source to pull secrets at plan/apply time
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/rds/master-password"
}
resource "aws_db_instance" "prod" {
engine = "postgres"
instance_class = "db.r5.2xlarge"
username = "admin"
password = data.aws_secretsmanager_secret_version.db_password.secret_string
publicly_accessible = false
# Prevent Terraform from detecting password "drift"
lifecycle {
ignore_changes = [password]
}
}
하지만 그것만으로는 충분하지 않습니다. 주 state 파일 여전히 민감한 값 포함하고 있습니다. 완전한 해결책:
# backend.tf
terraform {
backend "s3" {
bucket = "company-terraform-state"
key = "prod/data/terraform.tfstate"
region = "us-east-1"
encrypt = true # SSE-KMS encryption
kms_key_id = "arn:aws:kms:us-east-1:xxx:key/yyy"
dynamodb_table = "terraform-state-lock"
}
}
더 엄격한 S3 버킷 정책, 접근 로깅, 그리고 절대 개발자에게 직접 상태 파일 접근을 허용하지 않기. 대신 terraform output을 사용하세요.
반패턴 #5: 200줄의 중첩 블록으로 이루어진 "신의 자원"
나가 발견한 것
resource "aws_ecs_task_definition" "api" {
family = "api"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = 1024
memory = 2048
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = jsonencode([
{
name = "api"
image = "company/api:latest" # 🚨 LATEST TAG IN PROD
portMappings = [{ containerPort = 8080 }]
environment = [
{ name = "DB_HOST", value = "prod-db.cluster-xxx.us-east-1.rds.amazonaws.com" },
{ name = "DB_NAME", value = "production" },
{ name = "REDIS_URL", value = "prod-redis.xxx.cache.amazonaws.com:6379" },
# ... 45 more environment variables hardcoded here ...
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = "/ecs/api"
"awslogs-region" = "us-east-1"
"awslogs-stream-prefix" = "api"
}
}
# ... 80 more lines of health checks, mount points, ulimits ...
}
])
}
문제가 복잡해지고 있습니다:
- 환경 변수는 고정 코드화되어 있습니다(SSM/Secrets Manager에서 소스로부터 가져오지 않음).
-
latest태그는 배포가 재현할 수 없음을 의미합니다. jsonencode볼륨은 PR 리뷰에서 테스트되거나 비교할 수 없습니다.- 환경 변수의 한 변경 사항이 전체 작업 정의 재설정을 트리거합니다.
리팩토링된 버전
# Use templatefile for complex JSON — it's testable and readable
resource "aws_ecs_task_definition" "api" {
family = "api-${var.environment}"
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
cpu = var.task_cpu
memory = var.task_memory
execution_role_arn = aws_iam_role.ecs_execution.arn
task_role_arn = aws_iam_role.ecs_task.arn
container_definitions = templatefile("${path.module}/templates/api-container.json.tpl", {
image_tag = var.image_tag # Pinned, passed from CI/CD
environment = var.environment
db_host = data.aws_ssm_parameter.db_host.value
redis_url = data.aws_ssm_parameter.redis_url.value
log_group = aws_cloudwatch_log_group.api.name
aws_region = data.aws_region.current.name
})
}
리팩토링 플레이북 (이번 월요일에 해보세요)
이 케이스를 세 달 동안 뒤엉킨 후, 이것이 작동하는 순서는 다음과 같습니다:
1주차: 트라이게이션 및 보호
# 1. Enable state file encryption and locking NOW
# 2. Add branch protection — no direct pushes to main
# 3. Run terraform plan and SAVE the output as your baseline
terraform plan -no-color > baseline_plan_$(date +%Y%m%d).txt
# 4. Enable detailed audit logging on your state bucket
2-4주차: 모노리스 분리
# Use terraform state list to inventory everything
terraform state list > all_resources.txt
wc -l all_resources.txt # Mine had 2,847 resources
# Group by service domain
grep "aws_vpc\|aws_subnet\|aws_route" all_resources.txt > networking.txt
grep "aws_iam\|aws_kms" all_resources.txt > security.txt
grep "aws_rds\|aws_elasticache\|aws_s3" all_resources.txt > data.txt
grep "aws_ecs\|aws_alb\|aws_autoscaling" all_resources.txt > compute.txt
주 5-8: 모듈화를 점진적으로 수행
한 번에 하나의 서비스를모듈로 이동시킨다. 각 이동 후:
terraform plan를 실행한다 — 이는변경 사항이 없음을 보여야 한다.- 계획에 변경 사항이 표시되면 버그가 있습니다. 계속하기 전에 수정하세요.
- 다른 경험 많은 엔지니어로부터 PR 검토를 받으세요.
- 적용하고 24시간 동안 모니터링하세요.
주 9-12: 파이프라인을 강화하세요
terraform validate와tflint를 CI에 추가하세요.- 보안 스캔을 위해
checkov또는tfsec을 추가하세요. - 이상 탐지를 구현합니다 (차이를 알리는 일정된 계획).
- 비용 추정을
infracost과 함께 추가합니다.
이상 탐지 크론 작업은 우리를 구한 것
이것은 아무도 이야기하지 않는 일입니다. 완벽한 리팩토링 이후에도 이상이 발생합니다 콘솔에서 누군가 클릭했습니다. 자동 수정 도구가 변경을 수행합니다. Lambda가 보안 그룹을 수정합니다.
# .github/workflows/drift-detection.yml
name: "Drift Detection"
on:
schedule:
- cron: '0 6 * * 1-5' # Every weekday at 6 AM
jobs:
detect-drift:
runs-on: ubuntu-latest
strategy:
matrix:
layer: [foundation, security, data, compute, edge]
steps:
- uses: actions/checkout@v4
- name: Terraform Plan (Drift Check)
id: plan
working-directory: infrastructure/${{ matrix.layer }}
run: |
terraform init
terraform plan -detailed-exitcode -no-color > plan.txt 2>&1
echo "exitcode=$?" >> $GITHUB_OUTPUT
continue-on-error: true
- name: Alert on Drift
if: steps.plan.outputs.exitcode == '2'
run: |
# Exit code 2 = changes detected (drift!)
curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
-H 'Content-type: application/json' \
-d "{\"text\":\"🚨 Drift detected in *${{ matrix.layer }}* layer. Check the plan output.\"}"
첫 주에만 3건의 미승인 콘솔 변경을 포착했습니다.
최고 엔지니어가 엉망으로 물려받은 후의 마지막 충언
모든 것을 한 번에 리팩토링하지 마세요. 문제를 만들고 신뢰도를 잃게 될 거예요.
고치기 전에 발견한 사항을 문서화하세요. 공포스러운 부분을 스크린샷하세요. 사후 분석과 성과 평가에 필요할 거예요.
시작하기 전에 리더십의 승인을 받으세요. "기술 부채를 해결하기 위해 3번의 스프린트가 필요합니다"는 어려운 판매입니다. "현재 설정은 인프라 변경이 사고를 유발할 확률이 40%라는 것"은 예산을 승인받습니다.
각각의
terraform state mv은 별도의, 검토된 PR이어야 합니다. 기술적으로 필요하기 때문이 아니라, 단계 50의 37단계에서 문제가 발생했을 때 깨끗한 git 이력을 이분법적으로 분석하고 싶기 때문입니다.목표는 완벽한 Terraform이 아닙니다. 목표는 팀이 2시에 안전하게 운영할 수 있는 Terraform입니다.주니어 엔지니어가 실행할 수 없다면
terraform plan두려움 없이, 당신의 리팩토링은 끝나지 않았습니다.
스크롤러용 TL;DR
| 반패턴 | 수정하세요 | 우선순위 |
|---|---|---|
| 몬로빗 상태 파일 | 폭발 반경으로 나누고 주파수 변경 | P0 |
| 복사-붙여넣기 환경 | 모듈 + 환경 구성 | P1 |
-auto-approve CI에서 |
계획 아티팩트 + 수동 승인 게이트 | P0 |
| 상태/코드에 있는 비밀 | 비밀 관리자 + 암호화된 상태 + ignore_changes
|
P0 |
| JSON으로 직접 포함된 God 자원 |
templatefile + SSM 파라미터 |
P2 |
| 이상치 감지 없음 | 예약된plan알림과 함께 |
P1 |
Terraform 코드베이스를 보고 빈 공간에 "누가 이걸 만들었지?"라고 속삭였다면 — 당신만이 아니에요. 우리 모두 그런 경험이 있었어요. 좋은 소식은? 수정할 수 있어요. 한 번에 한 번의 상태 이동으로.
이것이 유용하신가요? 더 많은 테스트를 통과한 DevOps 콘텐츠를 위해 저를 따르세요. 저는 실제 생산 환경에서 일어나는 일에 대해 씁니다 — 문서에서의 행복한 경로가 아닙니다.










