Skip to content

✍️ 필사 모드: Terraform & IaC 완전 가이드 — 인프라를 코드로 관리하는 모든 것

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

1. Infrastructure as Code란

왜 인프라를 코드로 관리하는가

전통적인 인프라 관리 방식은 관리자가 콘솔에 접속하여 수동으로 서버를 프로비저닝하고 네트워크를 설정하는 것이었습니다. 이 방식에는 여러 문제가 있습니다.

  • 재현 불가능: 동일한 환경을 다시 만들기 어렵습니다.
  • 변경 추적 불가: 누가, 언제, 무엇을 변경했는지 알 수 없습니다.
  • 확장의 한계: 서버 10대까지는 수동으로 관리할 수 있지만, 100대 이상은 사실상 불가능합니다.
  • 환경 불일치: 개발, 스테이징, 프로덕션 환경이 미묘하게 달라지는 Configuration Drift가 발생합니다.

Infrastructure as Code(IaC)는 인프라의 원하는 상태를 코드 파일로 정의하고, 도구가 자동으로 해당 상태를 실현하는 방법론입니다. 코드이므로 Git으로 버전 관리하고, 코드 리뷰를 거치며, CI/CD 파이프라인으로 배포할 수 있습니다.

IaC 도구 비교

도구언어접근 방식상태 관리클라우드 지원
TerraformHCL선언적자체 State 파일멀티 클라우드
CloudFormationJSON/YAML선언적AWS 관리AWS 전용
PulumiTypeScript/Python/Go명령형 + 선언적Pulumi Cloud멀티 클라우드
AWS CDKTypeScript/Python/Go명령형CloudFormation 스택AWS 전용
CrossplaneYAML선언적 (K8s CRD)K8s etcd멀티 클라우드

Terraform은 HCL이라는 전용 선언적 언어를 사용하며, 가장 넓은 프로바이더 생태계를 보유하고 있습니다. AWS, GCP, Azure는 물론 Datadog, PagerDuty, GitHub 같은 SaaS도 관리할 수 있습니다.

CloudFormation은 AWS 네이티브 도구로, AWS 서비스와의 통합이 가장 빠릅니다. 새로운 AWS 서비스 출시와 동시에 지원됩니다.

Pulumi는 범용 프로그래밍 언어를 사용하므로 IDE 자동 완성, 타입 검사, 유닛 테스트를 그대로 활용할 수 있습니다.

AWS CDK는 CloudFormation 위에 추상화 계층을 올린 도구로, L2/L3 Construct로 복잡한 패턴을 간결하게 표현합니다.

선언적 vs 명령적 접근

IaC 도구는 크게 두 가지 접근 방식으로 나뉩니다.

선언적(Declarative): 원하는 최종 상태를 정의하면 도구가 현재 상태와의 차이를 계산하여 변경을 수행합니다. Terraform, CloudFormation이 이 방식입니다.

명령적(Imperative): 실행할 단계를 순서대로 기술합니다. 쉘 스크립트, Ansible(일부), Pulumi가 이 방식에 가깝습니다.

Terraform은 선언적 접근을 채택하여, 인프라의 "무엇(What)"을 정의하면 "어떻게(How)"는 Terraform이 알아서 처리합니다.


2. Terraform 기초

Provider

Provider는 Terraform이 특정 인프라 플랫폼과 통신하기 위한 플러그인입니다. AWS, GCP, Azure 같은 클라우드 프로바이더뿐 아니라 Kubernetes, Helm, Datadog, GitHub 등 수천 개의 프로바이더가 존재합니다.

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-2"

  default_tags {
    tags = {
      Environment = "production"
      ManagedBy   = "terraform"
    }
  }
}

required_providers 블록에서 프로바이더의 소스와 버전 제약을 명시합니다. ~> 5.0은 5.x 범위의 최신 버전을 사용하되 6.0 이상은 허용하지 않는다는 의미입니다.

Resource

Resource는 Terraform으로 관리할 인프라 객체를 선언합니다. 리소스 타입과 로컬 이름의 조합으로 고유하게 식별됩니다.

resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "main-vpc"
  }
}

resource "aws_subnet" "public" {
  count             = 2
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet-${count.index + 1}"
  }
}

리소스 간 참조는 리소스타입.로컬이름.속성 형태로 합니다. 위 예제에서 aws_vpc.main.id는 VPC 리소스의 ID를 참조합니다.

Data Source

Data Source는 Terraform 외부에서 이미 존재하는 리소스의 정보를 읽어옵니다. 읽기 전용이므로 인프라를 변경하지 않습니다.

data "aws_availability_zones" "available" {
  state = "available"
}

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }

  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

Variable과 Output

Variable은 모듈의 입력 파라미터이고, Output은 모듈의 출력값입니다.

# variables.tf
variable "environment" {
  description = "배포 환경 (dev, staging, prod)"
  type        = string
  default     = "dev"

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "environment는 dev, staging, prod 중 하나여야 합니다."
  }
}

variable "instance_type" {
  description = "EC2 인스턴스 타입"
  type        = string
  default     = "t3.medium"
}

variable "db_password" {
  description = "데이터베이스 비밀번호"
  type        = string
  sensitive   = true
}
# outputs.tf
output "vpc_id" {
  description = "생성된 VPC의 ID"
  value       = aws_vpc.main.id
}

output "alb_dns_name" {
  description = "ALB의 DNS 이름"
  value       = aws_lb.main.dns_name
}

3. HCL 문법 심화

블록 구조

HCL(HashiCorp Configuration Language)의 기본 구조는 블록입니다. 블록은 타입, 레이블, 본문으로 구성됩니다.

# 블록타입 "레이블1" "레이블2" {
#   속성 = 값
# }

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type

  root_block_device {
    volume_size = 20
    volume_type = "gp3"
  }
}

타입 시스템

HCL은 풍부한 타입 시스템을 지원합니다.

# 기본 타입
variable "name" {
  type = string
}

variable "port" {
  type = number
}

variable "enabled" {
  type = bool
}

# 컬렉션 타입
variable "availability_zones" {
  type = list(string)
}

variable "instance_tags" {
  type = map(string)
}

variable "allowed_ports" {
  type = set(number)
}

# 구조체 타입
variable "database_config" {
  type = object({
    engine         = string
    engine_version = string
    instance_class = string
    allocated_storage = number
    multi_az       = bool
  })
}

조건문

조건식은 삼항 연산자 형태로 작성합니다.

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"

  monitoring = var.environment == "prod" ? true : false
}

반복문 - count와 for_each

count는 숫자 기반 반복에 적합합니다.

resource "aws_subnet" "private" {
  count = 3

  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 10)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "private-subnet-${count.index + 1}"
  }
}

for_each는 맵이나 셋 기반 반복에 적합하며, 중간 항목을 삭제해도 인덱스가 밀리지 않습니다.

variable "subnets" {
  type = map(object({
    cidr_block        = string
    availability_zone = string
    public            = bool
  }))
}

resource "aws_subnet" "this" {
  for_each = var.subnets

  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr_block
  availability_zone = each.value.availability_zone

  map_public_ip_on_launch = each.value.public

  tags = {
    Name = each.key
  }
}

for 표현식

# 리스트 변환
locals {
  subnet_ids = [for s in aws_subnet.this : s.id]

  # 조건부 필터링
  public_subnet_ids = [for k, s in aws_subnet.this : s.id if s.map_public_ip_on_launch]

  # 맵 변환
  subnet_id_map = { for k, s in aws_subnet.this : k => s.id }
}

로컬 변수

로컬 변수는 모듈 내에서 반복적으로 사용하는 값을 한곳에 정의합니다.

locals {
  common_tags = {
    Project     = var.project_name
    Environment = var.environment
    ManagedBy   = "terraform"
    Team        = "platform"
  }

  name_prefix = "${var.project_name}-${var.environment}"

  is_production = var.environment == "prod"
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = local.is_production ? "t3.large" : "t3.micro"

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-web"
    Role = "webserver"
  })
}

4. 상태 관리

terraform.tfstate란

Terraform은 관리 중인 인프라의 현재 상태를 State 파일에 기록합니다. plan 명령은 State 파일의 상태와 코드에 선언된 상태를 비교하여 변경 사항을 계산합니다.

State 파일에는 민감한 정보(비밀번호, 키 등)가 포함될 수 있으므로, 로컬 파일 시스템에 저장하는 것은 위험합니다.

원격 백엔드 (S3 + DynamoDB)

팀 환경에서는 원격 백엔드를 사용하여 State를 중앙에서 관리해야 합니다. AWS에서는 S3 + DynamoDB 조합이 표준입니다.

terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "prod/vpc/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

S3 버킷에는 반드시 버전 관리를 활성화하고, 서버 측 암호화를 적용해야 합니다.

resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-terraform-state-bucket"

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

resource "aws_dynamodb_table" "terraform_lock" {
  name         = "terraform-lock"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

State Locking

DynamoDB 테이블은 State 잠금을 제공합니다. 두 명이 동시에 terraform apply를 실행하면, 먼저 실행한 사람이 잠금을 획득하고 나중에 실행한 사람은 대기하게 됩니다.

잠금이 해제되지 않는 경우(프로세스 비정상 종료 등)에는 terraform force-unlock LOCK_ID 명령으로 강제 해제할 수 있습니다. 단, 다른 사람이 실제로 작업 중이 아닌지 반드시 확인해야 합니다.

State 관리 명령어

# State 목록 확인
terraform state list

# 특정 리소스 상태 확인
terraform state show aws_vpc.main

# 리소스 이름 변경 (코드에서 이름을 바꿨을 때)
terraform state mv aws_vpc.main aws_vpc.primary

# State에서 리소스 제거 (실제 인프라는 유지)
terraform state rm aws_instance.temp

# 기존 인프라를 State로 가져오기
terraform import aws_vpc.existing vpc-0123456789abcdef0

5. 모듈

모듈이란

모듈은 관련 리소스를 하나의 패키지로 묶은 것입니다. 코드 재사용성을 높이고, 관심사를 분리하며, 팀 간 표준화된 인프라 패턴을 공유할 수 있게 합니다.

모듈 디렉토리 구조

modules/
  vpc/
    main.tf
    variables.tf
    outputs.tf
    README.md
  ec2/
    main.tf
    variables.tf
    outputs.tf
  rds/
    main.tf
    variables.tf
    outputs.tf

모듈 작성

# modules/vpc/main.tf
resource "aws_vpc" "this" {
  cidr_block           = var.cidr_block
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-vpc"
  })
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-igw"
  })
}

resource "aws_subnet" "public" {
  count = length(var.public_subnet_cidrs)

  vpc_id            = aws_vpc.this.id
  cidr_block        = var.public_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  map_public_ip_on_launch = true

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-public-${count.index + 1}"
    Tier = "public"
  })
}

resource "aws_subnet" "private" {
  count = length(var.private_subnet_cidrs)

  vpc_id            = aws_vpc.this.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-private-${count.index + 1}"
    Tier = "private"
  })
}
# modules/vpc/variables.tf
variable "name_prefix" {
  description = "리소스 이름 접두사"
  type        = string
}

variable "cidr_block" {
  description = "VPC CIDR 블록"
  type        = string
  default     = "10.0.0.0/16"
}

variable "public_subnet_cidrs" {
  description = "퍼블릭 서브넷 CIDR 목록"
  type        = list(string)
}

variable "private_subnet_cidrs" {
  description = "프라이빗 서브넷 CIDR 목록"
  type        = list(string)
}

variable "availability_zones" {
  description = "가용 영역 목록"
  type        = list(string)
}

variable "tags" {
  description = "공통 태그"
  type        = map(string)
  default     = {}
}
# modules/vpc/outputs.tf
output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.this.id
}

output "public_subnet_ids" {
  description = "퍼블릭 서브넷 ID 목록"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "프라이빗 서브넷 ID 목록"
  value       = aws_subnet.private[*].id
}

모듈 호출

module "vpc" {
  source = "./modules/vpc"

  name_prefix          = "myapp-prod"
  cidr_block           = "10.0.0.0/16"
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
  availability_zones   = ["ap-northeast-2a", "ap-northeast-2c"]

  tags = local.common_tags
}

# 모듈 출력 참조
resource "aws_instance" "web" {
  subnet_id = module.vpc.public_subnet_ids[0]
  # ...
}

Terraform Registry 모듈

Terraform Registry에는 커뮤니티와 HashiCorp이 검증한 모듈이 공개되어 있습니다.

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-2a", "ap-northeast-2c"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]

  enable_nat_gateway   = true
  single_nat_gateway   = false
  enable_dns_hostnames = true
}

6. Terragrunt

Terragrunt란

Terragrunt는 Terraform의 래퍼 도구로, DRY(Don't Repeat Yourself) 원칙을 적용하여 중복 설정을 제거합니다. 여러 환경(dev, staging, prod)을 관리할 때 특히 유용합니다.

디렉토리 구조

infrastructure/
  terragrunt.hcl              # 루트 설정
  environments/
    dev/
      terragrunt.hcl          # dev 환경 공통
      vpc/
        terragrunt.hcl
      ec2/
        terragrunt.hcl
      rds/
        terragrunt.hcl
    staging/
      terragrunt.hcl
      vpc/
        terragrunt.hcl
      ec2/
        terragrunt.hcl
    prod/
      terragrunt.hcl
      vpc/
        terragrunt.hcl
      ec2/
        terragrunt.hcl
      rds/
        terragrunt.hcl
  modules/
    vpc/
    ec2/
    rds/

루트 설정

# infrastructure/terragrunt.hcl
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    bucket         = "my-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region = "ap-northeast-2"
}
EOF
}

환경별 설정

# environments/prod/vpc/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../../modules/vpc"
}

inputs = {
  name_prefix          = "myapp-prod"
  cidr_block           = "10.0.0.0/16"
  public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnet_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
  availability_zones   = ["ap-northeast-2a", "ap-northeast-2c"]
}

의존성 관리

Terragrunt는 모듈 간 의존성을 명시적으로 선언할 수 있습니다.

# environments/prod/ec2/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "../../../modules/ec2"
}

dependency "vpc" {
  config_path = "../vpc"
}

inputs = {
  vpc_id    = dependency.vpc.outputs.vpc_id
  subnet_id = dependency.vpc.outputs.public_subnet_ids[0]
}

terragrunt run-all apply를 실행하면, 의존성 순서에 따라 VPC를 먼저 생성하고 EC2를 나중에 생성합니다.


7. 워크플로우

핵심 명령어

# 프로바이더 플러그인 다운로드 및 초기화
terraform init

# 변경 계획 확인 (실제 변경 없음)
terraform plan

# 변경 적용
terraform apply

# 특정 리소스만 적용
terraform apply -target=aws_vpc.main

# 인프라 전체 삭제
terraform destroy

# 코드 포맷팅
terraform fmt -recursive

# 설정 유효성 검사
terraform validate

# 의존성 그래프 출력
terraform graph | dot -Tpng > graph.png

Plan 파일 저장

# plan 결과를 파일로 저장
terraform plan -out=tfplan

# 저장된 plan을 그대로 적용 (추가 확인 없음)
terraform apply tfplan

plan 파일을 사용하면 plan 시점과 apply 시점 사이에 코드가 변경되어도 plan 시점의 변경만 적용됩니다.

CI/CD 연동

GitHub Actions를 사용한 Terraform CI/CD 파이프라인 예시입니다.

name: Terraform CI/CD

on:
  pull_request:
    paths:
      - 'infrastructure/**'
  push:
    branches:
      - main
    paths:
      - 'infrastructure/**'

jobs:
  plan:
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.7.0

      - name: Terraform Init
        working-directory: infrastructure
        run: terraform init

      - name: Terraform Format Check
        working-directory: infrastructure
        run: terraform fmt -check -recursive

      - name: Terraform Validate
        working-directory: infrastructure
        run: terraform validate

      - name: Terraform Plan
        working-directory: infrastructure
        run: terraform plan -no-color -out=tfplan

      - name: Comment Plan on PR
        uses: actions/github-script@v7
        with:
          script: |
            const output = `#### Terraform Plan
            \`\`\`
            Plan output here
            \`\`\`
            `;

  apply:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    environment: production
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.7.0

      - name: Terraform Init
        working-directory: infrastructure
        run: terraform init

      - name: Terraform Apply
        working-directory: infrastructure
        run: terraform apply -auto-approve

8. AWS 실전 예제 - VPC + EC2 + RDS + ALB

전체 아키텍처

이 예제에서는 다음 인프라를 구축합니다.

  • VPC (퍼블릭 서브넷 2개 + 프라이빗 서브넷 2개)
  • Application Load Balancer (ALB)
  • EC2 인스턴스 (Auto Scaling Group)
  • RDS PostgreSQL (Multi-AZ)
  • Security Group 체인

VPC 및 네트워크

# vpc.tf
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.0"

  name = "${local.name_prefix}-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-2a", "ap-northeast-2c"]
  public_subnets  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_subnets = ["10.0.11.0/24", "10.0.12.0/24"]

  enable_nat_gateway   = true
  single_nat_gateway   = true
  enable_dns_hostnames = true

  public_subnet_tags = {
    Tier = "public"
  }

  private_subnet_tags = {
    Tier = "private"
  }

  tags = local.common_tags
}

Security Groups

# security_groups.tf
resource "aws_security_group" "alb" {
  name_prefix = "${local.name_prefix}-alb-"
  vpc_id      = module.vpc.vpc_id

  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"]
  }

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-alb-sg"
  })

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_security_group" "app" {
  name_prefix = "${local.name_prefix}-app-"
  vpc_id      = module.vpc.vpc_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"]
  }

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-app-sg"
  })

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_security_group" "rds" {
  name_prefix = "${local.name_prefix}-rds-"
  vpc_id      = module.vpc.vpc_id

  ingress {
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.app.id]
  }

  tags = merge(local.common_tags, {
    Name = "${local.name_prefix}-rds-sg"
  })

  lifecycle {
    create_before_destroy = true
  }
}

ALB

# alb.tf
resource "aws_lb" "main" {
  name               = "${local.name_prefix}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = module.vpc.public_subnets

  tags = local.common_tags
}

resource "aws_lb_target_group" "app" {
  name     = "${local.name_prefix}-app-tg"
  port     = 8080
  protocol = "HTTP"
  vpc_id   = module.vpc.vpc_id

  health_check {
    path                = "/health"
    healthy_threshold   = 2
    unhealthy_threshold = 3
    timeout             = 5
    interval            = 30
  }

  tags = local.common_tags
}

resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.app.arn
  }
}

Auto Scaling Group

# asg.tf
resource "aws_launch_template" "app" {
  name_prefix   = "${local.name_prefix}-app-"
  image_id      = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type

  vpc_security_group_ids = [aws_security_group.app.id]

  user_data = base64encode(<<-SCRIPT
    #!/bin/bash
    yum update -y
    yum install -y docker
    systemctl start docker
    systemctl enable docker
    docker run -d -p 8080:8080 myapp:latest
    SCRIPT
  )

  tag_specifications {
    resource_type = "instance"
    tags = merge(local.common_tags, {
      Name = "${local.name_prefix}-app"
    })
  }
}

resource "aws_autoscaling_group" "app" {
  name                = "${local.name_prefix}-app-asg"
  desired_capacity    = 2
  max_size            = 4
  min_size            = 1
  target_group_arns   = [aws_lb_target_group.app.arn]
  vpc_zone_identifier = module.vpc.private_subnets

  launch_template {
    id      = aws_launch_template.app.id
    version = "$Latest"
  }

  tag {
    key                 = "Name"
    value               = "${local.name_prefix}-app"
    propagate_at_launch = true
  }
}

RDS

# rds.tf
resource "aws_db_subnet_group" "main" {
  name       = "${local.name_prefix}-db-subnet"
  subnet_ids = module.vpc.private_subnets

  tags = local.common_tags
}

resource "aws_db_instance" "main" {
  identifier     = "${local.name_prefix}-postgres"
  engine         = "postgres"
  engine_version = "15.4"
  instance_class = "db.t3.medium"

  allocated_storage     = 20
  max_allocated_storage = 100
  storage_encrypted     = true

  db_name  = "myapp"
  username = "admin"
  password = var.db_password

  multi_az               = true
  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.rds.id]

  backup_retention_period = 7
  skip_final_snapshot     = false
  final_snapshot_identifier = "${local.name_prefix}-final-snapshot"

  tags = local.common_tags
}

9. 보안

Sensitive 변수

sensitive로 표시된 변수는 plan/apply 출력에서 마스킹됩니다.

variable "db_password" {
  type      = string
  sensitive = true
}

output "db_endpoint" {
  value = aws_db_instance.main.endpoint
}

output "db_password" {
  value     = var.db_password
  sensitive = true
}

민감한 값 관리 방법

  1. 환경 변수: TF_VAR_db_password 환경 변수를 통해 주입합니다.
  2. terraform.tfvars 파일: .gitignore에 추가하여 버전 관리에서 제외합니다.
  3. Vault 연동: HashiCorp Vault에서 동적으로 시크릿을 가져옵니다.
# Vault에서 DB 비밀번호 가져오기
data "vault_generic_secret" "db" {
  path = "secret/data/production/database"
}

resource "aws_db_instance" "main" {
  password = data.vault_generic_secret.db.data["password"]
  # ...
}
  1. AWS Secrets Manager 연동
data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "production/database/password"
}

resource "aws_db_instance" "main" {
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
  # ...
}

정책 검사 - OPA (Open Policy Agent)

OPA를 사용하여 Terraform plan에 정책을 강제할 수 있습니다.

# policy/terraform.rego
package terraform

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_s3_bucket"
  not resource.change.after.server_side_encryption_configuration
  msg := "S3 버킷에는 반드시 암호화가 설정되어야 합니다."
}

deny[msg] {
  resource := input.resource_changes[_]
  resource.type == "aws_security_group_rule"
  resource.change.after.cidr_blocks[_] == "0.0.0.0/0"
  resource.change.after.from_port == 22
  msg := "SSH(22) 포트를 전체 인터넷(0.0.0.0/0)에 개방할 수 없습니다."
}
# plan을 JSON으로 출력
terraform plan -out=tfplan
terraform show -json tfplan > tfplan.json

# OPA로 정책 검사
opa eval --data policy/ --input tfplan.json "data.terraform.deny"

tfsec / Trivy 정적 분석

# tfsec로 보안 스캔
tfsec .

# Trivy로 IaC 스캔
trivy config .

10. 모범 사례

디렉토리 구조

project/
  environments/
    dev/
      main.tf
      variables.tf
      terraform.tfvars
      backend.tf
    staging/
      main.tf
      variables.tf
      terraform.tfvars
      backend.tf
    prod/
      main.tf
      variables.tf
      terraform.tfvars
      backend.tf
  modules/
    vpc/
    ec2/
    rds/
    alb/
  global/
    iam/
    route53/

네이밍 컨벤션

  • 리소스 이름: snake_case 사용 (aws_security_group.web_server)
  • 변수 이름: snake_case 사용 (instance_type, db_password)
  • 파일 이름: 리소스 유형별로 분리 (vpc.tf, ec2.tf, rds.tf)
  • 태그 값: 환경-서비스-역할 패턴 (prod-myapp-web)

코드 리뷰 체크리스트

  1. 보안: 시크릿이 하드코딩되지 않았는가? Security Group이 과도하게 열려 있지 않은가?
  2. 비용: 인스턴스 타입이 적절한가? 사용하지 않는 리소스는 없는가?
  3. 가용성: Multi-AZ가 적용되었는가? Auto Scaling이 설정되었는가?
  4. 상태 관리: State 키가 적절한가? 원격 백엔드가 설정되었는가?
  5. 모듈화: 중복 코드가 모듈로 추출될 수 있는가?
  6. 태깅: 모든 리소스에 필수 태그가 있는가?

자주 하는 실수와 해결법

실수해결
State 파일을 Git에 커밋.gitignore에 추가, 원격 백엔드 사용
시크릿 하드코딩sensitive 변수, Vault, Secrets Manager 사용
count로 리소스 생성 후 중간 항목 삭제for_each 사용
프로바이더 버전 미고정required_providers에 버전 명시
plan 없이 바로 applyCI/CD에서 plan 리뷰 필수화
모듈 없이 모든 코드를 한 파일에 작성모듈로 분리, 관심사 별 파일 분리

마무리

Terraform과 IaC는 현대 클라우드 인프라 관리의 핵심입니다. 코드로 인프라를 정의하면 재현 가능하고, 변경 추적이 가능하며, 코드 리뷰를 통해 품질을 높일 수 있습니다. 핵심 원칙을 정리하면 다음과 같습니다.

  1. 선언적 코드로 인프라를 정의하고, Git으로 버전 관리합니다.
  2. 원격 백엔드로 State를 안전하게 관리하고, 잠금으로 동시 변경을 방지합니다.
  3. 모듈로 코드를 재사용하고, 팀 표준을 정립합니다.
  4. CI/CD 파이프라인으로 plan 리뷰와 자동 배포를 구현합니다.
  5. 정책 검사로 보안과 규정 준수를 자동화합니다.

이 가이드의 예제를 기반으로 자신의 프로젝트에 맞게 수정하여 활용해 보세요.

현재 단락 (1/791)

전통적인 인프라 관리 방식은 관리자가 콘솔에 접속하여 수동으로 서버를 프로비저닝하고 네트워크를 설정하는 것이었습니다. 이 방식에는 여러 문제가 있습니다.

작성 글자: 0원문 글자: 18,988작성 단락: 0/791