発展LESSON 06

この教材で学ぶこと 複数環境とチーム運用のIaC設計を学びます。

Terraform 応用 — Workspace・Terragrunt・Atlantis

前提知識: TerraformとHCLの基礎。本教材ではチーム開発・複数環境・CI/CD統合を扱います。


1. Terraform Workspace — 環境分離

# Workspace = state ファイルを分離する仕組み
# 同じコードベースで dev/stg/prod を管理

terraform workspace list
# * default
#   dev
#   staging
#   production

terraform workspace new staging
terraform workspace select staging
terraform plan
# workspace によって設定を切り替え
locals {
  env = terraform.workspace  # "dev", "staging", "production"

  config = {
    dev = {
      instance_type = "t3.micro"
      min_size      = 1
      max_size      = 2
    }
    staging = {
      instance_type = "t3.small"
      min_size      = 1
      max_size      = 3
    }
    production = {
      instance_type = "t3.medium"
      min_size      = 2
      max_size      = 10
    }
  }

  current = local.config[local.env]
}

resource "aws_instance" "app" {
  instance_type = local.current.instance_type
  # ...
}

Workspace の制限: 同じ backend(S3 バケット等)を共有するため、IAM 設定が複雑。大規模では Terragrunt が有効。


2. Terragrunt — DRY な多環境管理

ディレクトリ構成(Terragrunt 推奨):

infra/
├── modules/                    # 再利用可能なモジュール
│   ├── vpc/
│   ├── ecs/
│   └── rds/
├── live/
│   ├── terragrunt.hcl          # 共通設定(backend・provider)
│   ├── dev/
│   │   ├── terragrunt.hcl      # dev 用の変数
│   │   ├── vpc/
│   │   │   └── terragrunt.hcl
│   │   └── ecs/
│   │       └── terragrunt.hcl
│   └── production/
│       ├── terragrunt.hcl
│       └── ...
# live/terragrunt.hcl(ルート:共通設定)
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite"
  }
  config = {
    bucket         = "mycompany-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
    dynamodb_table = "terraform-lock"
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite"
  contents = <<EOF
provider "aws" {
  region = "ap-northeast-1"
}
EOF
}
# live/production/vpc/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

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

inputs = {
  vpc_cidr     = "10.0.0.0/16"
  env          = "production"
  az_count     = 3
}
# 全環境を一括でプランしたい場合(--all オプション)
cd live/production
terragrunt run-all plan

# 依存関係も自動的に処理(vpc → ecs の順でデプロイ)
terragrunt run-all apply --terragrunt-non-interactive

3. Atlantis — Pull Request ベースのレビューフロー

Atlantis の仕組み:
  PR 作成
    ↓
  GitHub Webhook → Atlantis サーバー
    ↓
  Atlantis が自動で `terraform plan` を実行
    ↓
  PR にプランの結果をコメント
    ↓
  レビュアーが確認してコメントで承認
  `atlantis apply`
    ↓
  `terraform apply` が実行される → PR マージ
# atlantis.yaml(リポジトリルートに置く)
version: 3
automerge: false
parallel_plan: true
parallel_apply: false

projects:
  - name: production-vpc
    dir: live/production/vpc
    workspace: default
    autoplan:
      when_modified:
        - "*.tf"
        - "../../../modules/vpc/**/*.tf"
      enabled: true

  - name: production-ecs
    dir: live/production/ecs
    workspace: default
    depends_on:
      - production-vpc
    autoplan:
      enabled: true

4. tfstate の管理とロック

# backend.tf — state を S3 に保存・DynamoDB でロック
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "production/main/terraform.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"
    
    # State Locking: 同時実行を防ぐ
    # DynamoDB テーブルに LockID という項目でロックを取得
  }
}
# state の操作(緊急時)
terraform state list                    # 管理対象リソースの一覧
terraform state show aws_instance.app   # 特定リソースの詳細
terraform state mv aws_instance.old aws_instance.new  # リソース名変更
terraform state rm aws_instance.to_delete             # state から削除(実リソースは残る)

# state が壊れた・ロックが残った場合
terraform force-unlock LOCK_ID

5. モジュール設計のベストプラクティス

# modules/ecs/main.tf — 再利用可能なモジュール

variable "env" {
  description = "環境名(dev/staging/production)"
  type        = string
  validation {
    condition     = contains(["dev", "staging", "production"], var.env)
    error_message = "env は dev, staging, production のいずれかである必要があります。"
  }
}

variable "container_image" {
  description = "ECR イメージ URI"
  type        = string
}

variable "desired_count" {
  description = "ECS タスクの希望数"
  type        = number
  default     = 2
}

# 出力値(他のモジュールから参照できる)
output "service_name" {
  description = "ECS サービス名"
  value       = aws_ecs_service.main.name
}

output "cluster_arn" {
  description = "ECS クラスター ARN"
  value       = aws_ecs_cluster.main.arn
}

6. CI/CD パイプラインでの Terraform

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

on:
  pull_request:
    branches: [main]
    paths: ["infra/**"]
  push:
    branches: [main]
    paths: ["infra/**"]

permissions:
  id-token: write   # OIDC 認証に必要
  contents: read
  pull-requests: write

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

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/terraform-plan-role
          aws-region: ap-northeast-1

      - name: Terraform Plan
        id: plan
        working-directory: infra/live/production
        run: |
          terraform init
          terraform plan -no-color -out=tfplan 2>&1 | tee plan.txt

      - name: Comment PR with Plan
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync('infra/live/production/plan.txt', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `\`\`\`\n${plan.slice(0, 65000)}\n\`\`\``
            });

  apply:
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: production  # GitHub Environment で承認フローを設定
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/terraform-apply-role
          aws-region: ap-northeast-1

      - name: Terraform Apply
        working-directory: infra/live/production
        run: |
          terraform init
          terraform apply -auto-approve

🔗 次のステップ