【保存版】Terraform countの使い方完全ガイド:15個の実践的なユースケースで学ぶ効率的なリソース管理

Terraform countとは?実務で使える基礎知識

countパラメータの基本的な機能と特徴

Terraform countは、同一のリソースを複数作成する際に使用する機能です。countパラメータを使用することで、同じ設定のリソースを指定した数だけ簡単に作成できます。

基本的な構文:

resource "aws_instance" "web" {
  count         = 3  # 3つのインスタンスを作成
  ami           = "ami-0c55b159cbfafe1f0"
  instance_type = "t2.micro"

  tags = {
    Name = "web-server-${count.index}"  # 0から始まるインデックスを使用
  }
}

countの主な特徴:

  • 0から始まるインデックス(count.index)が自動的に提供される
  • リソース名は配列形式で参照(web[0], web[1], web[2])
  • 数値による制御が可能(条件分岐との組み合わせ)

countを使用するメリット:コード量削減とメンテナンス性向上

  1. コードの簡素化
  • 同じリソース定義の繰り返しを防止
  • DRY(Don’t Repeat Yourself)原則の実現
  • 変更時の作業量削減
  1. 動的なリソース管理
  • 環境変数による制御が容易
  • スケーリングが簡単
   variable "instance_count" {
     description = "Number of instances to create"
     type        = number
     default     = 2
   }

   resource "aws_instance" "app" {
     count = var.instance_count
     # ... その他の設定
   }
  1. メンテナンス性の向上
  • 一括変更が可能
  • コードの見通しが良好
  • バージョン管理がしやすい

countとfor_eachの違いと使い方

機能countfor_each
データ型数値マップまたはセット
インデックス参照数値インデックスキーによる参照
リソース削除時の動作インデックスが詰められるキーに基づいて個別に管理
ユースケース同一設定の複製異なる設定の一括作成

for_eachの使用例:

resource "aws_instance" "web" {
  for_each = {
    "prod" = "t2.medium"
    "dev"  = "t2.micro"
  }

  instance_type = each.value
  tags = {
    Name = "web-${each.key}"
  }
}

選択のポイント:

  1. 同一設定の複製 → count
  2. 個別の設定が必要 → for_each
  3. リソースの追加/削除が頻繁 → for_each
  4. シンプルな数値制御 → count

Terraform countの基本的な使い方:ステップバイプラクティス

count構文の基本パターンと記述方法

countの基本構文には以下のパターンがあります:

# 基本的な数値指定
resource "aws_subnet" "public" {
  count             = 3
  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]
}

# 変数による制御
resource "aws_instance" "web" {
  count         = var.environment == "production" ? 3 : 1
  instance_type = "t2.micro"
}

# リストの長さに基づく指定
resource "aws_iam_user" "developers" {
  count = length(var.developer_names)
  name  = var.developer_names[count.index]
}

count.indexの活用方法と注意点

count.indexの高度な使用方法:

# CIDRブロックの動的生成
resource "aws_subnet" "private" {
  count             = 3
  vpc_id            = aws_vpc.main.id
  # 10.0.1.0/24, 10.0.2.0/24, 10.0.3.0/24
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)

  tags = {
    Name = format("private-subnet-%d", count.index + 1)
    Tier = count.index < 2 ? "app" : "db"
  }
}

# インスタンスタイプの動的割り当て
resource "aws_instance" "cluster" {
  count = 3
  instance_type = count.index == 0 ? "t2.medium" : "t2.micro"  # マスターノードは大きめのインスタンス
}

注意点:

  • indexは0から始まる
  • リソース削除時にインデックスが再割り当て
  • 参照時は必ずインデックスを指定

条件分岐とcountの組み合わせテクニック

  1. 環境による制御:
resource "aws_instance" "app" {
  count = var.environment == "production" ? 3 : 1

  instance_type = var.environment == "production" ? "t2.medium" : "t2.micro"

  tags = {
    Environment = var.environment
    Role        = count.index == 0 ? "primary" : "secondary"
  }
}
  1. リソースの条件付き作成:
# バックアップが必要な場合のみS3バケットを作成
resource "aws_s3_bucket" "backup" {
  count  = var.enable_backup ? 1 : 0
  bucket = "backup-${var.environment}-${count.index}"
}

# 開発環境では不要なリソースをスキップ
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
  count = var.environment != "development" ? 1 : 0

  alarm_name          = "high-cpu-usage"
  comparison_operator = "GreaterThanThreshold"
  threshold           = "80"
}
  1. 複数条件の組み合わせ:
locals {
  # 本番環境かつバックアップ有効時のみ作成
  backup_count = var.environment == "production" && var.enable_backup ? 3 : 0
}

resource "aws_ebs_volume" "backup" {
  count             = local.backup_count
  availability_zone = data.aws_availability_zones.available.names[count.index]
  size              = 100

  tags = {
    Name = "backup-volume-${count.index + 1}"
  }
}

実践的なユースケースで学ぶTerraform count活用法

複数のEC2インスタンスを効率的にデプロイ

locals {
  instance_configs = {
    small  = { count = 2, type = "t2.micro" }
    medium = { count = 3, type = "t2.medium" }
  }
}

resource "aws_instance" "app_servers" {
  count         = local.instance_configs[var.size].count
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = local.instance_configs[var.size].type

  user_data = <<-EOF
              #!/bin/bash
              echo "Server ${count.index + 1}" > /var/www/html/index.html
              EOF

  tags = {
    Name = "app-server-${count.index + 1}"
    Role = count.index == 0 ? "primary" : "secondary"
  }
}

異なるAZに同じリソースを展開

data "aws_availability_zones" "available" {
  state = "available"
}

resource "aws_subnet" "private" {
  count             = 3
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "private-subnet-${data.aws_availability_zones.available.names[count.index]}"
    Tier = "application"
  }
}

resource "aws_nat_gateway" "main" {
  count         = 3
  allocation_id = aws_eip.nat[count.index].id
  subnet_id     = aws_subnet.public[count.index].id

  depends_on = [aws_internet_gateway.main]

  tags = {
    Name = "nat-gateway-${data.aws_availability_zones.available.names[count.index]}"
  }
}

環境ごとのリソース数制御の実装

locals {
  environments = {
    dev     = { instance_count = 1, instance_type = "t2.micro" }
    staging = { instance_count = 2, instance_type = "t2.small" }
    prod    = { instance_count = 3, instance_type = "t2.medium" }
  }

  current_env = local.environments[var.environment]
}

resource "aws_instance" "application" {
  count         = local.current_env.instance_count
  ami           = data.aws_ami.amazon_linux_2.id
  instance_type = local.current_env.instance_type

  root_block_device {
    volume_size = var.environment == "prod" ? 100 : 50
  }

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

resource "aws_ebs_volume" "data" {
  count             = var.environment == "prod" ? local.current_env.instance_count : 0
  availability_zone = aws_instance.application[count.index].availability_zone
  size              = 200
  type              = "gp3"

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

resource "aws_cloudwatch_metric_alarm" "cpu_high" {
  count               = var.environment == "prod" ? local.current_env.instance_count : 0
  alarm_name          = "${var.environment}-cpu-utilization-high-${count.index + 1}"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = "2"
  metric_name         = "CPUUtilization"
  namespace           = "AWS/EC2"
  period              = "300"
  statistic           = "Average"
  threshold           = "80"

  dimensions = {
    InstanceId = aws_instance.application[count.index].id
  }
}

Terraform countのベストプラクティスと注意点

countを使用する際のコーディング規約

# 1. 変数による制御
variable "instance_count" {
  description = "Number of instances to launch"
  type        = number
  default     = 2

  validation {
    condition     = var.instance_count > 0 && var.instance_count <= 10
    error_message = "Instance count must be between 1 and 10."
  }
}

# 2. ローカル変数でロジックを整理
locals {
  is_production = var.environment == "production"
  server_count  = local.is_production ? var.instance_count : 1
}

# 3. 命名規則の統一
resource "aws_instance" "web" {
  count = local.server_count

  tags = {
    Name = format("web-%s-%02d", var.environment, count.index + 1)
  }
}

よくあるエラーとトラブルシューティング

  1. インデックス参照エラー
# 問題のあるコード
resource "aws_eip" "lb" {
  count = var.create_eip ? 1 : 0
}

resource "aws_instance" "web" {
  # 条件が満たされない場合、エラー発生
  associate_public_ip_address = aws_eip.lb[0].id  
}

# 修正例
resource "aws_instance" "web" {
  associate_public_ip_address = var.create_eip ? aws_eip.lb[0].id : null
}
  1. count.indexの誤用
# 問題のあるコード
resource "aws_subnet" "private" {
  count             = 3
  cidr_block        = "10.0.${count.index}.0/24"  # 0から始まるため、意図しないCIDR
}

# 修正例
resource "aws_subnet" "private" {
  count             = 3
  cidr_block        = "10.0.${count.index + 1}.0/24"
}

メンテナンス性を考慮したモジュール設計

# モジュールの構造例
variable "instances" {
  description = "Map of instance configurations"
  type = map(object({
    count         = number
    instance_type = string
    tags         = map(string)
  }))
}

locals {
  # インスタンス設定の展開
  instance_configs = flatten([
    for name, config in var.instances : [
      for i in range(config.count) : {
        name         = name
        index        = i
        instance_type = config.instance_type
        tags         = merge(config.tags, {
          Name = format("%s-%02d", name, i + 1)
        })
      }
    ]
  ])
}

resource "aws_instance" "managed" {
  count         = length(local.instance_configs)
  instance_type = local.instance_configs[count.index].instance_type

  tags = local.instance_configs[count.index].tags

  lifecycle {
    create_before_destroy = true
  }
}

# 使用例
module "instances" {
  source = "./modules/instances"

  instances = {
    web = {
      count         = 2
      instance_type = "t2.micro"
      tags         = { Role = "web" }
    }
    app = {
      count         = 3
      instance_type = "t2.small"
      tags         = { Role = "app" }
    }
  }
}

実践演習:カウントを使用した具体的な実装例

マルチAZ構成のVPCサブネット作成

# AZの取得
data "aws_availability_zones" "available" {
  state = "available"
}

# VPC作成
resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "multi-az-vpc"
  }
}

# パブリックサブネット作成
resource "aws_subnet" "public" {
  count             = 3
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  map_public_ip_on_launch = true

  tags = {
    Name = "public-subnet-${data.aws_availability_zones.available.names[count.index]}"
    Type = "Public"
  }
}

# プライベートサブネット作成
resource "aws_subnet" "private" {
  count             = 3
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 3)
  availability_zone = data.aws_availability_zones.available.names[count.index]

  tags = {
    Name = "private-subnet-${data.aws_availability_zones.available.names[count.index]}"
    Type = "Private"
  }
}

# ルートテーブルの作成
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "public-rt"
  }
}

resource "aws_route_table" "private" {
  count  = 3
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "private-rt-${count.index + 1}"
  }
}

Auto Scaling用のセキュリティグループ設定

locals {
  ports = {
    http  = 80
    https = 443
    app   = 8080
  }
}

resource "aws_security_group" "asg" {
  name_prefix = "asg-sg-"
  vpc_id      = aws_vpc.main.id

  dynamic "ingress" {
    for_each = local.ports
    content {
      from_port   = ingress.value
      to_port     = ingress.value
      protocol    = "tcp"
      cidr_blocks = ["0.0.0.0/0"]
      description = "Allow ${ingress.key} traffic"
    }
  }

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

  tags = {
    Name = "asg-security-group"
  }
}

resource "aws_launch_template" "app" {
  name_prefix   = "app-template"
  image_id      = data.aws_ami.amazon_linux_2.id
  instance_type = "t2.micro"

  network_interfaces {
    associate_public_ip_address = true
    security_groups            = [aws_security_group.asg.id]
  }

  user_data = base64encode(<<-EOF
              #!/bin/bash
              yum update -y
              yum install -y httpd
              systemctl start httpd
              systemctl enable httpd
              EOF
  )
}

resource "aws_autoscaling_group" "app" {
  desired_capacity    = 3
  max_size           = 6
  min_size           = 1
  target_group_arns  = [aws_lb_target_group.app.arn]
  vpc_zone_identifier = aws_subnet.private[*].id

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

複数環境向けIAMロールの一括作成

locals {
  environments = ["dev", "staging", "prod"]

  role_policies = {
    dev = [
      "arn:aws:iam::aws:policy/AWSCloudTrailReadOnlyAccess",
      "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
    ]
    staging = [
      "arn:aws:iam::aws:policy/AWSCloudTrailReadOnlyAccess",
      "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess",
      "arn:aws:iam::aws:policy/AmazonRDSReadOnlyAccess"
    ]
    prod = [
      "arn:aws:iam::aws:policy/AWSCloudTrailReadOnlyAccess",
      "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess",
      "arn:aws:iam::aws:policy/AmazonRDSReadOnlyAccess",
      "arn:aws:iam::aws:policy/CloudWatchFullAccess"
    ]
  }
}

resource "aws_iam_role" "environment_role" {
  count = length(local.environments)
  name  = "${local.environments[count.index]}-application-role"

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

  tags = {
    Environment = local.environments[count.index]
  }
}

resource "aws_iam_role_policy_attachment" "environment_policy" {
  count = length(flatten([
    for env in local.environments : [
      for policy in local.role_policies[env] : {
        role = aws_iam_role.environment_role[index(local.environments, env)].name
        policy = policy
      }
    ]
  ]))

  role       = flatten([
    for env in local.environments : [
      for policy in local.role_policies[env] : {
        role = aws_iam_role.environment_role[index(local.environments, env)].name
        policy = policy
      }
    ]
  ])[count.index].role

  policy_arn = flatten([
    for env in local.environments : [
      for policy in local.role_policies[env] : {
        role = aws_iam_role.environment_role[index(local.environments, env)].name
        policy = policy
      }
    ]
  ])[count.index].policy
}