基礎LESSON 05

この教材で学ぶこと インフラを宣言的に管理する基本を学びます。

Terraform — IaC でインフラをコード管理

前提知識: クラウドの基本。本教材ではインフラをコードで表現する感覚を身につけます。


1. なぜ Terraform か

手段 再現性 ドリフト検知 マルチクラウド
マネコン手動 ❌ ❌ -
AWS CLI / SDK △ ❌ △
CloudFormation ✅ △ ❌ AWS のみ
Terraform ✅ ✅ ✅

Desired State 思想: 「あるべき状態」を宣言 → Terraform が差分を計算して適用(Kubernetes の Deployment と同じ考え方)


2. 基本的な使い方

terraform init      # プロバイダーのダウンロード(初回・provider変更時)
terraform plan      # 差分確認(実際には何も変えない)
terraform apply     # 適用(plan の内容を実行)
terraform destroy   # 全リソースを削除
terraform fmt       # コードフォーマット
terraform validate  # 構文チェック
terraform show      # 現在のstateを確認

3. 基本構文(HCL)

# main.tf

# プロバイダー設定
terraform {
  required_version = ">= 1.7"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"  # 5.x 系を使用
    }
  }
  # 本番ではstateをS3に保存
  backend "s3" {
    bucket         = "my-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
    dynamodb_table = "terraform-lock"  # ステートロック
  }
}

provider "aws" {
  region = "ap-northeast-1"
  default_tags {           # 全リソースにタグを付ける
    tags = {
      ManagedBy   = "Terraform"
      Environment = var.environment
    }
  }
}

4. 変数・出力・データソース

# variables.tf
variable "environment" {
  description = "デプロイ環境"
  type        = string
  validation {
    condition     = contains(["prod", "stg", "dev"], var.environment)
    error_message = "environment は prod/stg/dev のいずれか"
  }
}

variable "instance_type" {
  type    = string
  default = "t3.medium"
}

# outputs.tf
output "alb_dns_name" {
  description = "ALBのDNS名"
  value       = aws_lb.main.dns_name
}

output "rds_endpoint" {
  description = "RDSエンドポイント(機密なのでsensitive)"
  value       = aws_db_instance.main.endpoint
  sensitive   = true  # terraform output では表示されない
}

# data.tf — 既存リソースの参照
data "aws_vpc" "existing" {
  tags = {
    Name = "existing-vpc"
  }
}

data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

5. 実践的な構成例(EC2 + ALB + RDS)

# ec2.tf
resource "aws_instance" "app" {
  count         = var.environment == "prod" ? 2 : 1  # 環境で数を変える
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  subnet_id     = element(aws_subnet.private[*].id, count.index)

  vpc_security_group_ids = [aws_security_group.app.id]
  iam_instance_profile   = aws_iam_instance_profile.app.name

  user_data = base64encode(templatefile("${path.module}/templates/userdata.sh.tpl", {
    db_host  = aws_db_instance.main.endpoint
    app_env  = var.environment
  }))

  tags = {
    Name = "${var.environment}-app-${count.index + 1}"
  }
}

# セキュリティグループ
resource "aws_security_group" "app" {
  name   = "${var.environment}-app-sg"
  vpc_id = data.aws_vpc.existing.id

  ingress {
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]  # ALBからのみ
  }

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

# RDS
resource "aws_db_instance" "main" {
  identifier        = "${var.environment}-db"
  engine            = "postgres"
  engine_version    = "16.1"
  instance_class    = var.environment == "prod" ? "db.t3.medium" : "db.t3.micro"
  allocated_storage = 20

  db_name  = "myapp"
  username = "admin"
  password = random_password.db.result  # random プロバイダーで生成

  multi_az               = var.environment == "prod"  # 本番のみMulti-AZ
  skip_final_snapshot    = var.environment != "prod"
  deletion_protection    = var.environment == "prod"

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

resource "random_password" "db" {
  length  = 32
  special = false
}

# パスワードを Secrets Manager に保存
resource "aws_secretsmanager_secret_version" "db" {
  secret_id = aws_secretsmanager_secret.db.id
  secret_string = jsonencode({
    username = aws_db_instance.main.username
    password = random_password.db.result
    host     = aws_db_instance.main.endpoint
  })
}

6. モジュール化

# modules/vpc/main.tf に VPC の定義を切り出す

# 呼び出し側
module "vpc" {
  source = "./modules/vpc"

  environment  = var.environment
  cidr_block   = "10.0.0.0/16"
  azs          = ["ap-northeast-1a", "ap-northeast-1c"]
  public_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
  private_cidrs = ["10.0.11.0/24", "10.0.12.0/24"]
}

# 出力の参照
resource "aws_instance" "app" {
  subnet_id = module.vpc.private_subnet_ids[0]
}

7. CI/CD での Terraform 運用

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

on:
  pull_request:
    paths: ['terraform/**']
  push:
    branches: [main]
    paths: ['terraform/**']

jobs:
  plan:
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
      - run: terraform plan -out=tfplan
      - name: PRにplan結果をコメント
        uses: actions/github-script@v7
        # ... plan出力をPRにコメント

  apply:
    if: github.ref == 'refs/heads/main'
    steps:
      - run: terraform apply -auto-approve tfplan

🔗 次のステップ