들어가며
인프라를 수동으로 관리하면 환경 간 불일치, 변경 이력 추적 불가, 재현 불가능한 설정 등 수많은 문제에 부딪힙니다. Infrastructure as Code(IaC)는 인프라를 코드로 정의하여 버전 관리, 리뷰, 자동화를 가능하게 합니다.
Terraform은 HashiCorp에서 개발한 IaC 도구로, 선언적 문법으로 클라우드 인프라를 정의하고 프로비저닝합니다. AWS, GCP, Azure 등 다양한 프로바이더를 지원하며, 현재 IaC 도구의 사실상 표준입니다.
이 글에서는 Terraform의 핵심 개념부터 AWS 실전 인프라 구축 예제까지, 백엔드 개발자가 알아야 할 Terraform 활용법을 정리합니다.
Terraform 핵심 개념
주요 용어
| 개념 | 설명 |
|---|---|
| Provider | 인프라 플랫폼과의 인터페이스 (AWS, GCP, Azure 등) |
| Resource | 생성할 인프라 요소 (EC2, RDS, S3 등) |
| Data Source | 기존 인프라 정보를 읽어오는 참조 |
| State | 현재 인프라 상태를 추적하는 JSON 파일 |
| Module | 재사용 가능한 Terraform 구성 묶음 |
| Variable | 외부에서 주입하는 입력값 |
| Output | 다른 모듈이나 사용자에게 노출하는 출력값 |
Terraform 워크플로우
# 1. 초기화 - 프로바이더 플러그인 다운로드
terraform init
# 2. 계획 - 변경 사항 미리보기 (실제 변경 없음)
terraform plan
# 3. 적용 - 인프라 변경 실행
terraform apply
# 4. 상태 확인
terraform show
terraform state list
# 5. 인프라 삭제
terraform destroy
HCL 문법 기초
Terraform은 HCL(HashiCorp Configuration Language)을 사용합니다.
Provider 설정
# versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
# 원격 상태 관리 (팀 협업 시 필수)
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "ap-northeast-2"
dynamodb_table = "terraform-lock"
encrypt = true
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = var.project_name
}
}
}
변수(Variable) 정의
# variables.tf
variable "aws_region" {
description = "AWS Region"
type = string
default = "ap-northeast-2"
}
variable "environment" {
description = "Environment name (dev, staging, prod)"
type = string
validation {
condition = contains(["dev", "staging", "prod"], var.environment)
error_message = "Environment must be dev, staging, or prod."
}
}
variable "vpc_cidr" {
description = "VPC CIDR block"
type = string
default = "10.0.0.0/16"
}
variable "db_password" {
description = "Database master password"
type = string
sensitive = true # plan/apply 출력에서 마스킹
}
Output 정의
# outputs.tf
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "rds_endpoint" {
description = "RDS endpoint"
value = aws_db_instance.main.endpoint
}
output "alb_dns_name" {
description = "ALB DNS name"
value = aws_lb.main.dns_name
}
AWS VPC 프로비저닝
프로덕션 인프라의 기반이 되는 VPC를 구성합니다.
# vpc.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.project_name}-${var.environment}-vpc"
}
}
# 퍼블릭 서브넷 (2개 AZ)
resource "aws_subnet" "public" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.project_name}-public-${count.index + 1}"
Type = "Public"
}
}
# 프라이빗 서브넷 (2개 AZ)
resource "aws_subnet" "private" {
count = 2
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + 10)
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.project_name}-private-${count.index + 1}"
Type = "Private"
}
}
# 인터넷 게이트웨이
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project_name}-igw"
}
}
# NAT 게이트웨이 (프라이빗 서브넷의 외부 통신용)
resource "aws_eip" "nat" {
domain = "vpc"
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.public[0].id
tags = {
Name = "${var.project_name}-nat"
}
}
# 라우팅 테이블
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.project_name}-public-rt"
}
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main.id
}
tags = {
Name = "${var.project_name}-private-rt"
}
}
# 서브넷-라우팅테이블 연결
resource "aws_route_table_association" "public" {
count = 2
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
count = 2
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private.id
}
# 가용 영역 조회
data "aws_availability_zones" "available" {
state = "available"
}
EC2 인스턴스와 보안 그룹
# security_groups.tf
resource "aws_security_group" "app" {
name_prefix = "${var.project_name}-app-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
lifecycle {
create_before_destroy = true
}
}
# ec2.tf
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
}
resource "aws_instance" "app" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = aws_subnet.private[0].id
vpc_security_group_ids = [aws_security_group.app.id]
iam_instance_profile = aws_iam_instance_profile.app.name
root_block_device {
volume_type = "gp3"
volume_size = 30
encrypted = true
}
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y java-17-amazon-corretto
# 애플리케이션 배포 스크립트
EOF
tags = {
Name = "${var.project_name}-app"
}
}
RDS 데이터베이스
# rds.tf
resource "aws_security_group" "rds" {
name_prefix = "${var.project_name}-rds-"
vpc_id = aws_vpc.main.id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
}
resource "aws_db_subnet_group" "main" {
name = "${var.project_name}-db-subnet"
subnet_ids = aws_subnet.private[*].id
}
resource "aws_db_instance" "main" {
identifier = "${var.project_name}-${var.environment}-db"
engine = "mysql"
engine_version = "8.0"
instance_class = var.db_instance_class
allocated_storage = 20
max_allocated_storage = 100 # 자동 스토리지 스케일링
storage_type = "gp3"
storage_encrypted = true
db_name = var.db_name
username = var.db_username
password = var.db_password
vpc_security_group_ids = [aws_security_group.rds.id]
db_subnet_group_name = aws_db_subnet_group.main.name
multi_az = var.environment == "prod" ? true : false
skip_final_snapshot = var.environment != "prod"
backup_retention_period = var.environment == "prod" ? 7 : 1
backup_window = "03:00-04:00"
maintenance_window = "Mon:04:00-Mon:05:00"
parameter_group_name = aws_db_parameter_group.main.name
tags = {
Name = "${var.project_name}-db"
}
}
resource "aws_db_parameter_group" "main" {
family = "mysql8.0"
name = "${var.project_name}-mysql-params"
parameter {
name = "character_set_server"
value = "utf8mb4"
}
parameter {
name = "collation_server"
value = "utf8mb4_unicode_ci"
}
parameter {
name = "slow_query_log"
value = "1"
}
parameter {
name = "long_query_time"
value = "1"
}
}
S3 버킷
# s3.tf
resource "aws_s3_bucket" "app_storage" {
bucket = "${var.project_name}-${var.environment}-storage"
}
resource "aws_s3_bucket_versioning" "app_storage" {
bucket = aws_s3_bucket.app_storage.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "app_storage" {
bucket = aws_s3_bucket.app_storage.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "app_storage" {
bucket = aws_s3_bucket.app_storage.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_lifecycle_configuration" "app_storage" {
bucket = aws_s3_bucket.app_storage.id
rule {
id = "archive-old-objects"
status = "Enabled"
transition {
days = 90
storage_class = "STANDARD_IA"
}
transition {
days = 180
storage_class = "GLACIER"
}
expiration {
days = 365
}
}
}
모듈화
재사용 가능한 모듈로 인프라 코드를 구조화합니다.
프로젝트 디렉토리 구조
terraform/
├── modules/
│ ├── vpc/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── rds/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ └── ec2/
│ ├── main.tf
│ ├── variables.tf
│ └── outputs.tf
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ ├── staging/
│ │ ├── main.tf
│ │ ├── terraform.tfvars
│ │ └── backend.tf
│ └── prod/
│ ├── main.tf
│ ├── terraform.tfvars
│ └── backend.tf
└── modules.tf
모듈 사용 예제
# environments/prod/main.tf
module "vpc" {
source = "../../modules/vpc"
project_name = var.project_name
environment = "prod"
vpc_cidr = "10.0.0.0/16"
}
module "rds" {
source = "../../modules/rds"
project_name = var.project_name
environment = "prod"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
instance_class = "db.r6g.large"
db_password = var.db_password
app_sg_id = module.ec2.security_group_id
}
module "ec2" {
source = "../../modules/ec2"
project_name = var.project_name
environment = "prod"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnet_ids
instance_type = "t3.large"
instance_count = 3
}
환경별 tfvars
# environments/dev/terraform.tfvars
project_name = "myapp"
environment = "dev"
instance_type = "t3.small"
db_instance_class = "db.t3.micro"
# environments/prod/terraform.tfvars
project_name = "myapp"
environment = "prod"
instance_type = "t3.large"
db_instance_class = "db.r6g.large"
상태 관리와 팀 협업
Terraform의 State 파일은 현재 인프라 상태를 추적하는 핵심 요소입니다. 팀에서 협업할 때는 반드시 원격 백엔드를 사용해야 합니다.
S3 + DynamoDB Backend 구성
# State 저장용 S3 버킷과 Lock용 DynamoDB (최초 한 번 수동 생성)
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state-bucket"
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_dynamodb_table" "terraform_lock" {
name = "terraform-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
유용한 Terraform 명령어
# 상태 조회
terraform state list
terraform state show aws_instance.app
# 특정 리소스만 적용
terraform apply -target=aws_instance.app
# 코드 포맷팅
terraform fmt -recursive
# 문법 검증
terraform validate
# Plan 결과 파일로 저장 후 적용
terraform plan -out=plan.tfplan
terraform apply plan.tfplan
# 리소스를 State에서 제거 (실제 인프라는 유지)
terraform state rm aws_instance.app
# 기존 인프라를 State로 가져오기
terraform import aws_instance.app i-1234567890abcdef0
마치며
Terraform을 활용한 Infrastructure as Code는 현대 백엔드 개발에서 필수적인 역량이 되었습니다. 이 글에서 다룬 내용을 정리하면:
- HCL 문법으로 Provider, Resource, Variable, Output을 선언적으로 정의합니다
- terraform plan/apply 워크플로우로 안전하게 인프라를 변경합니다
- AWS VPC, EC2, RDS, S3 등 핵심 리소스를 코드로 프로비저닝합니다
- 모듈화와 환경 분리로 재사용 가능하고 일관된 인프라를 유지합니다
- S3 Backend로 팀 협업 시 안전한 상태 관리를 보장합니다
처음에는 AWS 콘솔이 더 편하게 느껴질 수 있지만, 인프라 규모가 커지고 환경이 늘어나면 Terraform의 가치를 체감하게 됩니다. 작은 프로젝트부터 시작해서 점진적으로 범위를 넓혀가는 것을 추천합니다.
'DevOps' 카테고리의 다른 글
| Docker Compose 실전 - Spring Boot + DB + Redis + Kafka 개발 환경 구축 (0) | 2026.04.14 |
|---|---|
| Prometheus + Grafana 실전 구축 - Spring Boot 모니터링 완벽 가이드 (2) | 2026.04.09 |
| Kubernetes 실전 운영 - Helm, Ingress, HPA 오토스케일링 (0) | 2026.04.07 |
| Kubernetes 입문 - Pod, Service, Deployment 핵심 개념 총정리 (0) | 2026.04.06 |
| Terraform으로 NCP(네이버 클라우드) 인프라 구축하기 - VPC부터 서버 배포까지 (0) | 2026.04.03 |