콘텐츠로 이동
Data Prep
상세

Terraform (Infrastructure as Code)

인프라를 코드로 정의하고 버전 관리하는 IaC 도구. 클라우드 리소스의 생성, 변경, 삭제를 자동화하고 재현 가능한 인프라를 구축함.


1. Terraform 기초

1.1 IaC를 사용하는 이유

문제 IaC 없이 IaC로 해결
환경 불일치 수동 설정으로 dev/prod 차이 발생 동일 코드로 일관된 환경
변경 추적 누가 언제 뭘 바꿨는지 불명확 Git으로 모든 변경 이력 관리
재현성 환경 재구축 시 문서 의존 코드 실행으로 동일 환경 재현
협업 설정 공유/리뷰 어려움 PR/코드 리뷰 프로세스 적용
규모 확장 수동 작업 한계 자동화로 대규모 인프라 관리

1.2 Terraform vs 다른 IaC 도구

도구 특징 적합한 경우
Terraform 멀티 클라우드, 선언적, 상태 관리 범용 인프라 관리
CloudFormation AWS 전용, AWS 서비스와 긴밀 통합 AWS only 환경
Pulumi 범용 프로그래밍 언어 사용 복잡한 로직이 필요한 경우
Ansible 설정 관리 + 프로비저닝 서버 구성 관리 중심
CDK 프로그래밍 언어로 CloudFormation 생성 AWS + 개발자 친화적

1.3 핵심 개념

Terraform 워크플로우

주요 용어: - Provider: 클라우드/서비스 API와 연결 (aws, google, kubernetes 등) - Resource: 관리할 인프라 리소스 (EC2, S3, VPC 등) - Data Source: 외부에서 읽어오는 데이터 - Module: 재사용 가능한 Terraform 코드 묶음 - State: 현재 인프라 상태를 추적하는 파일 - Backend: State 파일 저장 위치 (S3, GCS 등)


2. 기본 사용법

2.1 디렉토리 구조

project/
├── main.tf           # 메인 리소스 정의
├── variables.tf      # 변수 선언
├── outputs.tf        # 출력 값 정의
├── terraform.tfvars  # 변수 값 (gitignore 대상)
├── providers.tf      # Provider 설정
├── backend.tf        # State 백엔드 설정
└── modules/          # 커스텀 모듈
    └── vpc/
        ├── main.tf
        ├── variables.tf
        └── outputs.tf

2.2 Provider 설정

# providers.tf
terraform {
  required_version = ">= 1.5.0"

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

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = var.environment
      Project     = var.project_name
      ManagedBy   = "terraform"
    }
  }
}

# 다중 Provider (멀티 리전)
provider "aws" {
  alias  = "us_east"
  region = "us-east-1"
}

2.3 변수 정의

# variables.tf
variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "ap-northeast-2"
}

variable "environment" {
  description = "Environment name"
  type        = string

  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "instance_types" {
  description = "EC2 instance types by environment"
  type        = map(string)
  default = {
    dev     = "t3.micro"
    staging = "t3.small"
    prod    = "t3.medium"
  }
}

variable "enable_monitoring" {
  description = "Enable CloudWatch monitoring"
  type        = bool
  default     = true
}

variable "allowed_cidrs" {
  description = "Allowed CIDR blocks for access"
  type        = list(string)
  default     = []
}

2.4 기본 명령어

# 초기화 (provider 다운로드, backend 설정)
terraform init

# 실행 계획 확인
terraform plan

# 특정 파일로 plan 저장
terraform plan -out=tfplan

# 변경 적용
terraform apply

# 저장된 plan 적용 (확인 없이)
terraform apply tfplan

# 리소스 삭제
terraform destroy

# 특정 리소스만 대상
terraform apply -target=aws_instance.web

# 상태 확인
terraform state list
terraform state show aws_instance.web

# 포맷팅
terraform fmt -recursive

# 유효성 검사
terraform validate

3. ML 인프라 예제

3.1 S3 + IAM (데이터 레이크)

# main.tf
resource "aws_s3_bucket" "ml_data" {
  bucket = "${var.project_name}-ml-data-${var.environment}"
}

resource "aws_s3_bucket_versioning" "ml_data" {
  bucket = aws_s3_bucket.ml_data.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "ml_data" {
  bucket = aws_s3_bucket.ml_data.id

  rule {
    id     = "raw-data-lifecycle"
    status = "Enabled"
    filter {
      prefix = "raw/"
    }
    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }
    transition {
      days          = 90
      storage_class = "GLACIER"
    }
  }

  rule {
    id     = "delete-old-checkpoints"
    status = "Enabled"
    filter {
      prefix = "checkpoints/"
    }
    expiration {
      days = 14
    }
  }
}

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

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# IAM Role for SageMaker
resource "aws_iam_role" "sagemaker" {
  name = "${var.project_name}-sagemaker-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "sagemaker.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy" "sagemaker_s3" {
  name = "s3-access"
  role = aws_iam_role.sagemaker.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "s3:GetObject",
          "s3:PutObject",
          "s3:DeleteObject",
          "s3:ListBucket"
        ]
        Resource = [
          aws_s3_bucket.ml_data.arn,
          "${aws_s3_bucket.ml_data.arn}/*"
        ]
      }
    ]
  })
}

3.2 VPC + EKS (ML 클러스터)

# vpc.tf
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"

  name = "${var.project_name}-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["ap-northeast-2a", "ap-northeast-2b", "ap-northeast-2c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway   = true
  single_nat_gateway   = var.environment != "prod"
  enable_dns_hostnames = true

  public_subnet_tags = {
    "kubernetes.io/role/elb" = 1
  }
  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = 1
  }
}

# eks.tf
module "eks" {
  source  = "terraform-aws-modules/eks/aws"
  version = "~> 19.0"

  cluster_name    = "${var.project_name}-eks"
  cluster_version = "1.28"

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets

  cluster_endpoint_public_access = true

  eks_managed_node_groups = {
    cpu = {
      name           = "cpu-nodes"
      instance_types = ["m5.xlarge"]
      min_size       = 1
      max_size       = 10
      desired_size   = 2

      labels = {
        node-type = "cpu"
      }
    }

    gpu = {
      name           = "gpu-nodes"
      instance_types = ["g5.xlarge"]
      min_size       = 0
      max_size       = 5
      desired_size   = 0
      ami_type       = "AL2_x86_64_GPU"

      labels = {
        node-type = "gpu"
      }
      taints = [{
        key    = "nvidia.com/gpu"
        value  = "true"
        effect = "NO_SCHEDULE"
      }]
    }
  }
}

3.3 ECR (컨테이너 레지스트리)

resource "aws_ecr_repository" "ml_images" {
  for_each = toset(["training", "inference", "preprocessing"])

  name                 = "${var.project_name}/${each.key}"
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }
}

resource "aws_ecr_lifecycle_policy" "ml_images" {
  for_each   = aws_ecr_repository.ml_images
  repository = each.value.name

  policy = jsonencode({
    rules = [
      {
        rulePriority = 1
        description  = "Keep last 10 images"
        selection = {
          tagStatus   = "any"
          countType   = "imageCountMoreThan"
          countNumber = 10
        }
        action = {
          type = "expire"
        }
      }
    ]
  })
}

4. State 관리

4.1 Remote Backend (S3)

# backend.tf
terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "ml-infrastructure/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

# State 버킷과 Lock 테이블 생성 (별도 프로젝트로 관리)
resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-terraform-state"
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

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

4.2 State 조작

# 리소스 목록
terraform state list

# 리소스 상세 보기
terraform state show aws_instance.web

# 리소스 이름 변경 (코드 변경 시)
terraform state mv aws_instance.old aws_instance.new

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

# 다른 State로 이동
terraform state mv -state-out=other.tfstate aws_instance.web aws_instance.web

# State 가져오기 (기존 리소스를 Terraform 관리로)
terraform import aws_instance.web i-1234567890abcdef0

5. 모듈

5.1 모듈 구조

# modules/ml-training-job/variables.tf
variable "job_name" {
  type = string
}

variable "image_uri" {
  type = string
}

variable "instance_type" {
  type    = string
  default = "ml.p3.2xlarge"
}

variable "s3_bucket" {
  type = string
}

variable "role_arn" {
  type = string
}

# modules/ml-training-job/main.tf
resource "aws_sagemaker_training_job" "this" {
  training_job_name = var.job_name

  role_arn = var.role_arn

  algorithm_specification {
    training_image = var.image_uri
    training_input_mode = "File"
  }

  resource_config {
    instance_type  = var.instance_type
    instance_count = 1
    volume_size_in_gb = 100
  }

  input_data_config {
    channel_name = "training"
    data_source {
      s3_data_source {
        s3_data_type = "S3Prefix"
        s3_uri       = "s3://${var.s3_bucket}/training-data/"
      }
    }
  }

  output_data_config {
    s3_output_path = "s3://${var.s3_bucket}/models/"
  }

  stopping_condition {
    max_runtime_in_seconds = 86400
  }
}

# modules/ml-training-job/outputs.tf
output "job_arn" {
  value = aws_sagemaker_training_job.this.arn
}

5.2 모듈 사용

# 로컬 모듈
module "training_job" {
  source = "./modules/ml-training-job"

  job_name      = "my-training-job"
  image_uri     = "${aws_ecr_repository.training.repository_url}:latest"
  instance_type = "ml.p3.2xlarge"
  s3_bucket     = aws_s3_bucket.ml_data.id
  role_arn      = aws_iam_role.sagemaker.arn
}

# 공개 모듈 (Terraform Registry)
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 5.0"
  # ...
}

6. 환경 분리

6.1 Workspace

# Workspace 생성/전환
terraform workspace new dev
terraform workspace new prod
terraform workspace select dev
terraform workspace list

# 현재 workspace 참조
resource "aws_instance" "web" {
  instance_type = terraform.workspace == "prod" ? "t3.large" : "t3.micro"

  tags = {
    Environment = terraform.workspace
  }
}

6.2 디렉토리 분리 (권장)

environments/
├── dev/
│   ├── main.tf
│   ├── terraform.tfvars
│   └── backend.tf
├── staging/
│   ├── main.tf
│   ├── terraform.tfvars
│   └── backend.tf
└── prod/
    ├── main.tf
    ├── terraform.tfvars
    └── backend.tf

modules/
├── vpc/
├── eks/
└── ml-infra/
# environments/dev/main.tf
module "ml_infra" {
  source = "../../modules/ml-infra"

  environment   = "dev"
  instance_type = "t3.micro"
}

# environments/prod/main.tf
module "ml_infra" {
  source = "../../modules/ml-infra"

  environment   = "prod"
  instance_type = "t3.large"
}

7. Best Practices

7.1 코드 관리

# 1. 버전 고정
terraform {
  required_version = "~> 1.5.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

# 2. 변수에 validation 추가
variable "environment" {
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Invalid environment."
  }
}

# 3. 의미 있는 리소스 이름
resource "aws_s3_bucket" "ml_training_data" {  # good
  # ...
}
resource "aws_s3_bucket" "bucket1" {  # bad
  # ...
}

# 4. 태그 일관성
provider "aws" {
  default_tags {
    tags = {
      Project     = var.project_name
      Environment = var.environment
      ManagedBy   = "terraform"
    }
  }
}

7.2 보안

# 1. 민감한 변수 표시
variable "db_password" {
  type      = string
  sensitive = true
}

# 2. 민감한 출력 표시
output "db_password" {
  value     = aws_db_instance.main.password
  sensitive = true
}

# 3. Secret Manager 사용
data "aws_secretsmanager_secret_version" "db_credentials" {
  secret_id = "prod/db/credentials"
}

locals {
  db_creds = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)
}

resource "aws_db_instance" "main" {
  username = local.db_creds["username"]
  password = local.db_creds["password"]
}

7.3 CI/CD 통합

# .github/workflows/terraform.yml
name: Terraform

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  terraform:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

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

      - name: Terraform Init
        run: terraform init

      - name: Terraform Format
        run: terraform fmt -check

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        run: terraform plan -out=tfplan
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Terraform Apply
        if: github.ref == 'refs/heads/main' && github.event_name == 'push'
        run: terraform apply -auto-approve tfplan
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

8. 비용 고려사항

8.1 비용 추정

# Infracost로 비용 추정
brew install infracost
infracost auth login

# 비용 분석
infracost breakdown --path .

# PR에서 비용 변화 확인
infracost diff --path . --compare-to infracost-base.json

8.2 비용 절감 패턴

# 1. Dev 환경에서는 최소 사양
resource "aws_instance" "web" {
  instance_type = var.environment == "prod" ? "t3.large" : "t3.micro"
}

# 2. NAT Gateway 단일 사용 (dev/staging)
module "vpc" {
  single_nat_gateway = var.environment != "prod"
}

# 3. Auto-stop (개발 리소스)
resource "aws_autoscaling_schedule" "stop_at_night" {
  count                  = var.environment == "dev" ? 1 : 0
  scheduled_action_name  = "stop-at-night"
  autoscaling_group_name = aws_autoscaling_group.main.name
  min_size               = 0
  max_size               = 0
  desired_capacity       = 0
  recurrence             = "0 22 * * MON-FRI"  # 매일 22시
}

참고 자료