Terraform 환경 분리가 중요한 이유
인프라를 코드로 관리할 때 가장 먼저 부딪히는 문제는 dev/staging/production 환경 분리입니다. 같은 인프라 코드를 여러 환경에 배포하되, 각 환경의 설정(인스턴스 크기, 레플리카 수, 도메인 등)은 달라야 합니다. Terraform은 Workspace, tfvars 파일, 디렉터리 분리 등 다양한 전략을 제공하지만, 잘못 선택하면 운영 사고로 이어집니다.
이 글에서는 Workspace의 한계, 디렉터리 기반 환경 분리, Terragrunt DRY 패턴, State 격리 전략, CI/CD 파이프라인 통합까지 실무에서 검증된 패턴을 다룹니다.
Terraform Workspace: 기본과 한계
Workspace는 같은 코드로 여러 State를 관리하는 내장 기능입니다.
# Workspace 생성 및 전환
terraform workspace new dev
terraform workspace new staging
terraform workspace new prod
terraform workspace select dev
terraform workspace list
# 코드에서 workspace 참조
locals {
env = terraform.workspace
instance_type = {
dev = "t3.small"
staging = "t3.medium"
prod = "m5.xlarge"
}
replica_count = {
dev = 1
staging = 2
prod = 3
}
}
resource "aws_instance" "app" {
instance_type = local.instance_type[local.env]
ami = var.ami_id
tags = {
Name = "app-${local.env}"
Environment = local.env
}
}
resource "aws_rds_cluster" "main" {
cluster_identifier = "db-${local.env}"
instance_count = local.replica_count[local.env]
# prod만 Multi-AZ
availability_zones = local.env == "prod" ? ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"] : ["ap-northeast-2a"]
}
Workspace의 치명적 한계
| 문제 | 설명 | 위험도 |
|---|---|---|
| State 동일 백엔드 | 모든 환경이 같은 S3 버킷에 저장, 접근 권한 분리 어려움 | 높음 |
| workspace 착각 사고 | prod에서 작업 중인지 인지하기 어려움 → 실수로 destroy | 치명적 |
| 환경별 프로바이더 차이 | dev는 AWS, prod는 GCP 같은 구조 불가 | 중간 |
| 모듈 버전 고정 어려움 | prod는 안정 버전, dev는 최신 버전 적용 불가 | 중간 |
결론: Workspace는 소규모 프로젝트나 개인 개발 환경에만 적합합니다. 프로덕션급 환경 분리에는 디렉터리 기반 또는 Terragrunt를 권장합니다.
디렉터리 기반 환경 분리
가장 안전하고 명확한 패턴입니다. 환경별로 완전히 독립된 State와 백엔드를 가집니다.
infrastructure/
├── modules/ # 공유 모듈
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── ecs-service/
│ └── rds/
├── environments/
│ ├── dev/
│ │ ├── main.tf # 모듈 호출
│ │ ├── backend.tf # dev 전용 State 백엔드
│ │ ├── variables.tf
│ │ ├── terraform.tfvars # dev 설정값
│ │ └── providers.tf # dev AWS 계정
│ ├── staging/
│ │ ├── main.tf
│ │ ├── backend.tf
│ │ ├── terraform.tfvars
│ │ └── providers.tf
│ └── prod/
│ ├── main.tf
│ ├── backend.tf # prod 전용 별도 계정
│ ├── terraform.tfvars
│ └── providers.tf
└── global/ # 환경 공통 (IAM, DNS 등)
├── iam/
└── route53/
환경별 백엔드 분리
# environments/dev/backend.tf
terraform {
backend "s3" {
bucket = "mycompany-terraform-dev"
key = "infrastructure/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-lock-dev"
encrypt = true
# dev AWS 계정의 S3
role_arn = "arn:aws:iam::111111111111:role/terraform-dev"
}
}
# environments/prod/backend.tf
terraform {
backend "s3" {
bucket = "mycompany-terraform-prod"
key = "infrastructure/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-lock-prod"
encrypt = true
# prod 전용 AWS 계정 — 완전 격리
role_arn = "arn:aws:iam::999999999999:role/terraform-prod"
}
}
모듈 호출 패턴
# environments/prod/main.tf
module "vpc" {
source = "../../modules/vpc"
environment = "prod"
vpc_cidr = var.vpc_cidr
azs = var.availability_zones
enable_nat = true
nat_gateway_count = 3 # prod는 AZ별 NAT
}
module "api_service" {
source = "../../modules/ecs-service"
environment = "prod"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
desired_count = var.api_desired_count
cpu = var.api_cpu
memory = var.api_memory
# prod 전용: Auto Scaling 활성화
enable_autoscaling = true
min_capacity = 3
max_capacity = 20
}
# environments/prod/terraform.tfvars
vpc_cidr = "10.0.0.0/16"
availability_zones = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
api_desired_count = 3
api_cpu = 1024
api_memory = 2048
Terragrunt으로 DRY 환경 관리
디렉터리 기반의 단점은 환경 간 코드 중복입니다. Terragrunt는 이를 해결하는 래퍼 도구로, Terraform State 관리 심화에서 다뤘던 State 관리와 결합하면 강력한 환경 관리 체계를 구축할 수 있습니다.
# 디렉터리 구조
infrastructure/
├── modules/
│ ├── vpc/
│ └── ecs-service/
├── terragrunt.hcl # 루트: 공통 설정
├── dev/
│ ├── env.hcl # dev 환경 변수
│ ├── vpc/
│ │ └── terragrunt.hcl
│ └── ecs-service/
│ └── terragrunt.hcl
├── staging/
│ ├── env.hcl
│ ├── vpc/
│ │ └── terragrunt.hcl
│ └── ecs-service/
│ └── terragrunt.hcl
└── prod/
├── env.hcl
├── vpc/
│ └── terragrunt.hcl
└── ecs-service/
└── terragrunt.hcl
# 루트 terragrunt.hcl — 백엔드 자동 생성
remote_state {
backend = "s3"
generate = {
path = "backend.tf"
if_exists = "overwrite_terragrunt"
}
config = {
bucket = "mycompany-tf-${local.env}"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "tf-lock-${local.env}"
}
}
locals {
env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
env = local.env_vars.locals.environment
}
# 공통 프로바이더
generate "provider" {
path = "provider.tf"
if_exists = "overwrite_terragrunt"
contents = <<EOF
provider "aws" {
region = "ap-northeast-2"
default_tags {
tags = {
Environment = "${local.env}"
ManagedBy = "terraform"
}
}
}
EOF
}
# prod/env.hcl
locals {
environment = "prod"
account_id = "999999999999"
}
# prod/vpc/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../modules/vpc"
}
inputs = {
environment = "prod"
vpc_cidr = "10.0.0.0/16"
availability_zones = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
enable_nat = true
nat_gateway_count = 3
}
# prod/ecs-service/terragrunt.hcl
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "../../modules/ecs-service"
}
dependency "vpc" {
config_path = "../vpc"
}
inputs = {
environment = "prod"
vpc_id = dependency.vpc.outputs.vpc_id
subnet_ids = dependency.vpc.outputs.private_subnet_ids
desired_count = 3
cpu = 1024
memory = 2048
}
Terragrunt의 dependency 블록으로 모듈 간 의존 관계를 선언하고, terragrunt run-all apply로 전체 환경을 올바른 순서로 배포할 수 있습니다.
CI/CD 파이프라인 통합
# .github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
paths: ['infrastructure/**']
pull_request:
paths: ['infrastructure/**']
jobs:
plan:
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, prod]
environment: ${{ matrix.environment }}
steps:
- uses: actions/checkout@v4
- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets[format('AWS_ROLE_{0}', matrix.environment)] }}
aws-region: ap-northeast-2
- name: Terraform Plan
working-directory: infrastructure/environments/${{ matrix.environment }}
run: |
terraform init
terraform plan -out=tfplan -detailed-exitcode
continue-on-error: true
- name: Comment Plan on PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const plan = '${{ steps.plan.outputs.stdout }}'
github.rest.issues.createComment({
issue_number: context.issue.number,
body: `### ${{ matrix.environment }} Plann```n${plan}n````
})
apply:
needs: plan
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
strategy:
max-parallel: 1 # 순차 배포: dev → staging → prod
matrix:
environment: [dev, staging, prod]
environment:
name: ${{ matrix.environment }}
# prod는 수동 승인 필요 (GitHub Environment Protection Rules)
steps:
- uses: actions/checkout@v4
- name: Terraform Apply
working-directory: infrastructure/environments/${{ matrix.environment }}
run: |
terraform init
terraform apply -auto-approve
환경 분리 안전장치
# 1. prevent_destroy로 프로덕션 리소스 보호
resource "aws_rds_cluster" "main" {
lifecycle {
prevent_destroy = true # terraform destroy 차단
}
}
# 2. 프로덕션 삭제 방지 변수
variable "enable_deletion_protection" {
type = bool
default = true # prod에서는 항상 true
}
resource "aws_rds_cluster" "main" {
deletion_protection = var.enable_deletion_protection
}
# 3. precondition으로 환경 검증
resource "aws_instance" "expensive" {
instance_type = var.instance_type
lifecycle {
precondition {
condition = !(var.environment == "dev" && can(regex("^(m5|c5|r5)\.(2x|4x|8x|12x|16x|24x)?large", var.instance_type)))
error_message = "dev 환경에서 고비용 인스턴스 사용 금지"
}
}
}
# 4. Sentinel/OPA 정책 (Terraform Cloud)
# policy.sentinel
import "tfplan/v2" as tfplan
main = rule {
all tfplan.resource_changes as _, rc {
rc.type is "aws_instance" and
rc.change.after.instance_type not in ["m5.24xlarge", "c5.24xlarge"]
}
}
Ansible Playbook 자동화 심화와 결합하면, Terraform으로 인프라를 프로비저닝하고 Ansible로 설정 관리하는 완전한 IaC 파이프라인을 구축할 수 있습니다.
전략별 비교 정리
| 전략 | State 격리 | 코드 중복 | 복잡도 | 권장 규모 |
|---|---|---|---|---|
| Workspace | 같은 백엔드 | 없음 | 낮음 | 개인/소규모 |
| tfvars 분리 | 같은 백엔드 | 낮음 | 낮음 | 소규모 팀 |
| 디렉터리 분리 | 완전 격리 | 중간 | 중간 | 중규모 이상 |
| Terragrunt | 완전 격리 | 최소 | 높음 | 대규모/멀티계정 |
마무리
Terraform 환경 분리의 핵심은 State 격리와 실수 방지입니다. Workspace는 편리하지만 프로덕션에는 위험하고, 디렉터리 분리는 안전하지만 코드가 중복되며, Terragrunt는 DRY하지만 학습 곡선이 있습니다. 팀 규모와 인프라 복잡도에 맞는 전략을 선택하되, prevent_destroy와 CI/CD 승인 게이트는 어떤 전략이든 필수입니다.