성장 스택

Terraform으로 인프라 코드화하기 – Terraform (3)

류큐큐 2025. 11. 16. 01:55

PC, RDS, SG, ECS…
처음엔 “Terraform으로 만들어서 코드화만 하면 끝이겠지?”라고 생각했다.
그런데 조금만 규모가 커지면 금방 깨닫는다.

“아, 이거 모듈 없으면 유지보수 지옥이겠는데?”

 

3편에서는 내가 Terraform을 모듈화하면서 얻은 이점과
실제로 지금 운영 중인 모듈 구조를 어떻게 만들었는지 정리해봤다.

 

왜 모듈화가 필요한가?

Terraform을 쓰다 보면 이런 코드가 생긴다.

  • dev/security-groups.tf
  • staging/security-groups.tf
  • prod/security-groups.tf

그리고 세 파일이 거의 똑같다.

처음엔 크게 신경 안 쓴다.
하지만 프로젝트가 조금만 커지면 문제는 바로 드러난다.

  • 같은 태그를 여러 군데에 또 적어야 하고
  • 한 곳만 수정하면 환경마다 값이 달라지고
  • 새 환경이 추가되면 똑같은 코드 복붙하게 되고
  • 일정 지나면 뭐가 최신인지 헷갈림

즉, 클릭 대신 코드로 바꿨는데
코드가 점점 ‘클릭 같은 코드’가 되어간다.

 

그래서 모듈이 필요해졌다.


모듈로 정리하기

예를 들어 보안 그룹을 환경별로 복붙하는 대신:

module "api_server_sg" {
  source      = "../../modules/security-group"
  name        = "api-server"
  environment = "dev"
  # ...
}

딱 한 줄만 바꾸면 dev → staging → prod로 확장된다.
코드를 “작성하는 시간”보다 “고민하고 검토하는 시간”이 준다.

모듈 사용의 장점

  • DRY 원칙 → 중복 제거
  • 일관성 유지 → 어떤 환경이든 같은 규칙
  • 수정 범위 최소화 → 한 번 수정하면 전체 반영
  • 정책 강제 가능 → 암호화, 태그 같은 규칙을 모듈에서 잠그기
  • 다른 프로젝트에서도 바로 재사용

📁 내가 실제로 쓰는 모듈 구조

모듈 디렉토리는 이렇게 생겼다.

terraform/modules/
├── common-tags/
├── cloudwatch-log-group/
├── ecs-service/
├── rds/
├── alb/
├── iam-role-policy/
└── security-group/

 

관심사별로 나눠놨고, 각각에 README, variables, outputs가 있다.
이렇게 나누면 모듈 내부에서 규칙을 강제하기도 쉽다.


모듈 1: Common Tags (태그를 표준화하기)

태그가 처음엔 별거 아닌 것처럼 느껴지는데
결국엔 모든 리포트, 비용 분석, 자동화 기준이 다 태그다.

그래서 태그 구조가 꼬이면 운영 자체가 꼬인다.

태그가 꼬였을 때 실제로 생기는 문제

  • CostCenter 필드가 환경마다 이름이 달라서 비용 보고서가 분리됨
  • 팀 이름이 다르게 써져서 “누가 만든 리소스인지” 안 보임
  • 자동화 스크립트나 OPA 정책이 태그를 찾지 못함
  • 누락된 태그 때문에 내부 거버넌스 정책 위반

그래서 나는 태그를 모듈로 만들었다.

모든 리소스는 반드시 이 모듈에서 태그를 가져가도록.


모듈 2: CloudWatch Log Group (암호화 + 표준화)

로그도 문제였다.
서비스마다 retention이 제각각이고
KMS 암호화가 들어간 곳도 있고 안 된 곳도 있고
네이밍 규칙도 통일되지 않았다.

그래서 Log Group도 모듈로 묶었다.

  • 이름 규칙一 (/aws/ecs/service/component)
  • retention 유효성 검증
  • KMS 암호화 기본값
  • 태그 표준화

덕분에 ECS Task Definition에서 log_group만 넣으면 끝난다.


모듈 3: RDS (Multi-AZ, 암호화, 백업까지 한번에)

 

  • Multi-AZ를 깜빡하고 켜지 않거나
  • snapshot 설정을 빼먹거나
  • deletion_protection을 안 켜놓거나
  • 스토리지 타입을 제각각 쓰거나

이런 건 한 번 실수하면 바로 사고로 이어진다.

그래서 아예 “RDS는 이렇게 만든다”를 모듈에 박아버렸다.

모듈 내부에서:

  • KMS 암호화 필수
  • Multi-AZ 기본값 true
  • Deletion Protection 기본값 true
  • Backup 최소 7일
  • Performance Insight 기본 enabled
  • identifier 네이밍 규칙 검증
  • final snapshot 자동 생성

거버넌스는 코드에서 강제해야 한다

리뷰로 막으려는 건 강한 구조가 아니다.
사람은 실수하고, PR은 늘 바쁘고, 리뷰어는 모든 걸 기억하지 못한다.

그래서 나는 거버넌스를 모듈 안에 강제로 넣었다.

예를 들어:

 

필수 태그 검증

validation {
  condition     = contains(["dev", "staging", "prod"], var.environment)
  error_message = "Environment must be dev, staging, or prod."
}

KMS 암호화 강제

precondition {
  condition     = var.kms_key_id != null && var.kms_key_id != ""
  error_message = "KMS key is required for log encryption"
}

네이밍 규칙 강제

precondition {
  condition     = can(regex("^[a-z][a-z0-9-]*$", var.identifier))
  error_message = "Identifier must match naming convention"
}

이렇게 해두면 정책 위반을 시도하는 순간 Terraform이 바로 거절한다.


🔄 State 관리: 분리하고, 공유는 SSM으로

모듈만 잘 만들어도 좋지만,
State가 뒤섞이면 바로 사고 난다.

나는 State를 기능별로 철저히 나눴다.

terraform/network/
terraform/database/
terraform/services/api-server/
terraform/services/worker/
 

 

그리고 서로 필요한 값은
“state 파일끼리 참조하는 방식”을 쓰지 않는다.

그 대신 SSM Parameter Store에 저장해서 사용한다.

이 방식의 장점:

  • State 의존성 없어서 충돌 안 남
  • 환경별로 깔끔하게 분리됨
  • 다른 팀/서비스가 쉽게 참조 가능
  • 모듈에서만 변경하고, 참조는 전부 읽기 전용

모듈 작성할 때 내가 지키는 규칙들

1. 모듈은 한 가지 일만 하게 한다

ALB, ECS, RDS를 한 모듈에서 만들면
그건 “모듈”이 아니라 “서비스 인프라 빌더”다.

2. 기본값은 프로덕션 기준으로 안전하게

실수로 위험한 값을 넣을 가능성을 원천 차단한다.

3. 변수 검증은 넉넉하게

잘못된 값이 PR까지 올라가는 걸 허용하면 안 된다.

4. 출력값은 넉넉하게

특히 RDS처럼 여러 서비스가 참조하는 리소스는 output이 중요하다.

5. README는 빠짐없이

모듈은 문서까지 하나의 제품이라고 생각하고 만든다.


🚀 다음 편 예고

다음 편(4편)에서는
실제 PR에서 Terraform 변경이 어떻게 검증되는지

  • terraform fmt
  • validate
  • tflint
  • tfsec
  • checkov
  • OPA 정책
  • Infracost

이런 것들이 어떻게 한 번에 자동 실행되는지
그리고 이 검증을 어떻게 PR 코멘트로 남기는지 정리해볼 예정이다.