Skip to content

インフラ設計ガイド

1. 概要

1.1 目的

Infrastructure as Code (IaC) によるインフラストラクチャの設計・構築・管理における規律とベストプラクティスを定義する。

1.2 対象システム

  • システム名: 会議室予約システム
  • クラウド基盤: AWS
  • IaC ツール: Terraform
  • アーキテクチャ: ヘキサゴナルアーキテクチャ

1.3 基本原則

uml diagram

2. プロジェクト構造

2.1 ディレクトリ構造

uml diagram

2.2 命名規則

2.2.1 リソース命名

命名パターン: "{project}-{environment}-{service}-{resource}"

:
  - VPC: "meeting-room-prod-vpc"
  - Subnet: "meeting-room-prod-public-subnet-1a"
  - Security Group: "meeting-room-prod-web-sg"
  - RDS: "meeting-room-prod-postgres-primary"
  - ALB: "meeting-room-prod-app-alb"

2.2.2 Terraform ファイル命名

標準ファイル構成:
  - main.tf: メインの構成定義
  - variables.tf: 入力変数定義
  - outputs.tf: 出力値定義
  - versions.tf: プロバイダーバージョン指定
  - locals.tf: ローカル変数定義 (必要に応じて)
  - data.tf: データソース定義 (必要に応じて)

3. モジュール設計

3.1 モジュール化の原則

uml diagram

3.2 会議室予約システム モジュール構成

3.2.1 ネットワークモジュール

# modules/networking/vpc/main.tf
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-vpc"
  })
}

resource "aws_subnet" "public" {
  count = length(var.public_subnet_cidrs)

  vpc_id                  = aws_vpc.main.id
  cidr_block              = var.public_subnet_cidrs[count.index]
  availability_zone       = var.availability_zones[count.index]
  map_public_ip_on_launch = true

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-public-subnet-${count.index + 1}"
    Type = "Public"
  })
}

resource "aws_subnet" "private" {
  count = length(var.private_subnet_cidrs)

  vpc_id            = aws_vpc.main.id
  cidr_block        = var.private_subnet_cidrs[count.index]
  availability_zone = var.availability_zones[count.index]

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-private-subnet-${count.index + 1}"
    Type = "Private"
  })
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-igw"
  })
}

resource "aws_nat_gateway" "main" {
  count = var.enable_nat_gateway ? length(aws_subnet.public) : 0

  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-nat-gw-${count.index + 1}"
  })

  depends_on = [aws_internet_gateway.main]
}

resource "aws_eip" "nat" {
  count = var.enable_nat_gateway ? length(aws_subnet.public) : 0

  domain = "vpc"

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-nat-eip-${count.index + 1}"
  })

  depends_on = [aws_internet_gateway.main]
}

3.2.2 コンピュートモジュール

# modules/compute/web-app/main.tf
resource "aws_launch_template" "main" {
  name_prefix   = "${var.name_prefix}-lt"
  image_id      = var.ami_id
  instance_type = var.instance_type
  key_name      = var.key_name

  vpc_security_group_ids = [aws_security_group.web.id]

  user_data = base64encode(templatefile("${path.module}/user_data.sh", {
    database_url = var.database_url
    app_version  = var.app_version
    environment  = var.environment
  }))

  tag_specifications {
    resource_type = "instance"
    tags = merge(var.tags, {
      Name = "${var.name_prefix}-instance"
    })
  }

  lifecycle {
    create_before_destroy = true
    precondition {
      condition     = contains(["t3.micro", "t3.small", "t3.medium"], var.instance_type)
      error_message = "Instance type must be t3.micro, t3.small, or t3.medium for cost optimization."
    }
  }
}

resource "aws_autoscaling_group" "main" {
  name             = "${var.name_prefix}-asg"
  vpc_zone_identifier = var.subnet_ids
  target_group_arns   = [aws_lb_target_group.main.arn]
  health_check_type   = "ELB"
  health_check_grace_period = 300

  min_size         = var.min_size
  max_size         = var.max_size
  desired_capacity = var.desired_capacity

  launch_template {
    id      = aws_launch_template.main.id
    version = "$Latest"
  }

  instance_refresh {
    strategy = "Rolling"
    preferences {
      min_healthy_percentage = 50
      instance_warmup        = 300
    }
  }

  tag {
    key                 = "Name"
    value               = "${var.name_prefix}-asg"
    propagate_at_launch = false
  }

  dynamic "tag" {
    for_each = var.tags
    content {
      key                 = tag.key
      value               = tag.value
      propagate_at_launch = true
    }
  }

  lifecycle {
    create_before_destroy = true
    postcondition {
      condition     = length(self.availability_zones) > 1
      error_message = "Auto Scaling Group must span multiple AZs for high availability."
    }
  }
}

resource "aws_lb" "main" {
  name               = "${var.name_prefix}-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = var.subnet_ids

  enable_deletion_protection = var.environment == "prod" ? true : false

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-alb"
  })
}

resource "aws_lb_target_group" "main" {
  name     = "${var.name_prefix}-tg"
  port     = 8080
  protocol = "HTTP"
  vpc_id   = var.vpc_id

  health_check {
    enabled             = true
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 5
    interval            = 30
    path                = "/health"
    matcher             = "200"
    port                = "traffic-port"
    protocol            = "HTTP"
  }

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-tg"
  })
}

resource "aws_lb_listener" "main" {
  load_balancer_arn = aws_lb.main.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.main.arn
  }
}

3.2.3 データベースモジュール

# modules/database/postgresql/main.tf
resource "aws_db_subnet_group" "main" {
  name       = "${var.name_prefix}-db-subnet-group"
  subnet_ids = var.subnet_ids

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-db-subnet-group"
  })
}

resource "aws_db_parameter_group" "main" {
  family = "postgres15"
  name   = "${var.name_prefix}-db-params"

  parameter {
    name  = "log_statement"
    value = "all"
  }

  parameter {
    name  = "log_min_duration_statement"
    value = "1000"
  }

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-db-params"
  })
}

resource "aws_db_instance" "main" {
  identifier = "${var.name_prefix}-db"

  allocated_storage     = var.allocated_storage
  max_allocated_storage = var.max_allocated_storage
  storage_type          = "gp3"
  storage_encrypted     = true

  engine         = "postgres"
  engine_version = "15.4"
  instance_class = var.instance_class

  db_name  = var.database_name
  username = var.database_username
  password = var.database_password

  db_subnet_group_name   = aws_db_subnet_group.main.name
  vpc_security_group_ids = [aws_security_group.database.id]
  parameter_group_name   = aws_db_parameter_group.main.name

  backup_retention_period = var.backup_retention_period
  backup_window          = var.backup_window
  maintenance_window     = var.maintenance_window

  skip_final_snapshot       = var.environment != "prod"
  final_snapshot_identifier = var.environment == "prod" ? "${var.name_prefix}-db-final-snapshot" : null

  deletion_protection = var.environment == "prod" ? true : false

  performance_insights_enabled = var.environment == "prod" ? true : false
  monitoring_interval         = var.environment == "prod" ? 60 : 0

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-db"
  })

  lifecycle {
    prevent_destroy = false
    precondition {
      condition     = var.allocated_storage >= 20
      error_message = "Database must have at least 20GB of storage."
    }
  }
}

resource "aws_db_instance" "replica" {
  count = var.create_replica ? 1 : 0

  identifier = "${var.name_prefix}-db-replica"

  replicate_source_db = aws_db_instance.main.identifier
  instance_class      = var.replica_instance_class

  skip_final_snapshot = true
  deletion_protection = false

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-db-replica"
  })
}

3.3 入力検証とセキュリティ

# modules/compute/web-app/variables.tf
variable "instance_type" {
  description = "EC2 instance type"
  type        = string

  validation {
    condition = contains([
      "t3.micro", "t3.small", "t3.medium", "t3.large",
      "m5.large", "m5.xlarge", "m5.2xlarge"
    ], var.instance_type)
    error_message = "Instance type must be a supported type for this application."
  }
}

variable "min_size" {
  description = "Minimum number of instances in ASG"
  type        = number

  validation {
    condition     = var.min_size > 0
    error_message = "Minimum size must be greater than 0 to avoid outages."
  }

  validation {
    condition     = var.min_size <= 10
    error_message = "Minimum size must be 10 or fewer for cost control."
  }
}

variable "environment" {
  description = "Environment name"
  type        = string

  validation {
    condition     = contains(["dev", "stage", "prod"], var.environment)
    error_message = "Environment must be one of: dev, stage, prod."
  }
}

variable "vpc_cidr" {
  description = "CIDR block for VPC"
  type        = string

  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "VPC CIDR must be a valid CIDR block."
  }
}

4. 環境管理

4.1 環境分離戦略

uml diagram

4.2 グローバル変数管理

# environments/global/variables.tf
locals {
  project_name = "meeting-room"

  common_tags = {
    Project     = local.project_name
    ManagedBy   = "terraform"
    Owner       = "platform-team"
    Repository  = "meeting-room-infrastructure"
  }

  environments = {
    dev = {
      name                = "dev"
      instance_type       = "t3.micro"
      min_size           = 1
      max_size           = 2
      desired_capacity   = 1
      db_instance_class  = "db.t3.micro"
      backup_retention   = 7
      multi_az          = false
      create_replica    = false
    }

    stage = {
      name                = "stage"
      instance_type       = "t3.small"
      min_size           = 2
      max_size           = 4
      desired_capacity   = 2
      db_instance_class  = "db.t3.small"
      backup_retention   = 7
      multi_az          = true
      create_replica    = false
    }

    prod = {
      name                = "prod"
      instance_type       = "t3.medium"
      min_size           = 2
      max_size           = 8
      desired_capacity   = 4
      db_instance_class  = "db.r6g.large"
      backup_retention   = 30
      multi_az          = true
      create_replica    = true
    }
  }

  vpc_configs = {
    dev = {
      vpc_cidr             = "10.0.0.0/16"
      public_subnet_cidrs  = ["10.0.1.0/24", "10.0.2.0/24"]
      private_subnet_cidrs = ["10.0.3.0/24", "10.0.4.0/24"]
      availability_zones   = ["ap-northeast-1a", "ap-northeast-1c"]
    }

    stage = {
      vpc_cidr             = "10.1.0.0/16"
      public_subnet_cidrs  = ["10.1.1.0/24", "10.1.2.0/24"]
      private_subnet_cidrs = ["10.1.3.0/24", "10.1.4.0/24"]
      availability_zones   = ["ap-northeast-1a", "ap-northeast-1c"]
    }

    prod = {
      vpc_cidr             = "10.2.0.0/16"
      public_subnet_cidrs  = ["10.2.1.0/24", "10.2.2.0/24"]
      private_subnet_cidrs = ["10.2.3.0/24", "10.2.4.0/24"]
      availability_zones   = ["ap-northeast-1a", "ap-northeast-1c"]
    }
  }
}

output "project_config" {
  value = {
    name        = local.project_name
    tags        = local.common_tags
    environments = local.environments
    vpc_configs = local.vpc_configs
  }
}

4.3 環境固有の構成

# environments/prod/main.tf
terraform {
  required_version = ">= 1.0"

  backend "s3" {
    bucket         = "meeting-room-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "ap-northeast-1"
    dynamodb_table = "meeting-room-terraform-locks"
    encrypt        = true
  }

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

provider "aws" {
  region = "ap-northeast-1"

  default_tags {
    tags = local.common_tags
  }
}

# グローバル設定の読み込み
data "terraform_remote_state" "global" {
  backend = "s3"

  config = {
    bucket = "meeting-room-terraform-state"
    key    = "global/terraform.tfstate"
    region = "ap-northeast-1"
  }
}

locals {
  environment = "prod"
  global_config = data.terraform_remote_state.global.outputs.project_config
  env_config = local.global_config.environments[local.environment]
  vpc_config = local.global_config.vpc_configs[local.environment]
  common_tags = merge(local.global_config.tags, {
    Environment = local.environment
  })
  name_prefix = "${local.global_config.name}-${local.environment}"
}

# ネットワークモジュール
module "vpc" {
  source = "../../modules/networking/vpc"

  name_prefix = local.name_prefix

  vpc_cidr             = local.vpc_config.vpc_cidr
  public_subnet_cidrs  = local.vpc_config.public_subnet_cidrs
  private_subnet_cidrs = local.vpc_config.private_subnet_cidrs
  availability_zones   = local.vpc_config.availability_zones

  enable_nat_gateway = true

  tags = local.common_tags
}

# データベースモジュール
module "database" {
  source = "../../modules/database/postgresql"

  name_prefix = local.name_prefix

  subnet_ids = module.vpc.private_subnet_ids
  vpc_id     = module.vpc.vpc_id

  instance_class      = local.env_config.db_instance_class
  allocated_storage   = 100
  max_allocated_storage = 1000

  database_name     = "meetingroom"
  database_username = local.db_credentials.username
  database_password = local.db_credentials.password

  backup_retention_period = local.env_config.backup_retention
  multi_az               = local.env_config.multi_az
  create_replica         = local.env_config.create_replica

  environment = local.environment
  tags        = local.common_tags
}

# Webアプリケーションモジュール
module "web_app" {
  source = "../../modules/compute/web-app"

  name_prefix = local.name_prefix

  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.public_subnet_ids

  instance_type    = local.env_config.instance_type
  min_size         = local.env_config.min_size
  max_size         = local.env_config.max_size
  desired_capacity = local.env_config.desired_capacity

  database_url = "postgresql://${local.db_credentials.username}:${local.db_credentials.password}@${module.database.endpoint}/meetingroom"
  app_version  = var.app_version

  environment = local.environment
  tags        = local.common_tags

  depends_on = [module.database]
}

# Secrets Managerからのデータベース認証情報取得
data "aws_secretsmanager_secret_version" "db_credentials" {
  secret_id = "${local.name_prefix}-db-credentials"
}

locals {
  db_credentials = jsondecode(data.aws_secretsmanager_secret_version.db_credentials.secret_string)
}

5. セキュリティ設計

5.1 シークレット管理

# modules/security/secrets-manager/main.tf
resource "aws_secretsmanager_secret" "db_credentials" {
  name        = "${var.name_prefix}-db-credentials"
  description = "Database credentials for ${var.name_prefix}"

  recovery_window_in_days = var.environment == "prod" ? 30 : 0

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-db-credentials"
    Type = "Database"
  })
}

resource "aws_secretsmanager_secret_version" "db_credentials" {
  secret_id = aws_secretsmanager_secret.db_credentials.id

  secret_string = jsonencode({
    username = var.database_username
    password = var.database_password
  })

  lifecycle {
    ignore_changes = [secret_string]
  }
}

# 自動ローテーション(本番環境のみ)
resource "aws_secretsmanager_secret_rotation" "db_credentials" {
  count = var.environment == "prod" ? 1 : 0

  secret_id           = aws_secretsmanager_secret.db_credentials.id
  rotation_lambda_arn = var.rotation_lambda_arn

  rotation_rules {
    automatically_after_days = 30
  }
}

5.2 IAM ロール設計

# modules/security/iam/main.tf
# EC2インスタンス用のIAMロール
resource "aws_iam_role" "ec2_role" {
  name = "${var.name_prefix}-ec2-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })

  tags = var.tags
}

# Secrets Manager読み取り権限
resource "aws_iam_policy" "secrets_manager_policy" {
  name = "${var.name_prefix}-secrets-manager-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "secretsmanager:GetSecretValue",
          "secretsmanager:DescribeSecret"
        ]
        Resource = [
          "arn:aws:secretsmanager:${var.region}:${var.account_id}:secret:${var.name_prefix}-db-credentials*"
        ]
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "secrets_manager_attachment" {
  policy_arn = aws_iam_policy.secrets_manager_policy.arn
  role       = aws_iam_role.ec2_role.name
}

# CloudWatch Logs権限
resource "aws_iam_policy" "cloudwatch_logs_policy" {
  name = "${var.name_prefix}-cloudwatch-logs-policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents",
          "logs:DescribeLogStreams"
        ]
        Resource = [
          "arn:aws:logs:${var.region}:${var.account_id}:log-group:/aws/ec2/${var.name_prefix}*"
        ]
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "cloudwatch_logs_attachment" {
  policy_arn = aws_iam_policy.cloudwatch_logs_policy.arn
  role       = aws_iam_role.ec2_role.name
}

resource "aws_iam_instance_profile" "ec2_profile" {
  name = "${var.name_prefix}-ec2-profile"
  role = aws_iam_role.ec2_role.name

  tags = var.tags
}

5.3 セキュリティグループ設計

# modules/security/security-groups/main.tf
# ALB用セキュリティグループ
resource "aws_security_group" "alb" {
  name_prefix = "${var.name_prefix}-alb-"
  vpc_id      = var.vpc_id

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

  ingress {
    description = "HTTPS"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

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

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-alb-sg"
    Type = "LoadBalancer"
  })

  lifecycle {
    create_before_destroy = true
  }
}

# Web層用セキュリティグループ
resource "aws_security_group" "web" {
  name_prefix = "${var.name_prefix}-web-"
  vpc_id      = var.vpc_id

  ingress {
    description     = "HTTP from ALB"
    from_port       = 8080
    to_port         = 8080
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }

  ingress {
    description = "SSH"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = [var.vpc_cidr]
  }

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

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-web-sg"
    Type = "WebServer"
  })

  lifecycle {
    create_before_destroy = true
  }
}

# データベース用セキュリティグループ
resource "aws_security_group" "database" {
  name_prefix = "${var.name_prefix}-db-"
  vpc_id      = var.vpc_id

  ingress {
    description     = "PostgreSQL from Web"
    from_port       = 5432
    to_port         = 5432
    protocol        = "tcp"
    security_groups = [aws_security_group.web.id]
  }

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-db-sg"
    Type = "Database"
  })

  lifecycle {
    create_before_destroy = true
  }
}

6. 監視・ロギング

6.1 CloudWatch 監視

# modules/monitoring/cloudwatch/main.tf
# ALB監視
resource "aws_cloudwatch_metric_alarm" "alb_response_time" {
  alarm_name          = "${var.name_prefix}-alb-response-time"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "TargetResponseTime"
  namespace           = "AWS/ApplicationELB"
  period              = "300"
  statistic           = "Average"
  threshold           = "2.0"
  alarm_description   = "This metric monitors ALB response time"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    LoadBalancer = var.alb_arn_suffix
  }

  tags = var.tags
}

resource "aws_cloudwatch_metric_alarm" "alb_healthy_hosts" {
  alarm_name          = "${var.name_prefix}-alb-healthy-hosts"
  comparison_operator = "LessThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "HealthyHostCount"
  namespace           = "AWS/ApplicationELB"
  period              = "300"
  statistic           = "Average"
  threshold           = "1"
  alarm_description   = "This metric monitors healthy host count"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    TargetGroup  = var.target_group_arn_suffix
    LoadBalancer = var.alb_arn_suffix
  }

  tags = var.tags
}

# データベース監視
resource "aws_cloudwatch_metric_alarm" "database_cpu" {
  alarm_name          = "${var.name_prefix}-db-cpu"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/RDS"
  period              = "300"
  statistic           = "Average"
  threshold           = "80"
  alarm_description   = "This metric monitors database CPU utilization"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    DBInstanceIdentifier = var.db_instance_id
  }

  tags = var.tags
}

resource "aws_cloudwatch_metric_alarm" "database_connections" {
  alarm_name          = "${var.name_prefix}-db-connections"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "DatabaseConnections"
  namespace           = "AWS/RDS"
  period              = "300"
  statistic           = "Average"
  threshold           = "80"
  alarm_description   = "This metric monitors database connection count"
  alarm_actions       = [aws_sns_topic.alerts.arn]

  dimensions = {
    DBInstanceIdentifier = var.db_instance_id
  }

  tags = var.tags
}

# SNS通知設定
resource "aws_sns_topic" "alerts" {
  name = "${var.name_prefix}-alerts"

  tags = var.tags
}

resource "aws_sns_topic_subscription" "email_alerts" {
  count = length(var.alert_email_addresses)

  topic_arn = aws_sns_topic.alerts.arn
  protocol  = "email"
  endpoint  = var.alert_email_addresses[count.index]
}

# カスタムメトリクス
resource "aws_cloudwatch_log_group" "app_logs" {
  name              = "/aws/ec2/${var.name_prefix}/application"
  retention_in_days = var.log_retention_days

  tags = var.tags
}

resource "aws_cloudwatch_log_group" "access_logs" {
  name              = "/aws/ec2/${var.name_prefix}/access"
  retention_in_days = var.log_retention_days

  tags = var.tags
}

7. テスト戦略

7.1 単体テスト

// test/unit/vpc_test.go
package test

import (
    "testing"

    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestVPCModule(t *testing.T) {
    t.Parallel()

    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
        TerraformDir: "../modules/networking/vpc",
        Vars: map[string]interface{}{
            "name_prefix": "test-vpc",
            "vpc_cidr":    "10.0.0.0/16",
            "public_subnet_cidrs": []string{
                "10.0.1.0/24",
                "10.0.2.0/24",
            },
            "private_subnet_cidrs": []string{
                "10.0.3.0/24",
                "10.0.4.0/24",
            },
            "availability_zones": []string{
                "ap-northeast-1a",
                "ap-northeast-1c",
            },
            "enable_nat_gateway": true,
            "tags": map[string]string{
                "Test":        "true",
                "Environment": "test",
            },
        },
    })

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    // 出力値の検証
    vpcId := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, vpcId, "VPC ID should not be empty")

    publicSubnetIds := terraform.OutputList(t, terraformOptions, "public_subnet_ids")
    assert.Len(t, publicSubnetIds, 2, "Should create 2 public subnets")

    privateSubnetIds := terraform.OutputList(t, terraformOptions, "private_subnet_ids")
    assert.Len(t, privateSubnetIds, 2, "Should create 2 private subnets")
}

7.2 統合テスト

// test/integration/full_stack_test.go
package test

import (
    "fmt"
    "testing"
    "time"

    http_helper "github.com/gruntwork-io/terratest/modules/http-helper"
    "github.com/gruntwork-io/terratest/modules/random"
    "github.com/gruntwork-io/terratest/modules/terraform"
    test_structure "github.com/gruntwork-io/terratest/modules/test-structure"
)

const envDir = "../environments/stage"

func TestFullStackDeployment(t *testing.T) {
    t.Parallel()

    // ステージベースのテスト実行
    defer test_structure.RunTestStage(t, "cleanup", func() {
        terraformOptions := test_structure.LoadTerraformOptions(t, envDir)
        terraform.Destroy(t, terraformOptions)
    })

    test_structure.RunTestStage(t, "deploy", func() {
        uniqueId := random.UniqueId()
        terraformOptions := &terraform.Options{
            TerraformDir: envDir,
            Vars: map[string]interface{}{
                "app_version":        "test-" + uniqueId,
                "integration_test":   true,
                "environment_suffix": uniqueId,
            },
        }

        test_structure.SaveTerraformOptions(t, envDir, terraformOptions)
        terraform.InitAndApply(t, terraformOptions)
    })

    test_structure.RunTestStage(t, "validate", func() {
        terraformOptions := test_structure.LoadTerraformOptions(t, envDir)

        // ALB エンドポイントの取得
        albDnsName := terraform.Output(t, terraformOptions, "alb_dns_name")
        url := fmt.Sprintf("http://%s", albDnsName)

        // ヘルスチェック
        http_helper.HttpGetWithRetryWithCustomValidation(
            t,
            fmt.Sprintf("%s/health", url),
            nil,
            30,
            10*time.Second,
            func(statusCode int, body string) bool {
                return statusCode == 200
            },
        )

        // アプリケーション機能テスト
        http_helper.HttpGetWithRetryWithCustomValidation(
            t,
            fmt.Sprintf("%s/api/meeting-rooms", url),
            nil,
            10,
            5*time.Second,
            func(statusCode int, body string) bool {
                return statusCode == 200
            },
        )
    })
}

7.3 セキュリティテスト

// test/security/security_test.go
package test

import (
    "testing"

    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestSecurityConfiguration(t *testing.T) {
    t.Parallel()

    terraformOptions := terraform.WithDefaultRetryableErrors(t, &terraform.Options{
        TerraformDir: "../environments/prod",
        PlanFilePath: "./tfplan",
    })

    // terraform plan の実行
    terraform.InitAndPlan(t, terraformOptions)
    plan := terraform.ShowWithStruct(t, terraformOptions)

    // セキュリティ設定の検証
    t.Run("Database encryption", func(t *testing.T) {
        dbInstances := plan.ResourcesByType["aws_db_instance"]
        for _, instance := range dbInstances {
            storageEncrypted := instance.AttributeValues["storage_encrypted"]
            assert.True(t, storageEncrypted.(bool), "Database storage should be encrypted")
        }
    })

    t.Run("Security group rules", func(t *testing.T) {
        securityGroups := plan.ResourcesByType["aws_security_group"]
        for _, sg := range securityGroups {
            ingress := sg.AttributeValues["ingress"].([]interface{})
            for _, rule := range ingress {
                ruleMap := rule.(map[string]interface{})
                cidrBlocks := ruleMap["cidr_blocks"].([]interface{})

                // 0.0.0.0/0 からのSSH接続を禁止
                if ruleMap["from_port"].(float64) == 22 {
                    assert.NotContains(t, cidrBlocks, "0.0.0.0/0", 
                        "SSH should not be open to the world")
                }
            }
        }
    })

    t.Run("Deletion protection", func(t *testing.T) {
        dbInstances := plan.ResourcesByType["aws_db_instance"]
        for _, instance := range dbInstances {
            if instance.AttributeValues["identifier"].(string) == "meeting-room-prod-db" {
                deletionProtection := instance.AttributeValues["deletion_protection"]
                assert.True(t, deletionProtection.(bool), 
                    "Production database should have deletion protection enabled")
            }
        }
    })
}

8. CI/CD 統合

8.1 GitHub Actions ワークフロー

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

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

env:
  TF_VERSION: 1.5.0
  TF_IN_AUTOMATION: true

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  validate:
    name: Validate Terraform
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Format Check
        run: terraform fmt -check -recursive ./terraform/

      - name: Terraform Validate
        run: |
          find ./terraform -name "*.tf" -path "*/modules/*" -execdir terraform init -backend=false \;
          find ./terraform -name "*.tf" -path "*/modules/*" -execdir terraform validate \;

  plan:
    name: Plan Infrastructure
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    needs: [validate]

    strategy:
      matrix:
        environment: [dev, stage]

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-session-name: terraform-${{ matrix.environment }}
          aws-region: ap-northeast-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Init
        working-directory: ./terraform/environments/${{ matrix.environment }}
        run: terraform init

      - name: Terraform Plan
        working-directory: ./terraform/environments/${{ matrix.environment }}
        run: |
          terraform plan -out=tfplan -var="app_version=${{ github.sha }}"
          terraform show -no-color tfplan > plan.txt

      - name: Comment PR with Plan
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const plan = fs.readFileSync('./terraform/environments/${{ matrix.environment }}/plan.txt', 'utf8');
            const truncatedPlan = plan.length > 65000 ? plan.substring(0, 65000) + "\n...truncated" : plan;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## Terraform Plan (${{ matrix.environment }})

              \`\`\`
              ${truncatedPlan}
              \`\`\`
              `
            });

  deploy-dev:
    name: Deploy to Development
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    needs: [validate]
    environment: development

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-session-name: terraform-dev
          aws-region: ap-northeast-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Init
        working-directory: ./terraform/environments/dev
        run: terraform init

      - name: Terraform Apply
        working-directory: ./terraform/environments/dev
        run: terraform apply -auto-approve -var="app_version=${{ github.sha }}"

  deploy-stage:
    name: Deploy to Staging
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    needs: [validate]
    environment: staging

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-session-name: terraform-stage
          aws-region: ap-northeast-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Init
        working-directory: ./terraform/environments/stage
        run: terraform init

      - name: Terraform Apply
        working-directory: ./terraform/environments/stage
        run: terraform apply -auto-approve -var="app_version=${{ github.sha }}"

  deploy-prod:
    name: Deploy to Production
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    needs: [deploy-stage]
    environment: production

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }}
          role-session-name: terraform-prod
          aws-region: ap-northeast-1

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ env.TF_VERSION }}

      - name: Terraform Init
        working-directory: ./terraform/environments/prod
        run: terraform init

      - name: Terraform Plan
        working-directory: ./terraform/environments/prod
        run: terraform plan -out=tfplan -var="app_version=${{ github.sha }}"

      - name: Manual Approval Required
        run: |
          echo "Production deployment requires manual approval"
          echo "Review the plan and approve in GitHub Actions"

      - name: Terraform Apply
        working-directory: ./terraform/environments/prod
        run: terraform apply tfplan

8.2 OIDC 設定

# terraform/iam-oidc/main.tf
resource "aws_iam_openid_connect_provider" "github" {
  url = "https://token.actions.githubusercontent.com"

  client_id_list = [
    "sts.amazonaws.com",
  ]

  thumbprint_list = [
    "6938fd4d98bab03faadb97b34396831e3780aea1",
    "1c58a3a8518e8759bf075b76b750d4f2df264fcd"
  ]

  tags = {
    Name      = "github-actions-oidc"
    ManagedBy = "terraform"
  }
}

resource "aws_iam_role" "github_actions" {
  name = "github-actions-terraform"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRoleWithWebIdentity"
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.github.arn
        }
        Condition = {
          StringEquals = {
            "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
          }
          StringLike = {
            "token.actions.githubusercontent.com:sub" = "repo:organization/meeting-room-infrastructure:*"
          }
        }
      }
    ]
  })

  tags = {
    Name      = "github-actions-terraform"
    ManagedBy = "terraform"
  }
}

resource "aws_iam_role_policy" "github_actions_terraform" {
  name = "terraform-permissions"
  role = aws_iam_role.github_actions.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Action = [
          "ec2:*",
          "rds:*",
          "elasticloadbalancing:*",
          "autoscaling:*",
          "iam:*",
          "s3:*",
          "dynamodb:*",
          "secretsmanager:*",
          "logs:*",
          "cloudwatch:*",
          "sns:*"
        ]
        Resource = "*"
      }
    ]
  })
}

9. コスト最適化

9.1 リソース最適化

# modules/cost-optimization/spot-instances/main.tf
resource "aws_launch_template" "spot" {
  name_prefix = "${var.name_prefix}-spot-"

  image_id      = var.ami_id
  instance_type = var.instance_type
  key_name      = var.key_name

  vpc_security_group_ids = var.security_group_ids

  instance_market_options {
    market_type = "spot"
    spot_options {
      spot_instance_type = "one-time"
      max_price         = var.spot_price
    }
  }

  user_data = var.user_data

  tag_specifications {
    resource_type = "instance"
    tags = merge(var.tags, {
      Name = "${var.name_prefix}-spot-instance"
    })
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_autoscaling_group" "spot" {
  name = "${var.name_prefix}-spot-asg"

  vpc_zone_identifier = var.subnet_ids
  target_group_arns   = var.target_group_arns
  health_check_type   = "ELB"

  min_size         = var.min_size
  max_size         = var.max_size
  desired_capacity = var.desired_capacity

  mixed_instances_policy {
    launch_template {
      launch_template_specification {
        launch_template_id = aws_launch_template.spot.id
        version           = "$Latest"
      }
    }

    instances_distribution {
      on_demand_base_capacity                  = var.on_demand_base_capacity
      on_demand_percentage_above_base_capacity = var.on_demand_percentage
      spot_allocation_strategy                 = "diversified"
      spot_instance_pools                      = 4
    }
  }

  tag {
    key                 = "Name"
    value               = "${var.name_prefix}-spot-asg"
    propagate_at_launch = false
  }
}

9.2 スケジューリングと自動化

# modules/scheduling/auto-shutdown/main.tf
resource "aws_lambda_function" "auto_shutdown" {
  filename         = "auto_shutdown.zip"
  function_name    = "${var.name_prefix}-auto-shutdown"
  role            = aws_iam_role.lambda_role.arn
  handler         = "lambda_function.lambda_handler"
  runtime         = "python3.9"
  timeout         = 300

  source_code_hash = data.archive_file.auto_shutdown_zip.output_base64sha256

  environment {
    variables = {
      ENVIRONMENT = var.environment
      TAG_KEY     = "AutoShutdown"
      TAG_VALUE   = "enabled"
    }
  }

  tags = var.tags
}

resource "aws_cloudwatch_event_rule" "auto_shutdown_schedule" {
  count = var.environment == "dev" ? 1 : 0

  name                = "${var.name_prefix}-auto-shutdown"
  description         = "Schedule for auto shutdown of development resources"
  schedule_expression = "cron(0 18 ? * MON-FRI *)"  # 平日18時

  tags = var.tags
}

resource "aws_cloudwatch_event_target" "lambda_target" {
  count = var.environment == "dev" ? 1 : 0

  rule      = aws_cloudwatch_event_rule.auto_shutdown_schedule[0].name
  target_id = "AutoShutdownTarget"
  arn       = aws_lambda_function.auto_shutdown.arn
}

resource "aws_lambda_permission" "allow_cloudwatch" {
  count = var.environment == "dev" ? 1 : 0

  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.auto_shutdown.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.auto_shutdown_schedule[0].arn
}

10. 災害対策・冗長化

10.1 マルチ AZ 構成

uml diagram

10.2 バックアップ戦略

# modules/backup/automated-backup/main.tf
resource "aws_backup_vault" "main" {
  name        = "${var.name_prefix}-backup-vault"
  kms_key_arn = aws_kms_key.backup.arn

  tags = var.tags
}

resource "aws_kms_key" "backup" {
  description             = "KMS key for backup vault encryption"
  deletion_window_in_days = 30

  tags = merge(var.tags, {
    Name = "${var.name_prefix}-backup-kms"
  })
}

resource "aws_backup_plan" "main" {
  name = "${var.name_prefix}-backup-plan"

  rule {
    rule_name         = "daily_backup"
    target_vault_name = aws_backup_vault.main.name
    schedule          = "cron(0 2 ? * * *)"  # 毎日午前2時

    lifecycle {
      cold_storage_after = 30
      delete_after       = 120
    }

    recovery_point_tags = merge(var.tags, {
      BackupType = "Daily"
    })
  }

  rule {
    rule_name         = "weekly_backup"
    target_vault_name = aws_backup_vault.main.name
    schedule          = "cron(0 2 ? * SUN *)"  # 毎週日曜日午前2時

    lifecycle {
      cold_storage_after = 30
      delete_after       = 365
    }

    recovery_point_tags = merge(var.tags, {
      BackupType = "Weekly"
    })
  }

  tags = var.tags
}

resource "aws_backup_selection" "database" {
  iam_role_arn = aws_iam_role.backup.arn
  name         = "${var.name_prefix}-db-backup-selection"
  plan_id      = aws_backup_plan.main.id

  resources = [
    var.rds_db_instance_arn
  ]

  condition {
    string_equals {
      key   = "aws:ResourceTag/Environment"
      value = var.environment
    }
  }
}

11. まとめ

11.1 ベストプラクティス チェックリスト

インフラコード品質:
  - [ ] すべてのリソースがコードで定義されている
  - [ ] バージョン管理されている
  - [ ] 適切なコメントとドキュメントがある
  - [ ] 命名規則に従っている

モジュール設計:
  - [ ] 単一責任の原則に従っている
  - [ ] 再利用可能な設計になっている
  - [ ] 入力検証が実装されている
  - [ ] 適切な出力値が定義されている

セキュリティ:
  - [ ] シークレット管理が適切に実装されている
  - [ ] IAM権限が最小権限になっている
  - [ ] セキュリティグループが適切に設定されている
  - [ ] 暗号化が有効になっている

運用:
  - [ ] 監視・アラートが設定されている
  - [ ] ログ収集が実装されている
  - [ ] バックアップ戦略が定義されている
  - [ ] 災害対策が考慮されている

テスト:
  - [ ] 単体テストが実装されている
  - [ ] 統合テストが実装されている
  - [ ] セキュリティテストが実装されている
  - [ ] CI/CDパイプラインが構築されている

11.2 継続的改善

uml diagram

このインフラ設計ガイドに従うことで、会議室予約システムの安全で効率的なインフラストラクチャを構築・運用できます。