수동으로 AWS 콘솔을 클릭해 인프라를 구성하던 시대는 끝났습니다. 클라우드 리소스가 수십, 수백 개로 늘어날수록 클릭 기반 관리의 한계는 명확해집니다. 설정 드리프트, 재현 불가능한 환경, 팀원 간 불일치. 이런 문제를 해결하기 위해 등장한 것이 IaC(Infrastructure as Code)이며, 그 중 가장 광범위하게 쓰이는 도구가 HashiCorp의 Terraform입니다. 이 글에서는 Terraform의 핵심 개념부터 실무에서 바로 적용 가능한 패턴까지 단계별로 살펴봅니다.
Terraform이란 무엇이고 왜 쓰는가
Terraform은 HCL(HashiCorp Configuration Language)로 클라우드 인프라를 선언적으로 기술하고, plan → apply 워크플로로 실제 리소스를 프로비저닝하는 오픈소스 도구입니다. AWS, GCP, Azure는 물론 Kubernetes, Datadog, GitHub까지 3,000개 이상의 프로바이더를 지원합니다.
핵심 철학은 “원하는 상태(desired state)를 선언하면 Terraform이 현재 상태와 비교해 필요한 변경만 수행한다”는 것입니다. 이를 통해 얻는 이점은 다음과 같습니다.
- 코드 리뷰로 인프라 변경을 감사할 수 있음
- Git 히스토리로 변경 이력 추적 가능
- 동일한 코드로 dev/staging/prod 환경을 일관되게 재현
- 팀 협업 시 설정 드리프트(configuration drift) 방지
| 도구 | 언어 | 접근 방식 | 멀티클라우드 | 상태 관리 |
|---|---|---|---|---|
| Terraform | HCL | 선언적 | 지원 (3,000+ 프로바이더) | state 파일 |
| AWS CloudFormation | YAML/JSON | 선언적 | AWS 전용 | 스택 내장 |
| Pulumi | Python/TS/Go 등 | 명령형/선언적 혼합 | 지원 | state 서비스 |
| Ansible | YAML | 명령형 | 지원 | 없음 (멱등성 직접 구현) |
Terraform이 CloudFormation보다 선호되는 이유는 멀티클라우드 지원과 풍부한 생태계입니다. 단, 오픈소스(MPL) 외에 엔터프라이즈 기능은 Terraform Cloud/Enterprise를 통해 유료로 제공되며, 2023년 이후 라이선스가 BSL(Business Source License)로 변경된 점은 알아둘 필요가 있습니다. 대안으로 OpenTofu가 활발히 성장 중입니다.
HCL 기초: 첫 번째 리소스 작성하기
Terraform 코드는 .tf 파일로 작성합니다. 가장 기본적인 구조는 프로바이더 선언, 리소스 정의, 변수와 출력값으로 구성됩니다.
# main.tf — AWS EC2 인스턴스 + 보안그룹 기본 예시
terraform {
required_version = ">= 1.5"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
variable "aws_region" {
description = "배포 리전"
type = string
default = "ap-northeast-2"
}
variable "instance_type" {
description = "EC2 인스턴스 타입"
type = string
default = "t3.micro"
}
resource "aws_security_group" "web" {
name = "web-sg"
description = "HTTP/HTTPS 및 SSH 허용"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "web" {
ami = "ami-0c9c942bd7bf113a2" # Amazon Linux 2023, ap-northeast-2
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.web.id]
tags = {
Name = "web-server"
Environment = "dev"
ManagedBy = "terraform"
}
}
output "instance_public_ip" {
description = "EC2 퍼블릭 IP"
value = aws_instance.web.public_ip
}
HCL의 핵심 블록 타입은 다음과 같습니다. terraform은 버전 제약을 선언하고, provider는 AWS/GCP 등 클라우드를 연결하며, resource는 실제 만들 인프라 객체입니다. variable로 재사용성을 높이고 output으로 프로비저닝 결과를 노출합니다.
plan/apply 워크플로는 다음 순서로 실행합니다.
# 1. 초기화: 프로바이더 플러그인 다운로드
terraform init
# 2. 실행 계획 미리보기 (실제 변경 없음)
terraform plan -out=tfplan
# 3. 계획 검토 후 적용
terraform apply tfplan
# 4. 특정 리소스만 삭제하려면
terraform destroy -target=aws_instance.web
# 5. 현재 상태 조회
terraform show
terraform state list
plan 단계에서 +(추가), ~(변경), -(삭제) 기호를 반드시 확인하고 팀 리뷰를 거친 뒤 apply하는 습관이 중요합니다.
원격 State 백엔드: 팀 협업의 핵심
Terraform은 실제 인프라와 코드의 매핑 정보를 state 파일(terraform.tfstate)에 저장합니다. 로컬에만 두면 두 가지 치명적인 문제가 발생합니다. 첫째, 팀원 간 state가 분기되어 충돌이 생깁니다. 둘째, state 파일에는 DB 패스워드 같은 민감 정보가 평문으로 포함될 수 있어 Git에 커밋하면 안 됩니다.
해결책은 원격 백엔드입니다. AWS S3 + DynamoDB 조합이 가장 일반적입니다.
# backend.tf — S3 원격 백엔드 + DynamoDB 잠금
terraform {
backend "s3" {
bucket = "my-company-tfstate"
key = "prod/web/terraform.tfstate"
region = "ap-northeast-2"
encrypt = true
dynamodb_table = "terraform-lock" # 동시 apply 방지용 분산 잠금
}
}
# S3 버킷과 DynamoDB 테이블은 별도 bootstrap 코드로 미리 생성
# (닭이 먼저냐 달걀이 먼저냐 문제 → 수동 생성 또는 별도 bootstrapping 모듈 활용)
resource "aws_s3_bucket" "tfstate" {
bucket = "my-company-tfstate"
lifecycle {
prevent_destroy = true # 실수로 state 버킷 삭제 방지
}
}
resource "aws_s3_bucket_versioning" "tfstate" {
bucket = aws_s3_bucket.tfstate.id
versioning_configuration {
status = "Enabled" # 실수로 state 덮어씌운 경우 복구 가능
}
}
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
DynamoDB 잠금 테이블이 없으면 두 엔지니어가 동시에 apply를 실행할 때 state가 충돌해 리소스가 이중으로 생성되거나 삭제될 수 있습니다. 실무에서 이 잠금을 빠뜨려 장애가 난 사례가 적지 않습니다.
모듈화: 재사용 가능한 인프라 구성요소 만들기
코드가 커질수록 중복이 늘어납니다. dev와 prod 환경에 동일한 VPC + ECS 조합을 반복해서 작성하는 것은 낭비입니다. Terraform 모듈은 리소스 묶음을 재사용 가능한 단위로 캡슐화합니다.
# 디렉토리 구조 예시
.
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── ecs-service/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── environments/
│ ├── dev/
│ │ ├── main.tf # 모듈 호출
│ │ ├── backend.tf
│ │ └── terraform.tfvars
│ └── prod/
│ ├── main.tf
│ ├── backend.tf
│ └── terraform.tfvars
└── README.md
# environments/prod/main.tf — 모듈 호출 예시
module "vpc" {
source = "../../modules/vpc"
cidr_block = "10.0.0.0/16"
availability_zones = ["ap-northeast-2a", "ap-northeast-2c"]
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.10.0/24", "10.0.20.0/24"]
environment = "prod"
}
module "api_service" {
source = "../../modules/ecs-service"
cluster_id = aws_ecs_cluster.main.id
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
container_image = "123456789.dkr.ecr.ap-northeast-2.amazonaws.com/api:latest"
desired_count = 3
cpu = 512
memory = 1024
environment = "prod"
}
공개 Terraform Registry(registry.terraform.io)에는 AWS VPC, EKS, RDS 등 검증된 공식 모듈이 있습니다. 처음부터 모두 직접 작성하기보다 공식 모듈을 래퍼로 감싸 조직 정책(태깅 규칙, 보안 설정)을 얹는 방식이 실용적입니다.
흔한 실수와 실전 팁
Terraform을 처음 도입할 때 팀이 자주 저지르는 실수와 그 대응 방법을 정리합니다.
- state 파일을 Git에 커밋 —
.gitignore에*.tfstate,*.tfstate.backup,.terraform/를 반드시 추가하세요. 민감 정보가 유출됩니다. - 프로바이더 버전 고정 미흡 —
~> 5.0처럼 마이너 버전까지 고정하지 않으면init시점에 따라 다른 버전이 설치되어 재현성이 깨집니다. - plan 없이 apply 실행 — CI/CD 파이프라인에서도
plan -out으로 결과를 저장하고 승인 후apply하는 2단계를 지키세요. - 하드코딩된 AMI ID — AMI ID는 리전마다 다릅니다.
data "aws_ami"데이터 소스로 동적으로 조회하세요. - 대형 monolithic 루트 모듈 — 리소스가 100개를 넘어가면
plan이 수분 걸리고 한 번의 실수가 전체에 영향을 줍니다. 환경별, 서비스별로 상태를 분리하세요. - terraform fmt/validate 생략 — PR 전에 반드시
terraform fmt -recursive,terraform validate를 실행하거나 pre-commit hook으로 자동화하세요.
보안 측면에서는 tfsec, Checkov 같은 정적 분석 도구를 CI에 통합해 보안 그룹 과잉 허용, 암호화 미적용 등을 자동으로 잡아내는 것이 좋습니다.
마무리
Terraform은 진입 장벽이 낮지 않습니다. HCL 문법, 상태 관리, 모듈 설계까지 익혀야 할 것이 많습니다. 하지만 일단 팀 전체가 IaC 방식으로 전환하면 인프라 변경의 가시성, 재현성, 협업 효율이 크게 향상됩니다. 소규모 프로젝트라면 단일 루트 모듈로 시작해 복잡해질 때 모듈화하고, 팀이 늘어날 때 Terraform Cloud나 Atlantis 같은 CI 기반 자동화를 도입하는 순서를 권장합니다.
자주 묻는 질문
Q. Terraform state 파일을 실수로 삭제했을 때 복구할 수 있나요?
A. S3 백엔드에 버전 관리를 활성화해두었다면 이전 버전을 복원할 수 있습니다. 복원 후 terraform plan을 실행해 현재 인프라와 차이가 없는지 반드시 확인하세요. 로컬 state만 있었다면 terraform import로 기존 리소스를 state에 다시 등록해야 합니다.
Q. 이미 수동으로 만든 AWS 리소스를 Terraform으로 가져올 수 있나요?
A. terraform import <resource_type>.<name> <id> 명령어로 기존 리소스를 state에 가져올 수 있습니다. 단, HCL 코드는 자동으로 생성되지 않으므로 직접 작성 후 plan을 실행해 드리프트를 제거해야 합니다. Terraform 1.5부터는 import 블록을 코드로 선언하는 방식도 지원합니다.
Q. Terraform Cloud와 Atlantis 중 어느 것을 선택해야 하나요?
A. 팀 규모가 작고 인프라가 단순하다면 무료 플랜이 있는 Terraform Cloud가 빠른 출발점입니다. 반면 GitLab/GitHub Enterprise 사내 환경을 사용하거나 state를 자체 관리해야 하는 컴플라이언스 요건이 있다면 셀프호스팅 가능한 Atlantis가 더 적합합니다.