Infrastructure as Code (Terraform)

Infrastructure as Code (Terraform)

1. IaC κ°œμš”

1.1 Infrastructure as Codeλž€?

IaCλŠ” 인프라λ₯Ό μ½”λ“œλ‘œ μ •μ˜ν•˜κ³  κ΄€λ¦¬ν•˜λŠ” λ°©μ‹μž…λ‹ˆλ‹€.

μž₯점: - 버전 관리 (Git) - μž¬ν˜„ κ°€λŠ₯μ„± - μžλ™ν™” - λ¬Έμ„œν™” - ν˜‘μ—…

1.2 IaC 도ꡬ 비ꡐ

도ꡬ μœ ν˜• μ–Έμ–΄ λ©€ν‹° ν΄λΌμš°λ“œ
Terraform 선언적 HCL βœ…
CloudFormation 선언적 JSON/YAML AWS만
Deployment Manager 선언적 YAML/Jinja GCP만
Pulumi 선언적 Python/TS λ“± βœ…
Ansible 절차적 YAML βœ…

2. Terraform 기초

2.1 μ„€μΉ˜

# macOS
brew install terraform

# Linux
wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip
unzip terraform_1.6.0_linux_amd64.zip
sudo mv terraform /usr/local/bin/

# 버전 확인
terraform version

2.2 κΈ°λ³Έ κ°œλ…

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Terraform μ›Œν¬ν”Œλ‘œμš°                                        β”‚
β”‚                                                             β”‚
β”‚  1. Write    β†’ .tf 파일 μž‘μ„±                                β”‚
β”‚  2. Init     β†’ terraform init (ν”„λ‘œλ°”μ΄λ” λ‹€μš΄λ‘œλ“œ)         β”‚
β”‚  3. Plan     β†’ terraform plan (λ³€κ²½ 사항 미리보기)          β”‚
β”‚  4. Apply    β†’ terraform apply (인프라 적용)                β”‚
β”‚  5. Destroy  β†’ terraform destroy (인프라 μ‚­μ œ)              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

2.3 HCL 문법

# ν”„λ‘œλ°”μ΄λ” μ„€μ •
provider "aws" {
  region = "ap-northeast-2"
}

# λ¦¬μ†ŒμŠ€ μ •μ˜
resource "aws_instance" "web" {
  ami           = "ami-12345678"
  instance_type = "t3.micro"

  tags = {
    Name = "WebServer"
  }
}

# λ³€μˆ˜
variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

# 좜λ ₯
output "public_ip" {
  value = aws_instance.web.public_ip
}

# 둜컬 κ°’
locals {
  environment = "production"
  common_tags = {
    Environment = local.environment
    ManagedBy   = "Terraform"
  }
}

# 데이터 μ†ŒμŠ€ (κΈ°μ‘΄ λ¦¬μ†ŒμŠ€ μ°Έμ‘°)
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

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

3. AWS 인프라 ꡬ성

3.1 VPC + EC2 예제

# main.tf

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

provider "aws" {
  region = var.region
}

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

  tags = {
    Name = "${var.project}-vpc"
  }
}

# 퍼블릭 μ„œλΈŒλ„·
resource "aws_subnet" "public" {
  count                   = 2
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.${count.index + 1}.0/24"
  availability_zone       = data.aws_availability_zones.available.names[count.index]
  map_public_ip_on_launch = true

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

# 인터넷 κ²Œμ΄νŠΈμ›¨μ΄
resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.project}-igw"
  }
}

# λΌμš°νŒ… ν…Œμ΄λΈ”
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}-public-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_security_group" "web" {
  name        = "${var.project}-web-sg"
  description = "Web server security group"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.ssh_allowed_cidr]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

# EC2 μΈμŠ€ν„΄μŠ€
resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = var.instance_type
  subnet_id              = aws_subnet.public[0].id
  vpc_security_group_ids = [aws_security_group.web.id]
  key_name               = var.key_name

  user_data = <<-EOF
    #!/bin/bash
    dnf update -y
    dnf install -y nginx
    systemctl start nginx
    systemctl enable nginx
    echo "<h1>Hello from Terraform!</h1>" > /usr/share/nginx/html/index.html
  EOF

  tags = {
    Name = "${var.project}-web"
  }
}

# 데이터 μ†ŒμŠ€
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"]
  }
}
# variables.tf

variable "region" {
  description = "AWS region"
  type        = string
  default     = "ap-northeast-2"
}

variable "project" {
  description = "Project name"
  type        = string
  default     = "myapp"
}

variable "instance_type" {
  description = "EC2 instance type"
  type        = string
  default     = "t3.micro"
}

variable "key_name" {
  description = "SSH key pair name"
  type        = string
}

variable "ssh_allowed_cidr" {
  description = "CIDR block allowed for SSH"
  type        = string
  default     = "0.0.0.0/0"
}
# outputs.tf

output "vpc_id" {
  description = "VPC ID"
  value       = aws_vpc.main.id
}

output "public_ip" {
  description = "Web server public IP"
  value       = aws_instance.web.public_ip
}

output "website_url" {
  description = "Website URL"
  value       = "http://${aws_instance.web.public_ip}"
}

4. GCP 인프라 ꡬ성

4.1 VPC + Compute Engine 예제

# main.tf

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

provider "google" {
  project = var.project_id
  region  = var.region
}

# VPC
resource "google_compute_network" "main" {
  name                    = "${var.name_prefix}-vpc"
  auto_create_subnetworks = false
}

# μ„œλΈŒλ„·
resource "google_compute_subnetwork" "public" {
  name          = "${var.name_prefix}-subnet"
  ip_cidr_range = "10.0.1.0/24"
  region        = var.region
  network       = google_compute_network.main.id
}

# λ°©ν™”λ²½ κ·œμΉ™ - HTTP
resource "google_compute_firewall" "http" {
  name    = "${var.name_prefix}-allow-http"
  network = google_compute_network.main.name

  allow {
    protocol = "tcp"
    ports    = ["80", "443"]
  }

  source_ranges = ["0.0.0.0/0"]
  target_tags   = ["http-server"]
}

# λ°©ν™”λ²½ κ·œμΉ™ - SSH
resource "google_compute_firewall" "ssh" {
  name    = "${var.name_prefix}-allow-ssh"
  network = google_compute_network.main.name

  allow {
    protocol = "tcp"
    ports    = ["22"]
  }

  source_ranges = [var.ssh_allowed_cidr]
  target_tags   = ["ssh-server"]
}

# Compute Engine μΈμŠ€ν„΄μŠ€
resource "google_compute_instance" "web" {
  name         = "${var.name_prefix}-web"
  machine_type = var.machine_type
  zone         = "${var.region}-a"

  boot_disk {
    initialize_params {
      image = "ubuntu-os-cloud/ubuntu-2204-lts"
      size  = 20
    }
  }

  network_interface {
    network    = google_compute_network.main.name
    subnetwork = google_compute_subnetwork.public.name

    access_config {
      // μ™ΈλΆ€ IP ν• λ‹Ή
    }
  }

  metadata_startup_script = <<-EOF
    #!/bin/bash
    apt-get update
    apt-get install -y nginx
    echo "<h1>Hello from Terraform on GCP!</h1>" > /var/www/html/index.html
  EOF

  tags = ["http-server", "ssh-server"]

  labels = {
    environment = var.environment
  }
}
# variables.tf

variable "project_id" {
  description = "GCP project ID"
  type        = string
}

variable "region" {
  description = "GCP region"
  type        = string
  default     = "asia-northeast3"
}

variable "name_prefix" {
  description = "Resource name prefix"
  type        = string
  default     = "myapp"
}

variable "machine_type" {
  description = "Compute Engine machine type"
  type        = string
  default     = "e2-micro"
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "dev"
}

variable "ssh_allowed_cidr" {
  description = "CIDR allowed for SSH"
  type        = string
  default     = "0.0.0.0/0"
}
# outputs.tf

output "instance_ip" {
  description = "Instance external IP"
  value       = google_compute_instance.web.network_interface[0].access_config[0].nat_ip
}

output "website_url" {
  description = "Website URL"
  value       = "http://${google_compute_instance.web.network_interface[0].access_config[0].nat_ip}"
}

5. μƒνƒœ 관리

5.1 원격 μƒνƒœ μ €μž₯μ†Œ

AWS S3 λ°±μ—”λ“œ:

terraform {
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-2"
    encrypt        = true
    dynamodb_table = "terraform-locks"  # μƒνƒœ 잠금
  }
}

GCP Cloud Storage λ°±μ—”λ“œ:

terraform {
  backend "gcs" {
    bucket = "my-terraform-state"
    prefix = "prod/terraform.tfstate"
  }
}

5.2 μƒνƒœ λͺ…λ Ήμ–΄

# μƒνƒœ λͺ©λ‘
terraform state list

# μƒνƒœ 쑰회
terraform state show aws_instance.web

# μƒνƒœμ—μ„œ λ¦¬μ†ŒμŠ€ 제거 (μ‹€μ œ λ¦¬μ†ŒμŠ€λŠ” μœ μ§€)
terraform state rm aws_instance.web

# μƒνƒœ 이동 (λ¦¬νŒ©ν† λ§)
terraform state mv aws_instance.old aws_instance.new

# μƒνƒœ κ°€μ Έμ˜€κΈ° (κΈ°μ‘΄ λ¦¬μ†ŒμŠ€)
terraform import aws_instance.web i-1234567890abcdef0

6. λͺ¨λ“ˆ

6.1 λͺ¨λ“ˆ ꡬ쑰

modules/
β”œβ”€β”€ vpc/
β”‚   β”œβ”€β”€ main.tf
β”‚   β”œβ”€β”€ variables.tf
β”‚   └── outputs.tf
└── ec2/
    β”œβ”€β”€ main.tf
    β”œβ”€β”€ variables.tf
    └── outputs.tf

6.2 λͺ¨λ“ˆ μ •μ˜

# modules/vpc/main.tf

resource "aws_vpc" "this" {
  cidr_block           = var.cidr_block
  enable_dns_hostnames = true

  tags = merge(var.tags, {
    Name = var.name
  })
}

resource "aws_subnet" "public" {
  count                   = length(var.public_subnets)
  vpc_id                  = aws_vpc.this.id
  cidr_block              = var.public_subnets[count.index]
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = merge(var.tags, {
    Name = "${var.name}-public-${count.index + 1}"
  })
}
# modules/vpc/variables.tf

variable "name" {
  type = string
}

variable "cidr_block" {
  type    = string
  default = "10.0.0.0/16"
}

variable "public_subnets" {
  type = list(string)
}

variable "availability_zones" {
  type = list(string)
}

variable "tags" {
  type    = map(string)
  default = {}
}
# modules/vpc/outputs.tf

output "vpc_id" {
  value = aws_vpc.this.id
}

output "public_subnet_ids" {
  value = aws_subnet.public[*].id
}

6.3 λͺ¨λ“ˆ μ‚¬μš©

# main.tf

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

  name               = "myapp"
  cidr_block         = "10.0.0.0/16"
  public_subnets     = ["10.0.1.0/24", "10.0.2.0/24"]
  availability_zones = ["ap-northeast-2a", "ap-northeast-2c"]

  tags = {
    Environment = "production"
  }
}

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

  name          = "myapp-web"
  subnet_id     = module.vpc.public_subnet_ids[0]
  instance_type = "t3.micro"
}

7. μ›Œν¬μŠ€νŽ˜μ΄μŠ€

# μ›Œν¬μŠ€νŽ˜μ΄μŠ€ λͺ©λ‘
terraform workspace list

# μƒˆ μ›Œν¬μŠ€νŽ˜μ΄μŠ€ 생성
terraform workspace new dev
terraform workspace new prod

# μ›Œν¬μŠ€νŽ˜μ΄μŠ€ μ „ν™˜
terraform workspace select prod

# ν˜„μž¬ μ›Œν¬μŠ€νŽ˜μ΄μŠ€
terraform workspace show
# μ›Œν¬μŠ€νŽ˜μ΄μŠ€λ³„ μ„€μ •
locals {
  environment = terraform.workspace

  instance_type = {
    dev  = "t3.micro"
    prod = "t3.large"
  }
}

resource "aws_instance" "web" {
  instance_type = local.instance_type[local.environment]
  # ...
}

8. λͺ¨λ²” 사둀

8.1 디렉토리 ꡬ쑰

terraform/
β”œβ”€β”€ environments/
β”‚   β”œβ”€β”€ dev/
β”‚   β”‚   β”œβ”€β”€ main.tf
β”‚   β”‚   β”œβ”€β”€ variables.tf
β”‚   β”‚   └── terraform.tfvars
β”‚   └── prod/
β”‚       β”œβ”€β”€ main.tf
β”‚       β”œβ”€β”€ variables.tf
β”‚       └── terraform.tfvars
β”œβ”€β”€ modules/
β”‚   β”œβ”€β”€ vpc/
β”‚   β”œβ”€β”€ ec2/
β”‚   └── rds/
└── global/
    └── iam/

8.2 μ½”λ“œ μŠ€νƒ€μΌ

# λ¦¬μ†ŒμŠ€ 이름 κ·œμΉ™
resource "aws_instance" "web" { }  # λ‹¨μˆ˜
resource "aws_subnet" "public" { } # 볡수 μ‚¬μš© μ‹œ count/for_each

# λ³€μˆ˜ κΈ°λ³Έκ°’
variable "instance_type" {
  description = "EC2 instance type"  # 항상 μ„€λͺ… 포함
  type        = string
  default     = "t3.micro"
}

# νƒœκ·Έ 일관성
locals {
  common_tags = {
    Project     = var.project
    Environment = var.environment
    ManagedBy   = "Terraform"
  }
}

8.3 λ³΄μ•ˆ

# λ―Όκ°ν•œ λ³€μˆ˜
variable "db_password" {
  type      = string
  sensitive = true
}

# λ―Όκ°ν•œ 좜λ ₯
output "db_password" {
  value     = var.db_password
  sensitive = true
}

9. CI/CD 톡합

9.1 GitHub Actions

# .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.6.0

      - name: Terraform Init
        run: terraform init

      - name: Terraform Plan
        run: terraform plan -no-color
        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'
        run: terraform apply -auto-approve
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

10. λ‹€μŒ 단계


참고 자료

to navigate between lessons