왜 환경 분리가 문제가 되는가
dev, staging, prod를 하나의 테라폼 코드로 관리할 때 가장 흔한 사고는 잘못된 환경에 apply하는 것이다. 상태 파일(state)이 섞이거나, 프로덕션 변수 값이 개발 환경에 새어 들어가면 복구 비용이 크다. 테라폼이 제공하는 두 가지 접근, 즉 워크스페이스(workspace)와 디렉터리 분리는 각각 격리 수준과 운영 편의성에서 트레이드오프가 다르다. 어느 쪽을 선택하느냐가 실수의 발생 확률을 결정한다.
워크스페이스 방식의 동작과 한계
워크스페이스는 동일한 코드와 백엔드를 공유하면서 상태만 env:/<workspace>/ 경로로 분리한다. 코드 중복이 없어 간편하지만, 백엔드 설정(버킷, 계정)이 공유되므로 진짜 격리가 아니다.
terraform workspace new prod
terraform workspace select prod
# main.tf 안에서 환경 분기
locals {
env = terraform.workspace
instance_type = {
dev = "t3.small"
prod = "m5.large"
}[terraform.workspace]
}
문제는 terraform.workspace가 문자열이라 조건 분기가 코드 전반에 흩어진다는 점이다. 또 terraform init은 워크스페이스를 구분하지 않으므로, prod에 있다는 걸 잊고 apply하기 쉽다. 프롬프트에 현재 워크스페이스를 표시하지 않으면 위험하다.
디렉터리 분리 방식
환경마다 별도 디렉터리를 두고, 각자 독립된 백엔드와 tfvars를 가진다. 코드 중복은 늘지만 격리가 명확하고, prod 백엔드를 다른 계정/버킷으로 완전히 분리할 수 있다.
envs/
dev/
backend.tf # dev-tfstate 버킷
terraform.tfvars
prod/
backend.tf # prod-tfstate 버킷(별도 계정)
terraform.tfvars
modules/
network/
compute/
공통 로직은 modules/에 두고 각 환경 디렉터리는 모듈 호출과 값만 담는다. 중복은 코드가 아니라 얇은 호출부에 한정된다.
# envs/prod/main.tf
module "network" {
source = "../../modules/network"
cidr_block = var.cidr_block
env = "prod"
}
두 방식 비교
| 기준 | 워크스페이스 | 디렉터리 분리 |
|---|---|---|
| 상태 격리 | 같은 백엔드 내 경로 분리 | 완전 분리(버킷/계정) |
| 계정 분리 | 어려움 | 쉬움 |
| 코드 중복 | 없음 | 호출부에 소량 |
| 실수 위험 | 높음(현재 상태 은닉) | 낮음(경로가 곧 환경) |
| 적합 상황 | 단기·유사 환경 | 계정 분리된 실서비스 |
실무 권장: 디렉터리 기본, 워크스페이스는 보조
프로덕션과 개발이 서로 다른 클라우드 계정을 쓰는 조직이라면 디렉터리 분리가 사실상 표준이다. 계정 경계가 곧 폭발 반경(blast radius)의 경계가 되기 때문이다. 워크스페이스는 동일 계정 안에서 PR별 임시 환경, 리뷰 환경처럼 수명이 짧고 구조가 같은 경우에 적합하다. 두 방식을 섞어 쓰는 것도 가능하다. 디렉터리로 계정을 나누고, 그 안에서 워크스페이스로 단기 인스턴스를 파생시키는 식이다.
주의점과 안전장치
어느 방식이든 apply 전에 대상 환경을 강제로 확인시키는 장치가 필요하다. CI에서 prod 변경은 수동 승인 단계를 두고, 로컬에서는 셸 프롬프트에 워크스페이스나 디렉터리를 노출한다.
deploy:
stage: deploy
script:
- cd envs/${TARGET_ENV}
- terraform init
- terraform plan -out=tfplan
- terraform apply tfplan
rules:
- if: '$TARGET_ENV == "prod"'
when: manual # prod는 반드시 수동 승인
추가로 상태 잠금(state locking)을 반드시 활성화하고, prod 백엔드에는 버전 관리와 삭제 방지를 건다. 워크스페이스를 쓴다면 CI 스크립트 첫 줄에서 terraform workspace select를 명시해 기본 워크스페이스로 실수하는 경로를 막아야 한다. 격리는 코드가 아니라 백엔드와 파이프라인 규칙에서 완성된다.