Terraformのfor_eachで2重ループを実現する

Table of Contents

結論

まずflatten関数を使ってネストになっているデータ構造を平らにします。

role_policy_arns = flatten([
  for k, v in local.roles : [
    for policy_arn in v.policy_arns : {
      role_name  = k
      policy_arn = policy_arn
    }
  ]
])

作成したmapのリストからfor_eachで使うためのmapを新しく作成します。
このときのキーはmapの値を使って一意になるようにします。

for_each = {
  for v in local.role_policy_arns : "${v.role_name}/${v.policy_arn}" => v
}

解説

$ terraform -v
Terraform v1.9.8
on linux_amd64
+ provider registry.terraform.io/hashicorp/aws v5.74.0

AWSのIAMロールの名前とそれにアタッチするIAMポリシーのARNを以下のように定義するとします。

locals {
  roles = {
    "test-role-1" = {
      policy_arns = [
        "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
        "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
      ]
    }
    "test-role-2" = {
      policy_arns = [
        "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
        "arn:aws:iam::aws:policy/AmazonS3FullAccess"
      ]
    }
    "test-role-3" = {
      policy_arns = [
        "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
        "arn:aws:iam::aws:policy/AmazonSQSFullAccess",
        "arn:aws:iam::aws:policy/AmazonSNSFullAccess",
        "arn:aws:iam::aws:policy/AmazonS3FullAccess"
      ]
    }
  }
}

mapのキーがIAMロールの名前で、mapの値としてアタッチするIAMポリシーのARNのリスト(policy_arns)が入っています。

IAMロールについては単純にfor_eachで作成できます。

resource "aws_iam_role" "test" {
  for_each = local.roles

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

作成したIAMロールにIAMポリシーをアタッチするため、aws_iam_role_policy_attachmentを使用します。
このリソースは引数としてIAMロールの名前とそれにアタッチするポリシーのARNを一つ受け取ります。
したがって、それぞれのIAMロールに対して一つずつポリシーをアタッチするため、for_eachで2重ループのような動作を実現したくなります。

この場合の一つの解決策としては、IAMポリシーをアタッチするためのモジュールを用意することがあります。
モジュールの引数としてリストを受け取り、モジュールの中でリストの各要素についてfor_eachでresourceを作成することで、2重ループの動作を実現できます。
処理が複雑であったり同じような処理を色々なところで使うということであれば、モジュールを作成するのがいいと思います。

この記事では、モジュールを用意するほどではないな…、という場合のために、flatten関数を使った解決策を紹介したいと思います。


aws_iam_role_policy_attachmentをfor_eachで回すためには、IAMロールの名前とそれにアタッチするポリシーのARNが一対一で対応するデータ構造であればよさそうです。
これは以下のようにすると実現できます。

locals {
  role_policy_arns = flatten([
    for k, v in local.roles : [
      for policy_arn in v.policy_arns : {
        role_name  = k
        policy_arn = policy_arn
      }
    ]
  ])
}
  1. policy_arnsを展開してIAMロールの名前とポリシーのARNを格納したmapを作成する
    • この処理でmapのリストが作成される
  2. 上の処理をそれぞれのIAMロールについて実行する
    • リストのリスト(2重リスト)が作成される

2重のリストになっているとfor_eachで回せないので、flatten関数を使って平らにします。
結果として、role_policy_arnsは以下のような値となります。

$ terraform console
> local.role_policy_arns
[
  {
    "policy_arn" = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
    "role_name" = "test-role-1"
  },
  {
    "policy_arn" = "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
    "role_name" = "test-role-1"
  },
  {
    "policy_arn" = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
    "role_name" = "test-role-2"
  },
  {
    "policy_arn" = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
    "role_name" = "test-role-2"
  },
  {
    "policy_arn" = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
    "role_name" = "test-role-3"
  },
  {
    "policy_arn" = "arn:aws:iam::aws:policy/AmazonSQSFullAccess"
    "role_name" = "test-role-3"
  },
  {
    "policy_arn" = "arn:aws:iam::aws:policy/AmazonSNSFullAccess"
    "role_name" = "test-role-3"
  },
  {
    "policy_arn" = "arn:aws:iam::aws:policy/AmazonS3FullAccess"
    "role_name" = "test-role-3"
  },
]

これをresourceのfor_eachで回します。
for_eachのキーは一意なものである必要がありますが、今回の場合はrole_namepolicy_arnを組み合わせてやることで一意な値を作成できます。

resource "aws_iam_role_policy_attachment" "test" {
  for_each = {
    for v in local.role_policy_arns : "${v.role_name}/${v.policy_arn}" => v
  }

  role       = each.value.role_name
  policy_arn = each.value.policy_arn

  depends_on = [
    aws_iam_role.test
  ]
}

最後にTerraformコード全体を掲載しておきます。

locals {
  roles = {
    "test-role-1" = {
      policy_arns = [
        "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
        "arn:aws:iam::aws:policy/AmazonEC2FullAccess"
      ]
    }
    "test-role-2" = {
      policy_arns = [
        "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
        "arn:aws:iam::aws:policy/AmazonS3FullAccess"
      ]
    }
    "test-role-3" = {
      policy_arns = [
        "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole",
        "arn:aws:iam::aws:policy/AmazonSQSFullAccess",
        "arn:aws:iam::aws:policy/AmazonSNSFullAccess",
        "arn:aws:iam::aws:policy/AmazonS3FullAccess"
      ]
    }
  }
}

resource "aws_iam_role" "test" {
  for_each = local.roles

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

locals {
  role_policy_arns = flatten([
    for k, v in local.roles : [
      for policy_arn in v.policy_arns : {
        role_name  = k
        policy_arn = policy_arn
      }
    ]
  ])
}

resource "aws_iam_role_policy_attachment" "test" {
  for_each = {
    for v in local.role_policy_arns : "${v.role_name}/${v.policy_arn}" => v
  }

  role       = each.value.role_name
  policy_arn = each.value.policy_arn

  depends_on = [
    aws_iam_role.test
  ]
}

Terraform,技術

Posted by 駄場さん