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
グループと権限セットはアカウントごとに分ける
いくつか読んだ中でもこの記事は同意できるところが多いです。記事に沿ってグループと権限セットをアカウントごとに分けます。
アカウントごとに権限セットを調整 & ユーザー紐づけしやすいようように、アカウント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のユーザーを作成する時にこの方法は使えず、ユーザーが自分でプロビジョニングできません。
代替手段として、IAM Access Identity > Settings
にあるSend email OTP for users created from API
を有効にしておくと、ユーザーがサインアップでメールアドレス入力後にTOTPを自動送信してくれます。
自動送信されたTOTPを入力するとメールアドレスの検証 & 初期パスワード & MFA構成に進む寸法です。
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
- IAM Identity Center とは | AWS IAM Identity Center
- IAM アイデンティティセンターを使用した認証 - AWS IAM Identity Center
- Google Workspace および IAM アイデンティティセンターによる SAML と SCIM の設定 - AWS IAM Identity Center
- Email one-time password to users created with API - AWS IAM Identity Center
- Automatic onboarding for new Identity Center users? | AWS re:Post
他サイト