LESSON 40分

「インフラをコードで管理するのは当たり前だ。問題はその先、『どう設計するか』だ。小さなプロジェクトのTerraformと、100人のチームが運用するTerraformは、構造がまるで違う」と佐藤CTOが語った。

推定読了時間: 40分


Terraform のモジュール設計

モジュール階層パターン

graph TD
    Root["terraform/"] --> Modules["modules/
再利用可能なモジュール"] Root --> Envs["environments/
環境別の設定"] Root --> TG["terragrunt.hcl
Terragrunt 設定"] Modules --> Net["networking/"] Modules --> Comp["compute/"] Modules --> Data["data/"] Modules --> Obs["observability/"] Net --> VPC["vpc/
main.tf / variables.tf / outputs.tf"] Net --> SG["security-group/"] Net --> LB["load-balancer/"] Comp --> ECS["ecs-service/"] Comp --> Lambda["lambda-function/"] Comp --> EC2["ec2-instance/"] Data --> RDS["rds/"] Data --> DDB["dynamodb/"] Data --> EC["elasticache/"] Obs --> CW["cloudwatch/"] Obs --> XR["x-ray/"] Envs --> Dev["dev/
main.tf / terraform.tfvars / backend.tf"] Envs --> Stg["staging/"] Envs --> Prod["prod/"] style Root fill:#1e293b,stroke:#475569,color:#f8fafc style Modules fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style Envs fill:#d1fae5,stroke:#059669,color:#065f46 style TG fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e style Net fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style Comp fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style Data fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style Obs fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af

モジュールの設計原則

原則説明
単一責任1つのモジュールは1つの関心事VPCモジュール、RDSモジュール
適切な粒度大きすぎず小さすぎずサブネット単体ではなくVPC全体
明示的なインターフェースvariables と outputs で契約vpc_id を output として公開
バージョニングセマンティックバージョニングsource = "git::...?ref=v1.2.0"
# modules/compute/ecs-service/variables.tf
variable "service_name" {
  type        = string
  description = "Name of the ECS service"
}

variable "container_image" {
  type        = string
  description = "Docker image URI"
}

variable "container_port" {
  type        = number
  default     = 3000
  description = "Port the container listens on"
}

variable "desired_count" {
  type        = number
  default     = 2
  description = "Number of desired tasks"
}

variable "cpu" {
  type        = number
  default     = 256
  description = "CPU units for the task"
}

variable "memory" {
  type        = number
  default     = 512
  description = "Memory (MB) for the task"
}

variable "vpc_id" {
  type        = string
  description = "VPC ID to deploy into"
}

variable "subnet_ids" {
  type        = list(string)
  description = "Subnet IDs for the service"
}

variable "environment_variables" {
  type        = map(string)
  default     = {}
  description = "Environment variables for the container"
}
# modules/compute/ecs-service/main.tf
resource "aws_ecs_task_definition" "this" {
  family                   = var.service_name
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = var.cpu
  memory                   = var.memory
  execution_role_arn       = aws_iam_role.execution.arn
  task_role_arn            = aws_iam_role.task.arn

  container_definitions = jsonencode([
    {
      name      = var.service_name
      image     = var.container_image
      essential = true
      portMappings = [
        {
          containerPort = var.container_port
          protocol      = "tcp"
        }
      ]
      environment = [
        for key, value in var.environment_variables : {
          name  = key
          value = value
        }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          "awslogs-group"         = aws_cloudwatch_log_group.this.name
          "awslogs-region"        = data.aws_region.current.name
          "awslogs-stream-prefix" = var.service_name
        }
      }
    }
  ])
}

resource "aws_ecs_service" "this" {
  name            = var.service_name
  cluster         = var.cluster_id
  task_definition = aws_ecs_task_definition.this.arn
  desired_count   = var.desired_count
  launch_type     = "FARGATE"

  network_configuration {
    subnets          = var.subnet_ids
    security_groups  = [aws_security_group.service.id]
    assign_public_ip = false
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.this.arn
    container_name   = var.service_name
    container_port   = var.container_port
  }

  lifecycle {
    ignore_changes = [desired_count]  # HPA が管理
  }
}

Workspace と State 管理

Workspace パターン

# workspace を使った環境分離(小規模向け)
resource "aws_instance" "web" {
  ami           = var.ami_id
  instance_type = terraform.workspace == "prod" ? "m5.large" : "t3.micro"

  tags = {
    Name        = "web-${terraform.workspace}"
    Environment = terraform.workspace
  }
}

「Workspace は小規模プロジェクトには便利だが、大規模では環境ごとにディレクトリを分ける方が安全だ。State の事故は取り返しがつかない」

State 管理のベストプラクティス

項目推奨理由
BackendS3 + DynamoDB共有・ロック・暗号化
State 分割レイヤー別爆発半径の最小化
ロックDynamoDB同時実行の競合防止
暗号化SSE-S3 または KMS機密情報の保護
バージョニングS3 バージョニングState の履歴と復元
# backend.tf
terraform {
  backend "s3" {
    bucket         = "myorg-terraform-state"
    key            = "prod/networking/terraform.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
    kms_key_id     = "alias/terraform-state"
  }
}

State 分割戦略

graph TD
    L1["Layer 1: Shared Infrastructure<br/>(VPC, IAM, DNS)<br/>変更頻度: 低 / 影響範囲: 全サービス"]:::layer1
    L2["Layer 2: Data Layer<br/>(RDS, ElastiCache, DynamoDB)<br/>変更頻度: 低 / 影響範囲: データ関連"]:::layer2
    L3["Layer 3: Compute Layer<br/>(ECS, Lambda, ALB)<br/>変更頻度: 高 / 影響範囲: 個別サービス"]:::layer3
    L4["Layer 4: Monitoring<br/>(CloudWatch, Alarms)<br/>変更頻度: 中 / 影響範囲: 運用"]:::layer4

    L1 --- S1[("prod/shared/<br/>terraform.tfstate")]:::state
    L2 --- S2[("prod/data/<br/>terraform.tfstate")]:::state
    L3 --- S3[("prod/compute/{service}/<br/>terraform.tfstate")]:::state
    L4 --- S4[("prod/monitoring/<br/>terraform.tfstate")]:::state

    L3 --> L2
    L3 --> L1
    L2 --> L1
    L4 --> L3

    classDef layer1 fill:#1A237E,stroke:#0D47A1,color:#FFFFFF
    classDef layer2 fill:#1B5E20,stroke:#2E7D32,color:#FFFFFF
    classDef layer3 fill:#E65100,stroke:#F57C00,color:#FFFFFF
    classDef layer4 fill:#4A148C,stroke:#7B1FA2,color:#FFFFFF
    classDef state fill:#ECEFF1,stroke:#607D8B,color:#37474F
# Layer 間のデータ参照は data source で
data "terraform_remote_state" "networking" {
  backend = "s3"
  config = {
    bucket = "myorg-terraform-state"
    key    = "prod/shared/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

resource "aws_ecs_service" "api" {
  # ネットワーク層から VPC 情報を参照
  network_configuration {
    subnets = data.terraform_remote_state.networking.outputs.private_subnet_ids
  }
}

Terragrunt によるDRY化

Terragrunt の構造

graph TD
    Root["infrastructure/"] --> TG["terragrunt.hcl
ルート設定"] Root --> Envs["environments/"] Root --> Mods["modules/
共通モジュール"] Envs --> Dev["dev/"] Envs --> Stg["staging/"] Envs --> Prod["prod/"] Dev --> DevEnv["env.hcl
環境変数"] Dev --> DevVPC["vpc/
terragrunt.hcl"] Dev --> DevECS["ecs/
terragrunt.hcl"] Dev --> DevRDS["rds/
terragrunt.hcl"] Stg --> StgEnv["env.hcl"] Stg --> StgVPC["vpc/"] Stg --> StgECS["ecs/"] Stg --> StgRDS["rds/"] Prod --> PrdEnv["env.hcl"] Prod --> PrdVPC["vpc/"] Prod --> PrdECS["ecs/"] Prod --> PrdRDS["rds/"] style Root fill:#1e293b,stroke:#475569,color:#f8fafc style TG fill:#fef3c7,stroke:#d97706,stroke-width:2px,color:#92400e style Envs fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style Mods fill:#d1fae5,stroke:#059669,color:#065f46 style Dev fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style Stg fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af style Prod fill:#dbeafe,stroke:#2563eb,stroke-width:2px,color:#1e40af
# terragrunt.hcl (ルート)
remote_state {
  backend = "s3"
  generate = {
    path      = "backend.tf"
    if_exists = "overwrite_terragrunt"
  }
  config = {
    bucket         = "myorg-terraform-state"
    key            = "${path_relative_to_include()}/terraform.tfstate"
    region         = "ap-northeast-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
  }
}

generate "provider" {
  path      = "provider.tf"
  if_exists = "overwrite_terragrunt"
  contents  = <<EOF
provider "aws" {
  region = "ap-northeast-1"
  default_tags {
    tags = {
      ManagedBy   = "terraform"
      Environment = "${local.environment}"
    }
  }
}
EOF
}
# environments/prod/env.hcl
locals {
  environment    = "prod"
  account_id     = "123456789012"
  vpc_cidr       = "10.0.0.0/16"
  instance_class = "db.r6g.xlarge"
}
# environments/prod/ecs/terragrunt.hcl
include "root" {
  path = find_in_parent_folders()
}

locals {
  env_vars = read_terragrunt_config(find_in_parent_folders("env.hcl"))
}

terraform {
  source = "../../../modules//compute/ecs-service"
}

dependency "vpc" {
  config_path = "../vpc"
}

dependency "rds" {
  config_path = "../rds"
}

inputs = {
  service_name    = "order-service"
  container_image = "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/order-service:v1.0.0"
  desired_count   = 4
  cpu             = 512
  memory          = 1024
  vpc_id          = dependency.vpc.outputs.vpc_id
  subnet_ids      = dependency.vpc.outputs.private_subnet_ids

  environment_variables = {
    DATABASE_URL = dependency.rds.outputs.connection_string
    NODE_ENV     = local.env_vars.locals.environment
  }
}

Remote Backend の比較

Backend特徴適用場面
S3 + DynamoDBAWS ネイティブ、低コストAWS メインのプロジェクト
Terraform CloudSaaS、実行環境付きチーム開発、ガバナンス重視
GCSGoogle Cloud ネイティブGCP メインのプロジェクト
Azure BlobAzure ネイティブAzure メインのプロジェクト
Consulオンプレ対応マルチクラウド、オンプレミス

まとめ

パターン用途規模
モジュール設計再利用・標準化全プロジェクト
Workspace環境分離(簡易)小規模
ディレクトリ分離環境分離(安全)中〜大規模
State 分割爆発半径の最小化中〜大規模
TerragruntDRY 化・依存管理大規模

チェックリスト

  • Terraform モジュールを単一責任で設計できる
  • State の分割戦略を説明できる
  • Remote Backend(S3 + DynamoDB)を設定できる
  • Terragrunt で環境間の重複を排除できる
  • terraform_remote_state でレイヤー間の依存を解決できる

次のステップへ

次のレッスンでは、AWS CDK や Pulumi を使ったプログラマブルな IaC アプローチを学びます。