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 핵심 개념¶
주요 용어: - 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시
}