tech.guitarrapc.cóm

Technical updates

AWS IAM Identity CenterをPulumiで構成する

AWS IAM Identity CenterをTerraformで構成するときはaws-ia/terraform-aws-iam-identity-centerが便利です。 同様にPulumiでも構成してみましょうという回です。

どのような構成にするのか

AWS IAM Identity CenterはAWSのSSO機能です。SSOの連携IdPとしてGoogle IdPやMicrosoft Entra IDなどいろいろ選べます。今回は簡単のため、SSOなしでIAM Identity Center単独ディレクトリとして構成します。

アクセスポータルにアクセスした際、ログインしたユーザーごとに任意のアカウントへ任意の権限でアクセスできるようにするため、全体像は次の通りとします。

  • アカウントはmaster、serviceAの2つがある
  • masterアカウントはOrganizationの管理アカウント
  • masterアカウントでIAM Identity Centerを構成する
  • IAM Identity Centerのグループと権限セットはアカウントごとに分ける
  • 権限セットは各アカウントにAdminとViewOnlyの2権限を用意する
  • グループと権限セットの命名はともにアカウント名_権限名とする
  • 全ユーザーは必要な権限ごとにグループへ紐づける(fooは両アカウントでAdmin権限、barは両アカウントでViewOnly権限)
  • 簡単のためユーザー名=Emailとする

mermaidコード

flowchart TD
  subgraph access[アクセスポータル]
    url[example.awsapps.com/start]
  end
  subgraph master[masterアカウント]
    subgraph iam[IAM Identity Center]
      subgraph ユーザー
        user1[foo]
        user2[bar]
      end
      subgraph for_master[masterアカウント用]
        subgraph group_master[グループ]
          group1[master_Admin]
          group2[master_ViewOnly]
        end
        subgraph permission_master[権限セット]
          permission1[master_Admin]
          permission2[master_ViewOnly]
        end
      end
      subgraph for_serviceA[serviceAアカウント用]
        subgraph group_serviceA[グループ]
          group3[serviceA_Admin]
          group4[serviceA_ViewOnly]
        end
        subgraph permission_serviceA[権限セット]
          permission3[serviceA_Admin]
          permission4[serviceA_ViewOnly]
        end
      end
      subgraph account[アカウント]
        master_admin[["アタッチメント"]]
        master_viewonly[["アタッチメント"]]
        account_master([masterアカウント])

        serviceA_admin[["アタッチメント"]]
        serviceA_viewonly[["アタッチメント"]]
        account_serviceA([serviceAアカウント])
      end
    end
  end
  subgraph serviceA[serviceAアカウント]
    ec2_2[ec2]
    ecs_2[ecs]
    lambda_2[lambda]
    s3_2[s3]
  end

  user1 -.-> group1
  user1 -.-> group3
  user2 -.-> group2
  user2 -.-> group4

  group1 -.-> master_admin
  group2 -.-> master_viewonly
  permission1 -.-> master_admin
  permission2 -.-> master_viewonly

  group3 -.-> serviceA_admin
  group4 -.-> serviceA_viewonly
  permission3 -.-> serviceA_admin
  permission4 -.-> serviceA_viewonly

  master_admin -.-> account_master
  master_viewonly -.-> account_master
  serviceA_admin -.-> account_serviceA
  serviceA_viewonly -.-> account_serviceA

  browser --> access
  access --> master
  access --> serviceA

IAM Access Identity全体構成図

グループと権限セットはアカウントごとに分ける

いくつか読んだ中でもこの記事は同意できるところが多いです。記事に沿ってグループと権限セットをアカウントごとに分けます。

アカウントごとに権限セットを調整 & ユーザー紐づけしやすいようように、アカウントx権限セットごとにグループを設けます。単一グループを複数アカウントに紐づけると、アカウントによってどのような権限なのかグループから推測できなくなるためです。同様に権限セットもアカウントx権限セットごとに設けています。AdminやViewOnlyという権限セットだけ用意しても、アカウントごとに紐づける権限が変わったときに困るためです。

なお、グループ名はアカウント名_権限(例master_Admin)とし、権限セットはグループ名と合わせます。

管理アカウントで一括管理

Organizationの管理アカウントでIAM Identity Centerを構成してメンバーアカウントにも展開するため、Organizationのサービスアクセスでsso.amazonaws.comを有効にしておきます。

// organization
var org = new Organization($"organization", new()
{
    FeatureSet = "ALL",
    AwsServiceAccessPrincipals = [
        "sso.amazonaws.com", // これ!
        // 省略
    ],
    EnabledPolicyTypes = [
        // 省略
    ],
});

TOTP送信を有効にしておく

IAM Access IdentityユーザーをAWSコンソールから作る場合、Send an email to this user with password setup instructions.オプションを選択して初期パスワードをメールで送ってユーザーがプロビジョニングできます。 しかしIaCやAPIでAccess Identityのユーザーを作成する時にこの方法は使えず、ユーザーが自分でプロビジョニングできません。

AWSコンソールからのUser作成

代替手段として、IAM Access Identity > SettingsにあるSend email OTP for users created from APIを有効にしておくと、ユーザーがサインアップでメールアドレス入力後にTOTPを自動送信してくれます。 自動送信されたTOTPを入力するとメールアドレスの検証 & 初期パスワード & MFA構成に進む寸法です。

Send email OTP for users created from APIは有効にしておく

Pulumiで構成する

Pulumi C#でIAM Identity Centerを構成してみましょう。

using Pulumi;
using Pulumi.Aws.IdentityStore;
using Pulumi.Aws.IdentityStore.Inputs;
using Pulumi.Aws.SsoAdmin;

return await Pulumi.Deployment.RunAsync(() =>
{
    var opt = new CustomResourceOptions();

    var instance = Output.Create(Pulumi.Aws.SsoAdmin.GetInstances.InvokeAsync(new()));
    var instanceId = instance.Apply(x => x.IdentityStoreIds[0]);
    var instanceArn = instance.Apply(x => x.Arns[0]);

    // アカウント/ユーザー定義
    var ssoType = IdentityCenterSsoType.Internal;
    IReadOnlyList<(string AccountName, string AccountId, IReadOnlyList<IdentityCenterPermission> Permissions)> accountDefinitions = [
        ("master", "11111111", [IdentityCenterPermission.Admin, IdentityCenterPermission.ViewOnly]),
        ("serviceA", "22222222", [IdentityCenterPermission.Admin, IdentityCenterPermission.ViewOnly]),
    ];

IReadOnlyList<(string Email, string FamilyName, string GivenName, IReadOnlyList<(string AccountName, IReadOnlyList<IdentityCenterPermission> Permissions)> GroupMembership)> userDefinitions = [
            ("foo@example.com", "Foo", "Example", [
                ("master", [IdentityCenterPermission.Admin]),
                ("serviceA", [IdentityCenterPermission.Admin]),
            ]),
            ("bar@example.com", "Bar", "Baz", [
                ("master", [IdentityCenterPermission.ViewOnly]),
                ("serviceA", [IdentityCenterPermission.ViewOnly]),
            ]),
        ];

    var name = "sample";

    // groups
    var groups = new Dictionary<string, Group>();
    foreach (var accountDetail in accountDefinitions)
    {
        foreach (var permission in accountDetail.Permissions)
        {
            var groupName = GetGroupName(accountDetail.AccountName, permission);
            var group = new Group($"{name}-{groupName}-group", new()
            {
                DisplayName = groupName,
                Description = $"{accountDetail.AccountName}'s {permission} IAM Identity Center Group",
                IdentityStoreId = instanceId,
            }, opt);
            groups.Add(groupName, group);
        }
    }

    // users
    foreach (var userDetail in userDefinitions)
    {
        var user = new User($"{name}-{userDetail.Email}-user", new()
        {
            UserName = userDetail.Email,
            Emails = new UserEmailsArgs
            {
                Primary = true,
                Value = userDetail.Email,
            },
            Name = new UserNameArgs
            {
                FamilyName = userDetail.FamilyName,
                GivenName = userDetail.GivenName,
            },
            DisplayName = $"{userDetail.GivenName} {userDetail.FamilyName}",
            IdentityStoreId = instanceId,
        }, opt);

        foreach (var membership in userDetail.GroupMembership)
        {
            foreach (var permission in membership.Permissions)
            {
                var membershipName = GetGroupName(membership.AccountName, permission);
                var group = groups[membershipName];
                _ = new GroupMembership($"{name}-{userDetail.Email}-{membershipName}-membership", new()
                {
                    MemberId = user.UserId,
                    GroupId = group.GroupId,
                    IdentityStoreId = instanceId,
                }, opt);
            }
        }
    }

    // permissionSets
    var permissionSets = new Dictionary<string, PermissionSet>();
    foreach (var accountDetail in accountDefinitions)
    {
        foreach (var permission in accountDetail.Permissions)
        {
            if (!PermissionSetPrefix.PermissionMapping.TryGetValue(permission, out var permissionSetDetail))
                continue;

            // GroupNameと同じ名前にする
            var permissionSetName = GetGroupName(accountDetail.AccountName, permission);

            var permissionSet = new PermissionSet($"{name}-{permissionSetName}-permissionset", new()
            {
                InstanceArn = instanceArn,
                Name = permissionSetName,
                Description = permissionSetDetail.Description,
                SessionDuration = permissionSetDetail.SessionDuration,
            }, opt);

            // AWS Managed policy attachment
            for (var i = 0; i < permissionSetDetail.ManagedPolicySets.Count; i++)
            {
                var attachment = new ManagedPolicyAttachment($"{name}-{permissionSetName}-{i}-attachment", new()
                {
                    InstanceArn = instanceArn,
                    PermissionSetArn = permissionSet.Arn,
                    ManagedPolicyArn = permissionSetDetail.ManagedPolicySets[i],
                }, opt);
            }

            permissionSets.Add(permissionSetName, permissionSet);
        }
    }

    // Account assignment
    foreach (var accountDetail in accountDefinitions)
    {
        foreach (var permission in accountDetail.Permissions)
        {
            var groupName = GetGroupName(accountDetail.AccountName, permission);
            var group = groups[groupName];
            var permissionSet = permissionSets[groupName];

            switch (ssoType)
            {
                case IdentityCenterSsoType.Internal:
                    var assignment = new AccountAssignment($"{name}-{groupName}-assignment", new()
                    {
                        InstanceArn = instanceArn,
                        PermissionSetArn = permissionSet.Arn,
                        PrincipalId = group.GroupId,
                        PrincipalType = "GROUP", // 外部IdP SSOじゃないのでアカウントに対してグループで紐づける
                        TargetId = accountDetail.AccountId,
                        TargetType = "AWS_ACCOUNT", // 外部IdP SSOじゃない
                    }, opt);
                    break;
                default:
                    throw new NotImplementedException($"SsoType {ssoType} is not implemented.");
            }
        }
    }

    // PermissionSet、Groupで同じ名前にするルールを強制するためのヘルパー
    static string GetGroupName(string account, IdentityCenterPermission identityPermission)
        => $"{account}_{identityPermission}";
});

public enum IdentityCenterPermission
{
    /// <summary>
    /// そのアカウントにおけるAdministrator権限
    /// </summary>
    Admin,
    /// <summary>
    /// そのアカウントにおけるReadOnly権限
    /// </summary>
    ViewOnly
    // 他に権限が必要になったら追加したり...
}

/// <summary>
/// IAM Identity CenterのSSO種別。InternalはIAM Identity Center自身でユーザーを管理するモード。サンプルではInternalのみ利用
/// </summary>
public enum IdentityCenterSsoType
{
    /// <summary>
    /// IAM Identity Center自身でユーザーを管理するモード
    /// </summary>
    Internal,
    /// <summary>
    /// Google WorkspaceやOktaなどのIDPを利用する場合に使うといいでしょう
    /// </summary>
    ExternalGoogleIdP,
}


/// <summary>
/// IAM Identity Centerの権限セットを事前定義する。
/// カスタム権限が欲しくなったらここに追加 + ポリシーセットの作成とグループ紐づけが必要になる。(例ではマネージドポリシーのみ)
/// </summary>
record PermissionSetPrefix
{
    public static Dictionary<IdentityCenterPermission, PermissionSetPrefix> PermissionMapping = new()
    {
        {
            IdentityCenterPermission.Admin, new()
            {
                Description = "Provides AWS full access permissions.",
                SessionDuration = "PT8H",
                ManagedPolicySets = ["arn:aws:iam::aws:policy/AdministratorAccess"]
            }
        },
        {
            IdentityCenterPermission.ViewOnly, new()
            {
                Description = "Provides AWS view only permissions.",
                SessionDuration = "PT8H",
                ManagedPolicySets = ["arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"]
            }
        },
    };

    public required string Description { get; init; }
    public required string SessionDuration { get; init; }
    public required IReadOnlyList<string> ManagedPolicySets { get; set; }
}

まとめ

AWS IAM Identity Centerは割とごちゃつきやすいのですが、Pulumi C#ならすんなり組むことができます。 IAM UserやIAM Groupと違って、アカウントを跨いだ設定をシンプルに組み込めるのは控えめにいって神です。

IAM UserやスイッチロールだとAWSの複数アカウントアクセスはAzureやGoogle Cloudに比べてログイン回りがしんどいのですが、IAM Identity Centerを使うことで比肩すものになります。Google IdPでSSOをすればGoogle Cloudととも似た体験になるのでおすすめです。

参考

AWS Docs

他サイト