for式 — リスト・mapの変換とフィルタリング

1. 概要

この記事では、以下の内容を解説します。

  • for式の基本構文(リスト出力・map出力)
  • ifフィルタとの組み合わせ
  • for_eachに渡すmapをfor式で生成するパターン
  • localsブロックと組み合わせたデータ変換
  • for式でよく使う組み込み関数との連携
  • よくある間違いと対処法

for式は、リスト・mapなどのコレクションを変換・フィルタリングするためのTerraform専用の式です。for_eachと名前が似ていますが別物で、for式は値を生成する式であり、リソースを作成するものではありません。localsと組み合わせてデータを加工し、for_eachに渡すのが典型的な使い方です。


2. for式とは

for式はコレクション(リスト・map・set)の各要素を変換して、新しいリスト([...])またはmap({...})を生成する式です。

# リストを別のリストに変換
[for 変数 in コレクション : 変換後の値]

# mapを別のmapに変換
{for キー変数, 値変数 in コレクション : 新キー => 新値}

Pythonのリスト内包表記に相当する機能です。


3. 基本構文

リストを出力する([ ])

# 基本形: [for <変数> in <コレクション> : <式>]
locals {
  # 文字列リストを大文字に変換
  upper_names = [for name in ["web", "app", "db"] : upper(name)]
  # → ["WEB", "APP", "DB"]

  # 数値リストを2倍にする
  doubled = [for n in [1, 2, 3] : n * 2]
  # → [2, 4, 6]

  # 文字列を整形する
  az_labels = [for az in ["ap-northeast-1a", "ap-northeast-1c"] : substr(az, -1, 1)]
  # → ["a", "c"]
}

mapを出力する({ })

mapを出力するにはキー => 値の形式で記述し、波括弧{ }で囲みます。

locals {
  # リストからmapを生成(値をキーとして使う)
  name_map = { for name in ["web", "app", "db"] : name => "${name}-server" }
  # → { "web" = "web-server", "app" = "app-server", "db" = "db-server" }

  # mapのキーと値を入れ替える
  inverted = { for k, v in { a = 1, b = 2, c = 3 } : v => k }
  # → { 1 = "a", 2 = "b", 3 = "c" }
}

4. mapを反復する

コレクションがmapobjectの場合、for式でキーと値の両方を受け取れます。

variable "instance_types" {
  type = map(string)
  default = {
    dev = "t3.micro"
    stg = "t3.small"
    prd = "t3.medium"
  }
}

locals {
  # mapのキーと値を両方使う
  instance_labels = [
    for env, type in var.instance_types : "${env}: ${type}"
  ]
  # → ["dev: t3.micro", "prd: t3.medium", "stg: t3.small"]
  # ※ mapの反復順はキーのアルファベット順になります

  # mapのキーだけ取得(keys()関数と同等)
  env_names = [for env, _ in var.instance_types : env]
  # → ["dev", "prd", "stg"]
}

5. ifでフィルタリングする

for式の末尾にif <条件>を追加すると、条件を満たす要素だけを残せます。

variable "users" {
  type = list(object({
    name    = string
    enabled = bool
    role    = string
  }))
  default = [
    { name = "alice", enabled = true,  role = "admin"  },
    { name = "bob",   enabled = false, role = "viewer" },
    { name = "carol", enabled = true,  role = "viewer" },
  ]
}

locals {
  # enabledがtrueのユーザー名だけを抽出
  active_users = [for u in var.users : u.name if u.enabled]
  # → ["alice", "carol"]

  # adminロールのユーザーのみ抽出してmapに変換
  admin_map = { for u in var.users : u.name => u.role if u.role == "admin" }
  # → { "alice" = "admin" }
}

6. for_eachに渡すmapをfor式で生成する

for_eachにはリストを直接渡せません。for式でリスト→mapに変換してから渡すのが実務で最も多いパターンです。

オブジェクトのリストからmapを生成する

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

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

variable "subnet_configs" {
  description = "サブネット設定のリスト"
  type = list(object({
    name = string
    cidr = string
    az   = string
    tier = string  # "public" or "private"
  }))
  default = [
    { name = "public-1a",  cidr = "10.0.1.0/24",  az = "ap-northeast-1a", tier = "public"  },
    { name = "public-1c",  cidr = "10.0.2.0/24",  az = "ap-northeast-1c", tier = "public"  },
    { name = "private-1a", cidr = "10.0.11.0/24", az = "ap-northeast-1a", tier = "private" },
    { name = "private-1c", cidr = "10.0.12.0/24", az = "ap-northeast-1c", tier = "private" },
  ]
}

locals {
  # for式でリスト → map(name => object) に変換
  subnets_map = { for s in var.subnet_configs : s.name => s }
  # → {
  #     "public-1a"  = { name = "public-1a",  cidr = "10.0.1.0/24", ... }
  #     "public-1c"  = { name = "public-1c",  cidr = "10.0.2.0/24", ... }
  #     "private-1a" = { name = "private-1a", cidr = "10.0.11.0/24", ... }
  #     "private-1c" = { name = "private-1c", cidr = "10.0.12.0/24", ... }
  #   }

  # フィルタ込み: パブリックサブネットのみのmap
  public_subnets_map = { for s in var.subnet_configs : s.name => s if s.tier == "public" }
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name      = "main-vpc"
    ManagedBy = "terraform"
  }
}

resource "aws_subnet" "main" {
  for_each = local.subnets_map  # for式で変換したmapを渡す

  vpc_id            = aws_vpc.main.id
  cidr_block        = each.value.cidr
  availability_zone = each.value.az

  tags = {
    Name      = each.key
    Tier      = each.value.tier
    ManagedBy = "terraform"
  }
}

7. localsと組み合わせたデータ変換パターン

複数のfor式を段階的に組み合わせる

locals {
  # ステップ1: 生のリストをフィルタしてactiveなもののみ
  active_configs = [
    for c in var.server_configs : c if c.enabled
  ]

  # ステップ2: フィルタ後のリストをfor_each用のmapに変換
  active_configs_map = {
    for c in local.active_configs : c.name => c
  }
}

resource "aws_instance" "servers" {
  for_each = local.active_configs_map

  ami           = data.aws_ami.amazon_linux_2023.id
  instance_type = each.value.instance_type

  root_block_device {
    volume_size = each.value.volume_size
    volume_type = "gp3"
    encrypted   = true
  }

  tags = {
    Name        = each.key
    Environment = each.value.env
    ManagedBy   = "terraform"
  }
}

data "aws_ami" "amazon_linux_2023" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

タグのmapを動的に生成する

variable "extra_tags" {
  description = "追加するタグのリスト(key=valueの文字列)"
  type        = list(string)
  default     = ["Team=infra", "CostCenter=platform"]
}

locals {
  # "key=value" の文字列リストをmapに変換
  parsed_extra_tags = {
    for tag in var.extra_tags :
    split("=", tag)[0] => split("=", tag)[1]
  }
  # → { "Team" = "infra", "CostCenter" = "platform" }

  # 共通タグと結合
  all_tags = merge(
    {
      ManagedBy   = "terraform"
      Environment = var.environment
    },
    local.parsed_extra_tags
  )
}

8. よくある間違い

mapを出力するのに[ ]を使ってしまう

# ❌ [ ] はリストを生成する。mapを生成するには { } を使う
locals {
  wrong = [for k, v in var.my_map : k => v]  # SyntaxError
  # → Error: Invalid argument

  correct = { for k, v in var.my_map : k => v }  # { } で囲む
}

キーの重複(groupingモードが必要)

# ❌ 同じキーが複数存在するとエラー
locals {
  # 複数のユーザーが同じroleを持つ場合、キーが重複してエラーになる
  role_map = { for u in var.users : u.role => u.name }
  # Error: Two different items produced the key "viewer"
}

# ✅ groupingモード(...)で同じキーの値をリストにまとめる
locals {
  role_map = { for u in var.users : u.role => u.name... }
  # → { "admin" = ["alice"], "viewer" = ["bob", "carol"] }
}

for式とfor_each(メタ引数)の混同

# ❌ for式(値を生成する式)とfor_each(リソースを複数作成するメタ引数)は別物

# for式: localsや変数で値を計算するために使う
locals {
  names = [for s in var.servers : s.name]  # ← for式
}

# for_each: resourceブロックでリソースを複数作成するために使う
resource "aws_instance" "servers" {
  for_each = local.servers_map  # ← for_eachメタ引数
}

9. for式 vs splat式

リスト内の特定属性を一括取得する場合、for式よりsplat式([*]の方が短く書けます。

# for式: 汎用的、フィルタや変換が可能
[for s in aws_instance.web : s.private_ip]

# splat式: 属性取得だけなら短く書ける
aws_instance.web[*].private_ip

splat式はフィルタや変換が不要な単純な属性取得に向いています。

→ [splat式([*])— リソースリストから属性を一括取得](/docs/expressions/splat/)


10. 関連記事


11. まとめ

  • for式は値を生成する式。リスト出力は[...]、map出力は{...}で囲む
  • 基本形: [for <変数> in <コレクション> : <式>]
  • mapの反復: for k, v in <map> : ... でキーと値の両方を参照できる
  • ifフィルタ: 末尾にif <条件>を追加すると条件を満たす要素だけを抽出
  • 最重要パターン: { for s in <list> : s.name => s } でリスト→mapに変換してfor_eachに渡す
  • キーが重複する場合は...(groupingモード)でリストにまとめる
  • localsと組み合わせて段階的にデータを変換するのが実務の定番

動作確認バージョン: Terraform >= 1.9 / AWS Provider ~> 5.0 対象リージョン: ap-northeast-1(東京) 公式ドキュメント: https://developer.hashicorp.com/terraform/language/expressions/for