IAM Identity Centerの設定をTerraformでやってみる

2024.01.29

IAM Identity Centerの以下操作をやってみました。

  1. IAM Identity Centerの有効化
  2. IAM Identity Centerの管理の委任
  3. ユーザー/グループ/許可セットの作成

2024年1月時点では、以下は対応するTerraform Resourceがないため、手動で行なっています。その他の作業はTerraformを使いました。

  1. IAM Identity Centerの有効化
  2. IAM Identity Centerの管理の委任

Terraformコードは以下にあります。

msato0731/terraform-sample/iic

前提

この手順はOrganizationsを利用していることを前提にしています。

管理アカウントへのアクセスを最小限にするために、IAM Identity Center管理を他のアカウントに委任します。

そのため、管理アカウントの他にIAM Identity Center管理用のアカウントを用意する必要があります。

1. IAM Identity Centerを有効化する(管理アカウント)

Organizationの管理アカウントにログインし、マネジメントコンソールからIAM Identity Centerにアクセスします。

有効にするを選択します。

有効化に成功すると、以下のようにダッシュボード画面が表示されます。

2. IAM Identity Centerの管理の委任(管理アカウント)

設定 -> 管理 -> 委任された管理者を登録 -> アカウントを登録の順に選択します。

委任先AWSアカウントを選択し、`アカウントを登録`を選択します。

以降の作業は、委任先アカウントで行います。

3. ユーザー・グループ・許可セットの作成

AWSアカウントは以下の4つがあるものとします。

AWSアカウント 説明
Prd-HogeService HogeService 本番環境
Stg-HogeService HogeService STG環境
Prd-FugaService FugaService 本番環境
Stg-FugaService FugaService STG環境

ユーザーとグループは以下です。

ユーザー 説明 グループ
taro.test インフラ管理責任者 HogeAdministrator
FugaAdministrator
jiro.test Hoge・Fuga Serviceの開発者 HogeDevelopper
FugaDevelopper
saburo.test Hoge Serviceの開発者 HogeDevelopper

グループに付与されている権限は以下です。

グループ 説明
*Administrator 対象サービスの本番/STG: Admin
*Developper 対象サービスの本番:Read・STG:Admin

AWSアカウント情報の設定

terraform.tfvarsを作成して、アカウントIDを記載します。

cp terraform.tfvars.sample terraform.tfvars

terraform.tfvars

account_ids = {
  # アカウントIDを書き換える
  hoge_prd : "123456789012",
  hoge_stg : "555555555555",
  fuga_prd : "999999999999",
  fuga_stg : "012345678901",
}

ユーザー・グループ・ユーザーとグループの関連付け

ユーザーとグループの記述は以下になります。

locals.tf

  ################################################################################
  # Groups
  ################################################################################
  groups = {
    hoge_admin = {
      name        = "HogeAdministrator"
      description = "hoge service prd/stg:admin"
    }
    hoge_dev = {
      name        = "HogeDevelopperTeam"
      description = "hoge servide prd:read, stg:admin"
    }
    fuga_admin = {
      name        = "FugaAdministrator"
      description = "fuga service prd/stg:admin"
    }
    fuga_dev = {
      name        = "FugaDevelopperTeam"
      description = "fuga servide prd:read, stg:admin"
    }
  }
  ################################################################################
  # Users
  ################################################################################
  users = {
    "taro.test@example.com" = {
      name = {
        family_name = "Test"
        given_name  = "Taro"
      }
      groups = [
        "hoge_admin",
        "fuga_admin",
      ]
    }
    "jiro.test@example.com" = {
      name = {
        family_name = "Test"
        given_name  = "Jiro"
      }
      groups = [
        "hoge_dev",
        "fuga_dev"
      ]
    }
    "saburo.test@example.com" = {
      name = {
        family_name = "Test"
        given_name  = "Saburo"
      }
      groups = [
        "hoge_dev"
      ]
    }
  }
  ################################################################################
  # Membership
  ################################################################################

  # ユーザーとユーザーが属するグループの組み合わせを作成
  users_groups_combined = [
    for user, user_data in local.users : {
      for group in user_data.groups :
      "${user}_${group}" => {
        "user"  = user
        "group" = group
      }
    }
  ]

  # ユーザーがブロックごとに分かれているため、ユーザーとグループの組み合わせを1つのブロックにまとめる
  users_groups_membership = zipmap(
    flatten(
      [for item in local.users_groups_combined : keys(item)]
    ),
    flatten(
      [for item in local.users_groups_combined : values(item)]
    )
  )

users_groups.tf

################################################################################
# Group
################################################################################

resource "aws_identitystore_group" "this" {
  for_each = local.groups

  identity_store_id = local.identity_store_id
  display_name      = each.value["name"]
  description       = each.value["description"]
}

################################################################################
# User
################################################################################

resource "aws_identitystore_user" "this" {
  for_each = local.users

  identity_store_id = local.identity_store_id
  display_name      = join(" ", [each.value.name.given_name, each.value.name.family_name])
  user_name         = each.key

  name {
    family_name = each.value["name"]["family_name"]
    given_name  = each.value["name"]["given_name"]
  }
}

################################################################################
# Membership
################################################################################

resource "aws_identitystore_group_membership" "this" {
  for_each = local.users_groups_membership

  identity_store_id = local.identity_store_id
  group_id          = aws_identitystore_group.this[each.value["group"]].group_id
  member_id         = aws_identitystore_user.this[each.value["user"]].user_id
}

ポイントは、aws_identitystore_group_membershipでユーザーとグループを関連づける必要があることです。

愚直に書くなら、このリソースはユーザーとグループの関連付けの数分書く必要があります。

local users_groups_membershipでユーザーとグループの関連付け用の配列を作っています。

users配列から、groupuserだけの配列を作っています。

users_groups_combined_output

  + users_groups_combined   = [
      + {
          + "jiro.test@example.com_fuga_dev" = {
              + group = "fuga_dev"
              + user  = "jiro.test@example.com"
            }
          + "jiro.test@example.com_hoge_dev" = {
              + group = "hoge_dev"
              + user  = "jiro.test@example.com"
            }
        },
      + {
          + "saburo.test@example.com_hoge_dev" = {
              + group = "hoge_dev"
              + user  = "saburo.test@example.com"
            }
        },
      + {
          + "taro.test@example.com_fuga_admin" = {
              + group = "fuga_admin"
              + user  = "taro.test@example.com"
            }
          + "taro.test@example.com_hoge_admin" = {
              + group = "hoge_admin"
              + user  = "taro.test@example.com"
            }
        },
    ]

このままだと、userごとにブロックが分かれてしまっていて、for_each等を使いづらいです。

そのため、ユーザーとグループの組み合わせを1つのブロックにまとめます。

users_groups_membership-output

  + users_groups_membership = {
      + "jiro.test@example.com_fuga_dev"   = {
          + group = "fuga_dev"
          + user  = "jiro.test@example.com"
        }
      + "jiro.test@example.com_hoge_dev"   = {
          + group = "hoge_dev"
          + user  = "jiro.test@example.com"
        }
      + "saburo.test@example.com_hoge_dev" = {
          + group = "hoge_dev"
          + user  = "saburo.test@example.com"
        }
      + "taro.test@example.com_fuga_admin" = {
          + group = "fuga_admin"
          + user  = "taro.test@example.com"
        }
      + "taro.test@example.com_hoge_admin" = {
          + group = "hoge_admin"
          + user  = "taro.test@example.com"
        }
    }

アクセス権限セット

アクセス権限セットの記述は以下になります。

locals.tf

  ########################
  # Permissions
  ########################
  permission_sets = {
    "admin" = {
      name               = "AdministratorAccess"
      description        = "Provides full access to AWS services and resources."
      managed_policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
    },
    "read_only" = {
      name               = "ReadOnlyAccess"
      description        = "Provides read-only access to AWS services and resources."
      managed_policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
    }
  }

  ################################################################################
  # Account Assignments
  ################################################################################
  account_assignments = [
    # Hoge Service Production
    {
      account_id     = var.account_ids.hoge_prd
      group          = "hoge_admin"
      permission_set = "admin"
    },
    {
      account_id     = var.account_ids.hoge_prd
      group          = "hoge_dev"
      permission_set = "read_only"
    },
    # Hoge Service Staging
    {
      account_id     = var.account_ids.hoge_stg
      group          = "hoge_admin"
      permission_set = "admin"
    },
    {
      account_id     = var.account_ids.hoge_stg
      group          = "hoge_dev"
      permission_set = "admin"
    },
    # Fuga Service Production
    {
      account_id     = var.account_ids.fuga_prd
      group          = "fuga_admin"
      permission_set = "admin"
    },
    {
      account_id     = var.account_ids.fuga_prd
      group          = "fuga_dev"
      permission_set = "read_only"
    },
    # Fuga Service Staging
    {
      account_id     = var.account_ids.fuga_stg
      group          = "fuga_admin"
      permission_set = "admin"
    },
    {
      account_id     = var.account_ids.fuga_stg
      group          = "fuga_dev"
      permission_set = "admin"
    }
  ]

  # アカウントID-グループ名-PermissionSet名をキーに設定
  assignment_map = {
    for a in local.account_assignments :
    format("%v-%v-%v", a.account_id, local.groups[a.group].name, local.permission_sets[a.permission_set].name) => a
  }
}

permission_sets.tf

resource "aws_ssoadmin_permission_set" "this" {
  for_each = local.permission_sets

  name         = each.value.name
  description  = each.value.description
  instance_arn = local.instance_arn
}

resource "aws_ssoadmin_managed_policy_attachment" "this" {
  for_each = local.permission_sets

  instance_arn       = local.instance_arn
  managed_policy_arn = each.value.managed_policy_arn
  permission_set_arn = aws_ssoadmin_permission_set.this[each.key].arn
}

account_assignments.tf

resource "aws_ssoadmin_account_assignment" "this" {
  for_each = local.assignment_map

  instance_arn       = local.instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.this[each.value.permission_set].arn
  principal_id       = aws_identitystore_group.this[each.value.group].group_id
  principal_type     = "GROUP"

  target_id   = each.value.account_id
  target_type = "AWS_ACCOUNT"
}

aws_ssoadmin_account_assignmentでアクセス許可セットとグループとアカウントIDを渡す必要があります。

上記の組み合わせをlocalaccount_assignmentsで定義しています。

local変数account_assignmentsを使ってfor_eachでリソースを作っても良いですが、以下の理由からlocal変数 user_group_membershipで加工しました。

  • resource側のfor_eachの記述がシンプルになる
  • リソースのキーが連番ではなく、ユニークな文字列になり分かりやすい
    • 一部リソースの作成だけ失敗したときに、対象箇所を見つけやすい
resource "aws_ssoadmin_account_assignment" "this" {
# そのまm
  for_each = {
    for idx, assignment in local.account_assignments: idx => assignment
  }

users_groups_membershipでは、以下のjiro.test@example.com_fuga_devのようにユニークなIDが付きます。

users_groups_membership_output

  + users_groups_membership = {
      + "jiro.test@example.com_fuga_dev"   = {
          + group = "fuga_dev"
          + user  = "jiro.test@example.com"
        }
      + "jiro.test@example.com_hoge_dev"   = {
          + group = "hoge_dev"
          + user  = "jiro.test@example.com"
        }
      + "saburo.test@example.com_hoge_dev" = {
          + group = "hoge_dev"
          + user  = "saburo.test@example.com"
        }
      + "taro.test@example.com_fuga_admin" = {
          + group = "fuga_admin"
          + user  = "taro.test@example.com"
        }
      + "taro.test@example.com_hoge_admin" = {
          + group = "hoge_admin"
          + user  = "taro.test@example.com"
        }
    }

おわりに

今回はカスタマー管理ポリシーやインラインポリシーは考慮せずに書きました。

追加したい場合は、Local変数permission_setsに要素を足して、アタッチ用のresourceを定義すればできそうです。

以上、AWS事業本部の佐藤(@chari7311)でした。

参考