단일 Terraform 모듈과 Terragrunt를 활용하여 Stage/Production 환경을 동일한 코드로 관리하며, GitHub Actions 기반 GitOps 파이프라인으로 인프라 배포 시간을 80% 단축하고 환경 간 불일치로 인한 장애를 Zero로 만든 프로젝트입니다. 7개 팀, 30명이 사용하는 플랫폼의 인프라를 안전하고 효율적으로 운영할 수 있는 체계를 구축했습니다.
infrastructure/
├── terragrunt.hcl # 루트 설정 (S3 백엔드, DynamoDB 락)
├── modules/
│ └── application/ # 단일 Terraform 모듈
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ ├── vpc.tf # 네트워크 리소스
│ ├── ecs.tf # 컨테이너 오케스트레이션
│ ├── alb.tf # 로드 밸런싱
│ ├── rds.tf # 데이터베이스
│ ├── security_groups.tf # 보안 설정
│ └── monitoring.tf # CloudWatch 알람
├── stage/
│ └── terragrunt.hcl # Stage 환경 변수
└── prod/
└── terragrunt.hcl # Production 환경 변수
# stage/ecs.tf - 200줄의 코드
resource "aws_ecs_service" "app" {
name = "app-stage"
cluster = "stage-cluster"
task_definition = "app-stage:latest"
desired_count = 2
# ... 수많은 중복 설정
}
# prod/ecs.tf - 동일한 200줄의 코드 (값만 다름)
resource "aws_ecs_service" "app" {
name = "app-prod"
cluster = "prod-cluster"
task_definition = "app-prod:latest"
desired_count = 4
# ... 동일한 중복 설정
}
# modules/application/ecs.tf (단일 모듈 - 한 번만 작성)
resource "aws_ecs_service" "app" {
name = "${var.environment}-app"
cluster = var.cluster_name
task_definition = "${var.app_name}:${var.app_version}"
desired_count = var.desired_count
deployment_configuration {
maximum_percent = var.deployment_maximum_percent
minimum_healthy_percent = var.deployment_minimum_healthy_percent
}
# 환경별 Auto Scaling 설정
dynamic "capacity_provider_strategy" {
for_each = var.capacity_providers
content {
capacity_provider = capacity_provider_strategy.value.name
weight = capacity_provider_strategy.value.weight
}
}
}
# stage/terragrunt.hcl - 환경별 변수만 정의
inputs = {
environment = "stage"
desired_count = 2
instance_type = "t3.small"
deployment_maximum_percent = 200
deployment_minimum_healthy_percent = 50
capacity_providers = [{
name = "FARGATE_SPOT"
weight = 100 # Stage는 비용 최적화
}]
}
# prod/terragrunt.hcl
inputs = {
environment = "production"
desired_count = 4
instance_type = "t3.large"
deployment_maximum_percent = 150
deployment_minimum_healthy_percent = 100
capacity_providers = [{
name = "FARGATE"
weight = 100 # Production은 안정성 우선
}]
}
name: Terraform Stage Deployment
on:
pull_request:
branches: [dev]
paths:
- 'terragrunt/stage/**'
- 'terragrunt/modules/**'
push:
branches: [dev]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_STAGE }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_STAGE }}
aws-region: ap-northeast-2
- name: Setup Terragrunt
uses: autero1/action-terragrunt@v1.2.0
with:
terragrunt_version: 0.45.0
terraform_version: 1.2.0
- name: Clean Cache
run: |
find . -type d -name ".terragrunt-cache" -exec rm -rf {} + 2>/dev/null || true
find . -type d -name ".terraform" -exec rm -rf {} + 2>/dev/null || true
- name: Terragrunt Plan
if: github.event_name == 'pull_request'
id: plan
run: |
cd terragrunt/stage
terragrunt plan -no-color -out=tfplan
terragrunt show -no-color tfplan > plan_output.txt
- name: Post Plan to PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const planOutput = fs.readFileSync('terragrunt/stage/plan_output.txt', 'utf8');
const truncatedPlan = planOutput.length > 60000
? planOutput.substring(0, 60000) + '\n\n... (truncated)'
: planOutput;
const comment = `## 📋 Terraform Plan - Stage Environment
<details>
<summary>Click to expand plan details</summary>
\`\`\`terraform
${truncatedPlan}
\`\`\`
</details>
✅ Review the changes above before merging.`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
- name: Terragrunt Apply
if: github.event_name == 'push' && github.ref == 'refs/heads/dev'
run: |
cd terragrunt/stage
terragrunt apply --terragrunt-non-interactive -auto-approve
name: Terraform Production Deployment
on:
workflow_dispatch:
inputs:
action:
description: 'Terraform action to perform'
required: true
default: 'plan'
type: choice
options:
- plan
- apply
confirm:
description: 'Type "yes" to confirm PRODUCTION deployment'
required: false
type: string
jobs:
terraform:
runs-on: ubuntu-latest
environment: production # GitHub Environment 보호 규칙 적용
steps:
- name: Validate Production Deployment
if: inputs.action == 'apply'
run: |
if [[ "${{ inputs.confirm }}" != "yes" ]]; then
echo "❌ Production deployment requires explicit confirmation"
echo "Please type 'yes' in the confirm field to proceed"
exit 1
fi
- name: Configure Production AWS Credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID_PROD }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY_PROD }}
aws-region: ap-northeast-2
- name: Production Apply with Double Confirmation
if: inputs.action == 'apply' && inputs.confirm == 'yes'
run: |
cd terragrunt/prod
# 변경사항 재확인
echo "🔍 Reviewing changes before production deployment..."
terragrunt plan -detailed-exitcode
# 실제 적용
echo "🚀 Applying to PRODUCTION environment..."
terragrunt apply --terragrunt-non-interactive -auto-approve
# 배포 후 검증
echo "✅ Validating deployment..."
terragrunt output -json > deployment_result.json
# terragrunt.hcl (루트 설정)
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "terraform-state-${get_aws_account_id()}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
# DynamoDB 테이블을 통한 상태 잠금
dynamodb_table = "terraform-state-locks"
# 버전 관리 활성화
versioning = {
enabled = true
}
# 실수로 인한 삭제 방지
lifecycle {
prevent_destroy = true
}
}
}
# 환경별 태그 자동 추가
inputs = {
tags = {
Environment = basename(get_terragrunt_dir())
ManagedBy = "Terragrunt"
Repository = "infrastructure-as-code"
LastUpdated = timestamp()
}
}
메트릭 | Before (수동) | After (Terragrunt + GitOps) | 개선율 |
---|---|---|---|
코드 라인 수 | 4,000줄 (환경별 중복) | 800줄 (단일 모듈) | 80% 감소 |
배포 준비 시간 | 1시간 | 5분 | 96% 단축 |
배포 실행 시간 | 15분 | 7분 | 77% 단축 |
롤백 시간 | 30시간 | 3분 | 95% 단축 |
환경 동기화 오류 | 월 5건 | 0건 | 100% 제거 |
graph LR
A[코드 변경] --> B[Stage 배포]
B --> C{테스트 통과?}
C -->|Yes| D[Production 배포]
C -->|No| E[수정 후 재배포]
D --> F[성공률 99%]
style F fill:#90EE90
실제 운영 결과:
문제: 병렬 실행 시 .terragrunt-cache
디렉토리 충돌
Error: Error acquiring the state lock
해결:
# 각 실행 전 캐시 정리
find . -type d -name ".terragrunt-cache" -exec rm -rf {} + 2>/dev/null || true
# Terragrunt 병렬 실행 제한
export TERRAGRUNT_PARALLELISM=1
문제: 실수로 Production에 잘못된 변경 적용 위험
해결:
confirm: yes
이중 확인 메커니즘코드 중복은 단순히 유지보수의 문제가 아니라 환경 간 불일치로 인한 장애의 근본 원인입니다. Terragrunt를 통한 DRY 원칙 구현으로:
# 모든 변경사항의 투명성 확보
- Pull Request로 변경사항 사전 검토
- Plan 결과를 PR 코멘트로 자동 공유
- 팀 전체가 인프라 변경 인지 가능
# 환경 간 차이는 오직 이것뿐
locals {
environment_config = {
stage = {
instance_count = 2
instance_type = "t3.small"
backup_enabled = false
}
production = {
instance_count = 4
instance_type = "t3.large"
backup_enabled = true
}
}
}
Terraform 모듈화와 Terragrunt의 결합은 단순한 기술 도입을 넘어 인프라 관리 패러다임의 전환을 가져왔습니다. 특히 "Stage에서 검증된 것은 Production에서도 반드시 작동한다"는 확신은 팀의 배포 속도와 안정성을 동시에 향상시켰습니다.
GitHub Actions를 통한 GitOps 파이프라인은 이 모든 프로세스를 투명하고 안전하게 만들어, 주니어 개발자도 자신있게 인프라를 변경할 수 있는 환경을 조성했습니다.
본 포스트는 실제 프로젝트 경험을 바탕으로 작성되었으며, 보안을 위해 일부 세부 정보는 일반화하여 표현했습니다.
기술 스택: Terraform, Terragrunt, GitHub Actions, AWS (ECS Fargate, RDS, ALB), CloudWatch
#Terraform #Terragrunt #GitOps #DevOps #IaC #AWS #GitHubActions #DRY