Table of contents
환경 분리는 왜 어려운가
“dev에 배포한 인프라를 그대로 prod에도 만들어주세요.” 말은 간단하지만 실제로는 까다롭다. prod는 크기가 다르고, 백업 정책도 다르고, 접근 권한도 다르다. 완전히 같은 것을 두 번 만드는 게 아니라, 같은 패턴을 조금씩 다른 파라미터로 재현하는 문제다.
환경이 분리된다는 건 state도 완전히 분리된다는 뜻이다. dev의 실수가 prod에 영향을 주면 안 된다. state가 같은 곳에 있으면 한쪽 작업이 다른 쪽을 망가뜨릴 수 있다.
flowchart TB
subgraph "환경 분리의 요구사항"
direction TB
Sep1["state 완전 분리"]
Sep2["변수/설정 차이 허용"]
Sep3["접근 권한 분리"]
Sep4["승인 프로세스 차등화"]
Sep5["코드 중복 최소화"]
end
Terraform이 제공하는 해결책은 두 가지다. 워크스페이스와 디렉토리 기반 분리. 둘을 보완해주는 도구로 Terragrunt가 있다. 이 셋을 차례로 살펴본다.
terraform workspace
Terraform은 기본적으로 default라는 워크스페이스에서 동작한다. 워크스페이스를 전환하면 state 파일이 분리된다.
# 현재 워크스페이스 확인
terraform workspace show
# default
# 새 워크스페이스 생성
terraform workspace new dev
terraform workspace new prod
# 워크스페이스 전환
terraform workspace select dev
# 리스트
terraform workspace list
# default
# * dev
# prod
원격 백엔드(S3 등)를 쓰는 경우, state 파일 경로가 워크스페이스별로 자동 분리된다. 예를 들어 백엔드 설정이 이렇다면:
terraform {
backend "s3" {
bucket = "my-tfstate"
key = "app/terraform.tfstate"
region = "ap-northeast-2"
}
}
실제 S3 경로는 이렇게 생긴다.
s3://my-tfstate/
├── app/terraform.tfstate # default
└── env:/
├── dev/app/terraform.tfstate # dev 워크스페이스
└── prod/app/terraform.tfstate # prod 워크스페이스
코드에서는 terraform.workspace 변수로 현재 워크스페이스 이름을 참조할 수 있다.
locals {
environment = terraform.workspace
instance_count = {
dev = 1
staging = 2
prod = 5
}[terraform.workspace]
}
resource "aws_instance" "app" {
count = local.instance_count
instance_type = local.environment == "prod" ? "m5.large" : "t3.micro"
tags = {
Environment = local.environment
}
}
한 번 보면 깔끔해 보인다. 코드는 한 벌, 워크스페이스만 바꾸면 여러 환경이 나오니까. 그런데 실무에서는 이게 함정이 되기도 한다.
워크스페이스의 한계
HashiCorp 공식 문서도 워크스페이스를 “경미하게 다른 환경을 위한 경량 분리”로만 권장한다. 프로덕션 분리에는 부적합하다는 뜻이다. 왜 그럴까.
1) 코드가 하나라 리스크가 크다
dev 워크스페이스에서 잘못된 코드 변경을 했다. 그런데 이 코드는 prod에서도 쓰는 코드다. dev에서 확인하고 prod로 넘어가는 게 아니라, 하나의 코드에 대해 워크스페이스만 다르게 apply하는 구조다. 프로덕션만을 위한 검증 절차를 넣기가 어렵다.
2) 설정 파일이 분리되지 않는다
워크스페이스는 state를 분리할 뿐, 코드는 공유한다. 환경별 설정을 terraform.workspace로 분기해서 쓰는데, 분기가 많아지면 코드가 복잡해지고 가독성이 떨어진다.
resource "aws_db_instance" "db" {
instance_class = {
dev = "db.t3.micro"
staging = "db.t3.medium"
prod = "db.r5.xlarge"
}[terraform.workspace]
allocated_storage = {
dev = 20
staging = 50
prod = 500
}[terraform.workspace]
backup_retention_period = {
dev = 1
staging = 7
prod = 30
}[terraform.workspace]
# ... 수십 개 속성마다 워크스페이스 분기
}
이 정도만 해도 읽기 싫어진다. 변수 파일로 빼낼 수 있지만, 결국 디렉토리 분리와 별 차이가 없어진다.
3) 백엔드 자체를 분리할 수 없다
워크스페이스는 같은 백엔드 안에서 key 경로만 다르다. dev state를 다른 AWS 계정의 S3 버킷에 두고 싶다면 워크스페이스로는 불가능하다. 환경별로 완전히 분리된 백엔드를 쓰려면 워크스페이스가 아니라 다른 접근이 필요하다.
4) 실수 가능성
현재 워크스페이스가 뭔지 프롬프트에 안 찍힌다. terraform apply를 쳤는데 사실 prod 워크스페이스였다면? 자동화를 안 한 상태에서는 실수가 나기 쉽다.
정리: 워크스페이스는 같은 팀원이 짧게 쓰는 실험용 샌드박스나 기능별 미리보기 환경 정도에는 적합하다. 장기적인 dev/staging/prod 분리에는 부적합하다.
디렉토리 기반 환경 분리
실무에서 훨씬 많이 쓰이는 방식이다. 환경별로 디렉토리를 완전히 분리한다.
infra/
├── modules/ # 재사용할 모듈들
│ ├── vpc/
│ ├── eks/
│ └── rds/
└── envs/
├── dev/
│ ├── main.tf # 모듈 조합
│ ├── variables.tf
│ ├── terraform.tfvars
│ └── backend.tf # dev용 state 백엔드
├── staging/
│ ├── main.tf
│ ├── variables.tf
│ ├── terraform.tfvars
│ └── backend.tf # staging용 state 백엔드
└── prod/
├── main.tf
├── variables.tf
├── terraform.tfvars
└── backend.tf # prod용 state 백엔드
각 환경은 독립된 디렉토리이고, 독립된 백엔드를 가진다. 모듈은 공유하되, 환경별 설정은 각 디렉토리 안에 둔다.
flowchart TB
Modules["modules/\n(재사용 단위)"]
Dev["envs/dev/"]
Staging["envs/staging/"]
Prod["envs/prod/"]
Modules --> Dev
Modules --> Staging
Modules --> Prod
Dev -->|"terraform init/apply\n(dev 디렉토리에서)"| DevState["dev state\n(dev S3 버킷)"]
Staging -->|"terraform init/apply\n(staging 디렉토리에서)"| StagingState["staging state\n(staging S3 버킷)"]
Prod -->|"terraform init/apply\n(prod 디렉토리에서)"| ProdState["prod state\n(prod S3 버킷)"]
각 환경의 main.tf는 이런 식으로 모듈을 조합한다.
# envs/prod/main.tf
module "vpc" {
source = "../../modules/vpc"
cidr_block = "10.0.0.0/16"
environment = "prod"
azs = ["ap-northeast-2a", "ap-northeast-2c", "ap-northeast-2d"]
}
module "eks" {
source = "../../modules/eks"
cluster_name = "prod-eks"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
node_groups = {
default = {
instance_type = "m5.large"
min_size = 3
max_size = 10
desired_size = 5
}
}
}
# envs/dev/main.tf
module "vpc" {
source = "../../modules/vpc"
cidr_block = "10.10.0.0/16" # prod와 다른 CIDR
environment = "dev"
azs = ["ap-northeast-2a", "ap-northeast-2c"]
}
module "eks" {
source = "../../modules/eks"
cluster_name = "dev-eks"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
node_groups = {
default = {
instance_type = "t3.medium" # 작게
min_size = 1
max_size = 3
desired_size = 1
}
}
}
같은 모듈을 쓰지만, 각 환경별로 파라미터가 다르다. 작업은 각 디렉토리에 들어가서 한다.
cd envs/dev
terraform init
terraform apply
# prod로 전환
cd ../prod
terraform init
terraform apply
장점
- 환경별로 완전히 독립된 state/백엔드
- 환경별로 다른 AWS 계정 사용 가능
- prod만 특별한 승인 프로세스를 거치게 만들기 쉬움
- 실수로 잘못된 환경에 apply할 가능성이 낮음
단점
- 코드 중복이 생김 (각 환경의
main.tf가 비슷) - 백엔드 설정을 환경별로 하드코딩하게 됨
terraform init을 환경마다 따로 해야 함
대부분의 실무 팀이 이 방식을 쓴다. 중복 문제는 모듈화로 어느 정도 완화되지만, 완벽하진 않다. 이 빈틈을 메우는 도구가 Terragrunt다.
Terragrunt 간단 소개
Terragrunt는 Gruntwork에서 만든 Terraform 래퍼다. “DRY(Don’t Repeat Yourself)하게 Terraform을 쓰자”가 핵심 철학이다.
Terragrunt의 핵심 아이디어는 환경별 디렉토리에서 반복되는 것(백엔드 설정, 프로바이더, 공통 변수)을 한 번만 선언하고 상속시키는 것이다.
전형적인 Terragrunt 프로젝트 구조다.
infra/
├── terragrunt.hcl # 루트 설정 (전역 백엔드, 프로바이더)
├── _envcommon/ # 환경 공통 컴포넌트 정의
│ ├── vpc.hcl
│ └── eks.hcl
└── envs/
├── dev/
│ ├── env.hcl # dev 전역 변수
│ ├── vpc/
│ │ └── terragrunt.hcl # _envcommon/vpc.hcl 상속
│ └── eks/
│ └── terragrunt.hcl
└── prod/
├── env.hcl
├── vpc/
│ └── terragrunt.hcl
└── eks/
└── terragrunt.hcl
루트 terragrunt.hcl에서 백엔드 한 번만 선언
# infra/terragrunt.hcl
remote_state {
backend = "s3"
config = {
bucket = "my-company-tfstate"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "my-company-tflock"
}
generate = {
path = "backend.tf"
if_exists = "overwrite"
}
}
${path_relative_to_include()}가 핵심이다. 각 하위 디렉토리의 상대 경로가 자동으로 key에 들어간다. envs/dev/vpc 에서 실행하면 key가 자동으로 envs/dev/vpc/terraform.tfstate가 된다. 환경별, 컴포넌트별 state 분리가 공짜로 해결된다.
환경 설정 한 번만
# envs/dev/env.hcl
locals {
environment = "dev"
aws_region = "ap-northeast-2"
account_id = "111122223333"
}
실제 컴포넌트 선언
# envs/dev/vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
include "envcommon" {
path = "${get_terragrunt_dir()}/../../../_envcommon/vpc.hcl"
}
locals {
env = read_terragrunt_config(find_in_parent_folders("env.hcl")).locals
}
terraform {
source = "../../../modules/vpc"
}
inputs = {
cidr_block = "10.10.0.0/16"
environment = local.env.environment
}
프로덕션 VPC의 terragrunt.hcl도 거의 같고, inputs 부분의 CIDR만 다르다.
Terragrunt가 주는 다른 강력한 기능은 의존성 자동 관리다.
# envs/dev/eks/terragrunt.hcl
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
}
dependency 블록이 다른 Terragrunt 프로젝트의 출력을 자동으로 가져온다. VPC의 출력을 수동으로 복사하거나 데이터 소스로 가져올 필요가 없다.
run-all 명령
# 모든 하위 프로젝트를 의존성 순서대로 apply
terragrunt run-all apply
# 특정 환경 전체
cd envs/dev
terragrunt run-all apply
Terragrunt가 DAG를 계산해서 VPC → EKS → 앱 순서로 자동 apply한다.
언제 Terragrunt를 도입할까
- 환경이 2개 이하고 단순하면 순수 Terraform으로 충분
- 환경이 3개 이상이거나 컴포넌트가 10개 넘게 늘어나면 Terragrunt 검토
- 멀티 계정 구조(환경별 다른 AWS 계정)라면 Terragrunt가 확실히 유리
- 러닝커브가 있으므로 팀 전체 학습 비용도 고려
전략 비교
지금까지의 세 가지 전략을 정리하면 이렇다.
| 항목 | workspace | 디렉토리 분리 | Terragrunt |
|---|---|---|---|
| state 분리 | 같은 백엔드 내 경로 분리 | 완전 독립 백엔드 | 완전 독립 백엔드 |
| 멀티 AWS 계정 | 어려움 | 가능 | 가능, 쉬움 |
| 코드 중복 | 적음 | 많음 (환경 수만큼) | 적음 |
| 백엔드 설정 | 한 번 | 환경별 반복 | 한 번 |
| 러닝커브 | 낮음 | 낮음 | 중간 |
| 의존성 자동화 | 없음 | 없음 | 있음 |
| 대규모 적합성 | 낮음 | 중간 | 높음 |
실전 권장 조합
- 1인 또는 2~3명 소규모: 순수 Terraform + 디렉토리 분리로 시작
- 중규모 팀, 환경 2~3개, 컴포넌트 5~10개: 디렉토리 분리 + 모듈화
- 대규모 팀 또는 멀티 계정: Terragrunt 도입 검토
어느 쪽을 택하든 prod state는 반드시 완전히 분리된 백엔드에 둔다. 워크스페이스로 prod를 분리하는 건 장기적으로 리스크가 크다.
CI/CD와의 조합
환경 분리는 CI/CD와도 맞물린다. 전형적인 파이프라인은 이렇다.
flowchart LR
PR["PR 생성"] --> Plan["terraform plan\n(모든 환경)"]
Plan --> Review["리뷰 & 승인"]
Review --> Merge["main 머지"]
Merge --> Dev["dev auto-apply"]
Dev --> Stg["staging auto-apply"]
Stg --> Manual["prod 수동 승인"]
Manual --> Prod["prod apply"]
PR 시점에 모든 환경의 plan 결과를 코멘트로 보여주고, 머지되면 dev와 staging은 자동 apply, prod는 수동 승인 단계를 거치는 흐름이다. 디렉토리 기반 분리라면 각 환경 디렉토리에서 개별적으로 Terraform을 돌리면 되고, Terragrunt라면 run-all로 한 번에 처리할 수 있다.
이 파이프라인은 다음 편(CI/CD 통합)에서 자세히 다룬다.
환경 분리는 “코드 중복 최소화”와 “state 독립성”이 정반대 방향으로 당기는 균형점이다. 팀 규모와 환경 복잡도에 맞춰 적절한 지점을 고른다. 확실한 건, prod는 반드시 독립된 state를 가져야 한다는 것이다.
다음 편에서는 Kubernetes와 Helm 프로바이더를 다룬다. Terraform으로 클러스터 내부 리소스까지 관리할지, ArgoCD에 맡길지의 선택 기준도 함께 살펴본다.


Loading comments...