😊

terraformでAWS管理ポリシーをマージする

2022/08/31に公開

背景

AWSでは管理ポリシーをユーザーやロールに割り当てることができます。しかし、Service Quota Limitがあり、10個までしか割り当てることができません。Quotaを引き上げることもできますが最大でも20個までとなります。無理にQuotaを引き上げるよりは、管理ポリシーの内容をマージしたカスタム管理ポリシーを作った方が早いことがあります。
ところで管理ポリシー自体も6,144文字というサイズ制限があるため、複数の管理ポリシーを単純結合しても溢れます。

方針

管理ポリシーのStatementには稀によく

{
  "Effect": "Allow",
  "Resource": "*",
  "Action": [
    "何か"
  ]
}

というパターンが現れます。Actionが異なるだけなのでこれを1つのStatementにマージします。
それでも文字数はカツカツで更なる削減策が必要です。Actionにはこれまた稀によくec2:*のように全てのを認めるものが含まれているため、この場合、個々のec2:何かは不要になります。

実装

それではterraform module化していきます。

merged_policy.tf
terraform {
  required_version = ">= 0.13"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
    }
  }
}

variable "policy_arns" {
  description = "(Required) List of IAM managed policy arn."
  type        = list(string)
}

# 管理ポリシーを読み込みます。
data "aws_iam_policy" "policies" {
  for_each = toset(var.policy_arns)
  arn      = each.value
}

locals {
  # 管理ポリシーのJSONを読み込み、`Statement`を取り出します。仕様上、単一オブジェクト
  # の場合とオブジェクト配列の場合があるため`[*]`で配列化しています。配列の配列になる
  # ため`flatten()`で平坦化します。
  all_statements = flatten([
    for it in data.aws_iam_policy.policies : jsondecode(it.policy).Statement[*]
  ])
  # 稀によくあるパターンか否かで場合分けをし、`...`を使ってグループ化します。
  # `true`には稀によくあるパターン、`false`にはそれ以外がまとめられます。
  # 稀によくあるパターンの条件としては、3項目で、`Action`が存在し、
  # `Effect`が`"Allow"`であり、`Resource`が`"*"`で見ています。3項目としているので
  # これ以外の`Condition`などは含まれていないことになります。
  partitioned = {
    for it in local.all_statements :
    length(it) == 3 && can(it.Action) &&
    try(it.Effect == "Allow" && one(it.Resource[*]) == "*", false) => it...
  }
  # 稀によくあるパターンについて、namespaceでグループ化します。
  grouped = {
    for it in flatten(local.partitioned.true[*].Action[*]) :
    split(":", it)[0] => it...
  }
  # 稀によくあるパターンのnamespace毎について、グループ内に`namespace:*`のパターンが
  # 存在するか確認します。存在する場合は`namespace:*`のみ、存在しない場合は
  # グループそのままとします。
  compaction = flatten([
    for ns, actions in local.grouped :
    contains(actions, "${ns}:*") ? ["${ns}:*"] : actions
  ])
  # 稀によくあるパターンについて`Statement`を再構築しつつ、その他のパターンと
  # マージします。
  merge_statements = flatten([
    { Effect = "Allow", Resource = "*", Action = local.compaction },
    try(local.partitioned.false, []),
  ])
}

output "json" {
  description = "IAM policy document of marged policies."
  value = jsonencode({ Version = "2012-10-17", Statement = local.merge_statements })
}

最後に

terraformの利点は、データソースとして既存のリソースを読み込み、それを使って処理を行い、新たなリソース作成に役立てることができる点にあると思います。CloudFormationではこうはいきません。

Discussion