DevOps

Terraform으로 인프라 코드화 - AWS 실전 예제로 배우는 IaC

백엔드 개발자 김승원 2026. 4. 7. 12:20

들어가며

인프라를 수동으로 관리하면 환경 간 불일치, 변경 이력 추적 불가, 재현 불가능한 설정 등 수많은 문제에 부딪힙니다. 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의 가치를 체감하게 됩니다. 작은 프로젝트부터 시작해서 점진적으로 범위를 넓혀가는 것을 추천합니다.