Terraform 모듈 설계 안티패턴 3가지와 Feature Flag로 풀어내기
들어가며
안녕하세요. 제조 비즈니스 테크놀로지부 소속 hongkii입니다.
Terraform으로 dev / staging / prod, 거기다 샌드박스 환경까지 운영하다 보면, 환경별로 리소스 구성을 조금씩 달리 가져가고 싶은 경우가 한 번쯤은 꼭 생기게 되는데요.
예를 들어 "prod에는 알림 시스템이 아직 없기 때문에 알림 대시보드만 빼고 싶다"거나, "샌드박스 환경에서는 비용을 줄이기 위해 RDS Multi-AZ 구성을 끄고 싶다" 같은 경우가 대표적입니다.
이럴 때 가장 먼저 떠오르는 게 모듈 안에 var.environment == "prod" 같은 분기를 넣는 방법입니다. 저도 처음엔 그렇게 작성했었는데, 환경이 하나씩 늘어날 때마다 모듈을 손대게 되어서 결국엔 모듈이 점점 복잡해지더라구요.
그래서 이번 포스트에서는 비교적 깔끔하게 유지보수할 수 있는 Feature Flag 패턴에 대해서 소개드리고자 합니다.
TL;DR
먼저 정리부터 하자면 다음과 같습니다.
- 모듈은 환경 이름을 알지 못하도록 만든다
- 환경별 차이는 Feature 단위 변수로 모듈이 외부에 공개한다
- 리스트로 통째로 넘기기보다 boolean 플래그로 의도를 드러낸다
이제 자주 보이는 안티패턴부터 하나씩 살펴보면서, 어떻게 고쳐가는지 보여드리겠습니다.
안티패턴 1. 환경 이름으로 분기하는 패턴
먼저 가장 자주 보이는 형태입니다.
# modules/dashboard/main.tf (안티패턴)
resource "grafana_dashboard" "notification" {
count = var.environment == "prod" ? 0 : 1
config_json = file("${path.module}/dashboards/notification.json")
}
"prod에서는 notification 대시보드를 만들지 않는다"라는 의도인데요, 잘 동작하긴 하지만 다음과 같은 문제를 안고 있습니다.
- 원래 환경에 의존하지 않아야 할 모듈이 특정 환경 이름을 알게 됨
staging-2같은 새로운 환경이 추가될 때마다 모듈 코드를 다시 열어봐야 함- 왜 prod만 다르게 처리하는지, 모듈 코드만 봐서는 의도를 파악하기 어려움
특히 공유 모듈을 수정할 때는, 그 변경이 다른 환경에 미칠 영향을 매번 확인해야 해서 작업 부담이 점점 커지게 됩니다.
베스트 프랙티스. Feature Flag 패턴
그럼 어떻게 고치면 좋을까요?
모듈은 "이 기능을 사용할지 안할지"만 변수로 받고, 그 값을 정하는 건 환경 쪽에 맡기는 것이 핵심입니다.
# modules/dashboard/variables.tf
variable "enable_notification_dashboard" {
description = "알림 계열 대시보드를 만들지 여부"
type = bool
default = true
}
# modules/dashboard/main.tf
resource "grafana_dashboard" "notification" {
count = var.enable_notification_dashboard ? 1 : 0
config_json = file("${path.module}/dashboards/notification.json")
}
그리고 호출하는 쪽에서는 환경에 맞게 플래그만 덮어써주시면 됩니다.
# env/prod/dashboard.tf
module "dashboard" {
source = "../../modules/dashboard"
enable_notification_dashboard = false # prod는 알림 기반이 아직 없어서 끔
}
# env/staging/dashboard.tf
module "dashboard" {
source = "../../modules/dashboard"
# default가 true라서 따로 안 적어도 됩니다
}
이렇게 바꿔두면 좋은 점이 몇 가지 있는데요.
- 모듈은 "무엇을 만드는지"만 알고, "어떤 환경에서 쓰이는지"는 신경 쓸 필요가 없게 됩니다
- 환경이 새로 생겨도, 그 환경의 파일만 추가하면 끝납니다
- 플래그 이름이 곧 기능을 나타내기 때문에, 왜 false로 설정해두었는지가 코드만 봐도 바로 보입니다
안티패턴 2. 리스트로 통째로 넘겨서 제외하기
"여러 리소스 중에 특정한 것만 빼고 싶을 때" 리스트로 다 넘겨버리는 방식도 자주 보이는 패턴인데요.
# env/prod/alert.tf (안티패턴)
module "alert" {
source = "../../modules/alert"
# notification 빼고 나머지를 전부 나열
alert_targets = ["api", "worker", "scheduler", "cache", "queue"]
}
이 코드도 동작은 하지만, 다음과 같은 부분이 마음에 걸립니다.
- 모듈 쪽에 알림 대상이 하나 더 늘어나면, 빼고 싶은 환경의 리스트도 같이 업데이트해줘야 합니다
- 리스트만 봐서는 "왜 notification만 빠져있는지" 그 이유를 알 수가 없구요
- 무엇보다 깜빡하고 누락이라도 되면, 의도치 않은 알림이 사라져 버리는 위험도 있습니다
여기에 Feature Flag를 적용하면 이렇게 깔끔해집니다.
# env/prod/alert.tf
module "alert" {
source = "../../modules/alert"
enable_notification_alert = false # 의도가 한눈에 보입니다
}
알림 대상이 새로 추가되더라도 이 라인은 바꿀 필요가 없습니다.
안티패턴 3. 이중 부정으로 머리가 꼬이는 케이스
플래그 이름을 잘못 정하면, 코드를 읽을 때마다 머릿속에서 한 번 더 뒤집어 생각하게 되는데요.
# 이런 식이 되기 쉽습니다
variable "enable_notification_alert" {
type = bool
default = true
}
# 호출하는 쪽
enable_notification_alert = false # "활성화를 false" ← 한 번에 안 읽힙니다
# 게다가 모듈 내부에서도 또 뒤집어쓰면 더 헷갈리게 됩니다
is_paused = var.enable_notification_alert ? false : true
이런 경우엔 동사를 의도에 맞춰서 정하는 것으로 해결할 수 있습니다. 일시 정지가 목적이었다면 pause_로 이름을 짓는 것이 훨씬 직관적입니다.
variable "pause_notification_alert" {
description = "알림을 일시 정지할지 여부"
type = bool
default = false
}
# 호출하는 쪽
pause_notification_alert = true # "일시 정지를 true" ← 그대로 읽힙니다
# 모듈 내부
is_paused = var.pause_notification_alert # 뒤집을 필요 없음
변수명만 바꾼 작은 차이지만, 코드 리뷰할 때 받는 인상이 꽤 달라지더라구요.
마무리
이번 포스트에서 소개드린 내용을 표로 정리해보면 다음과 같습니다.
| 패턴 | 평가 | 이유 |
|---|---|---|
| 환경 이름으로 분기 | 안티패턴 | 모듈이 환경에 의존하고, 환경이 늘 때마다 모듈도 수정해야 함 |
| 리스트로 통째로 넘겨서 제외 | 안티패턴 | 누락 위험이 있고, 의도가 잘 드러나지 않음 |
| Feature Flag (boolean) | 베스트 프랙티스 | 환경에 비의존적이고, 의도가 명확하며, 변경에 강함 |
| 동사와 값의 방향을 맞추는 명명 | 베스트 프랙티스 | 이중 부정을 제거해서 가독성을 높임 |
Terraform 모듈을 설계하실 때, "이 모듈에 환경 이름을 넘기고 싶어진다면, 한 번쯤 설계를 다시 봐야 할 신호" 정도로 생각해두시면 모듈을 좀 더 오래 깔끔하게 운영하실 수 있지 않을까 합니다.
참고로 "Feature Flag 패턴"이라는 명칭 자체는 커뮤니티에서 흔히 쓰이는 표현으로, HashiCorp 공식 명명은 아닙니다. 다만 이번 포스트에서 다룬 "모듈이 환경에 의존하지 않도록 설계한다", "boolean 변수로 리소스 생성을 제어한다"라는 원칙은 HashiCorp 공식 문서에서도 권장하는 방향이라, 모듈 설계 시의 기준점으로 삼아보셔도 좋을 것 같습니다.
여러 환경을 운영하고 계신 분들께 조금이나마 도움이 되었다면 좋겠습니다.
참고 자료
- Module Composition - HashiCorp Terraform Docs
- 모듈은 의존성을 직접 만들지 말고 외부로부터 주입(dependency inversion)받도록 설계해야 한다는 원칙
- The count Meta-Argument - HashiCorp Terraform Docs
count = var.flag ? 1 : 0형식의 조건부 리소스 생성 패턴







