Terraform은 정상적으로 동작하다가도 예기치 못한 문제들(아래 사례들)가 발생할 수 있으며, 이때 인프라 구조의 중요성을 실감하게 됩니다.
- main.tf 파일이 수천 줄에 달해 복잡도가 증가
- 스테이징과 프로덕션 환경의 명확한 분리 부재
- 특정 변경이 다른 리소스나 리전에 영향을 줄 수 있다는 불안감
Terraform 코드를 보다 깔끔하고 재사용 가능하며 확장성있게 구성하기 위해서, 모듈 활용 방식, 워크스페이스의 적절한 사용 시점, 그리고 **실무에서 검증된 모범사례(Best Practices)**를 소개합니다.
프로젝트 환경이 소수이든 다수이든 이러한 구조화를 통해 안정성과 운영 효율성을 높일 수 있습니다.
핵심은 환경별·기능별 코드 분리를 통한 명확한 프로젝트 구조화입니다.
추천하는 전형적인 구조는 다음과 같습니다:
terraform-project/
│
├── modules/ # Reusable building blocks
│ ├── vpc/
│ ├── ec2/
│ └── rds/
│
├── envs/ # Separate configurations per environment
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── backend.tf
│ ├── staging/
│ └── prod/
│
├── global/ # Shared resources (e.g., IAM roles)
│ └── iam/
│ ├── main.tf
│ └── outputs.tf
│
└── README.md
프로젝트 구조화 목적
- modules/: 재사용 가능하고 매개변수화된 코드를 포함합니다. 로직을 DRY(Do not Repeat Yourself) 원칙에 맞게 유지해줍니다.
- envs/: 프로덕션, 스테이징, 개발을 완전히 분리합니다. - 서로 다른 상태 파일, 백엔드, 필요하다면 다른 프로바이더까지 사용합니다.
- global/: 환경 수명 주기와는 별도로 존재하는 리소스를 보관합니다 (예: 공유 S3 버킷, IAM 역할).
다이어그램
Terraform 프로젝트 레이아웃 - 디렉토리 구성은 프로젝트 전체를 명확히 구분하고, 환경 간 간섭을 최소화하며, 관리 효율성을 높이는 데 목적이 있습니다.
모듈 이해하기
단일 파일로 작성했더라도, Terraform 코드를 작성했다면 이미 모듈을 사용한 것입니다. 모듈은 인프라 정의(.tf 파일)들이 담긴 폴더를 의미합니다. 인프라 규모가 커질수록 커스텀 모듈의 도입이 필수입니다. (재사용·표준화·유지보수성 확보).
- 재사용성: VPC를 한 번 정의해 두고 여러 환경에서 활용할 수 있습니다.
- 가독성: 메인 파일을 짧고 명확하게 유지할 수 있습니다.
- 유지보수성: 한 곳에서 버그를 고치면, 모든 곳에 업데이트가 반영됩니다.
예시: 간단한 VPC 모듈 다음은 기본적인 커스텀 모듈 레이아웃입니다.
modules/
└── vpc/
├── main.tf
├── variables.tf
└── outputs.tf
main.tf
resource “aws_vpc” “main” {
cidr_block = var.cidr_block
tags = {
Name = var.name
}
}
variables.tf
variable “cidr_block” {}
variable “name” {}
outputs.tf
output “vpc_id” {
value = aws_vpc.main.id
}
envs/dev/main.tf)
module “vpc” {
source = “../../modules/vpc”
cidr_block = “10.0.0.0/16”
name = “dev-vpc”
}
모듈 사용을 피해야 할 경우
- 단일 사용 코드: 한 번만 사용할 리소스를 억지로 모듈화하지 않는다.
- 과도한 중첩: 모듈은 얕고 집중되게 유지해야 하며, 보통 모듈 하나당 리소스 그룹 하나를 두는 것이 바람직하다.
워크스페이스의 특징
- 워크스페이스는 동일한 코드로 여러 환경을 관리할 수 있도록, 활성화된 워크스페이스별로 상태 파일을 분리한다(dev, prod 등).
- 겉보기에는 편리해 보이나, 모든 환경 관리에 전적으로 의존하기에는 한계가 있다.
- 비유적으로, 워크스페이스는 “급할 때는 유용하지만, 전체 시스템을 맡기기에는 부족한 도구”에 가깝다.
워크스페이스 작동 방식
다음 명령어들을 실행해보세요:
terraform workspace list
terraform workspace new staging
terraform workspace select prod
Terraform은 선택된 워크스페이스에 따라 동일한 폴더 안에 서로 다른 terraform.tfstate 파일을 저장합니다.
워크스페이스의 장점
- 빠른 설정: 추가 구성이 거의 필요 없음
- 상태 분리: 워크스페이스별로
tfstate파일 관리 가능 - 실험 환경에 적합: test, dev, playground 등 로컬 실험 용도로 유용
워크스페이스의 단점
- 폴더 공유 문제: 모든 환경이 동일한 코드 디렉토리를 사용 → 작은 실수도 여러 환경으로 확산
- 백엔드 제한: 워크스페이스는 동일한 백엔드를 공유 → 상태 잠금 관리 실패 시 위험 증가
- 가시성 부족: 어느 환경에 무엇이 배포됐는지 파악이 어렵고, 특히 CI/CD 파이프라인에서 혼동 발생
더 나은 대안 : 폴더 기반 환경 분리
- 환경별로 디렉토리를 나누어 독립적으로 관리
- 이미 제안된 프로젝트 구조에서 활용되는 방식
envs/
├── dev/
├── staging/
└── prod/
각각은 고유한 파일을 가집니다:
- backend.tf
- main.tf
- 변수 파일들
- 완전히 분리된 상태(state)
이 방식은 더 명확하고, 더 잘 제어할 수 있으며, CI/CD 파이프라인도 깔끔하게 유지할 수 있습니다.
Terraform 모범 사례 - 경험에서 얻은 교훈
Terraform은 강력하지만, 올바른 습관 없이는 유지가 어렵습니다. 다음은 실무에서 꼭 지켜야 할 핵심 원칙과 흔히 빠지는 함정들입니다.
-
모듈은 필요할 때만 사용
- 두 곳 이상에서 재사용될 경우에만 모듈화
- 단일 리소스라면 굳이 모듈로 감싸지 말 것
-
환경은 반드시 분리
- 환경별로 독립적인 백엔드와 팀마다 상태 파일 사용
- 프로덕션은 절대 “default” 워크스페이스에 두지 말 것
-
일관된 네이밍 규칙
- 팀 차원에서 규칙을 정해 관리 (예:**
dev-web-0,prod-web-2)
- 팀 차원에서 규칙을 정해 관리 (예:**
resource “aws_instance” “web” {
tags = {
Name = “${var.env}-web-${count.index}”
}
dev-web-0, prod-web-2와 같은 일관된 네이밍 패턴은 **예측 가능한 혼돈(predictable chaos)**을 만듭니다.
- 프로바이더와 Terraform 버전 고정
- required_version과 required_providers를 명확히 지정해 업그레이드 리스크 최소화
terraform {
required_version = “~> 1.5.0”
required_providers {
aws = {
source = “hashicorp/aws”
version = “~> 5.0”
}
}
}
-
잠금 기능이 있는 원격 상태(Remote State) 사용하기 팀 단위 프로젝트에서는 AWS의 S3 + DynamoDB 조합이나 Terraform Cloud와 같은 원격 백엔드를 사용해야 합니다.
- 상태 파일(
terraform.tfstate)을 로컬 머신에 보관하는 것은 예기치 못한 문제를 초래할 수 있으므로, 프로덕션 환경에서는 절대 로컬 상태 저장을 피해야 합니다.
- 상태 파일(
-
CI/CD로 자동화하기 Terraform을 GitLab CI, GitHub Actions 등 사용하는 파이프라인에 통합하세요.
terraform fmt
terraform validate
terraform plan
- 하드코딩된 시크릿은 피하기
- 환경 변수, 시크릿 매니저(AWS Secrets Manager, Vault 등), 또는 Terraform의
sensitive = true속성을 활용하세요.
- 환경 변수, 시크릿 매니저(AWS Secrets Manager, Vault 등), 또는 Terraform의
절대 이렇게 하지 마세요:
variable “db_password” {
default = “hunter2”
}

