IAM Identity CenterのユーザーをLambda-backed カスタムリソースを利用して作成してみた

2024.02.18

こんにちは、AWS事業部の木村です。

以前にIAM Identity Centerのユーザーの作成状況についてAthenaでトラッキングする方法についてご紹介させていただきました。

今回はトラッキングから一歩踏み込んでIAM Identity Centerのユーザー作成を定型化することを目指し、Lambda-backed カスタムリソースを利用したCloudFormationで作成する機会がございましたのでご紹介します。

Lambda-backed カスタムリソースを利用するに至った背景

2024/2/18現在、CloudFormationではIAM Identity Centerのユーザー作成に対応しておりません。そのためLambda-backed カスタムリソースを利用してユーザーの作成を行なっております。

AWS IAM Identity Center resource type reference

Terraformではユーザー作成について標準でサポートされております。環境の制約などがなければTerraformを利用して作成する方が簡易に対応できるかと思います。

Terraformでの作成については、以下記事で紹介されておりましたのでこちらをご参照ください。

今回目指すこと

今回CloudFormationでIAM Identity Centerのユーザー作成する目的としてユーザーの命名規則のルールを遵守させるための定型化が目的となっております。

具体的には以下のようなルールが存在していたとします。

項目 作成ルール
ユーザー名 ${メールアドレス}
メールアドレス ${メールアドレス}
${名}
${姓}
表示名 ${所属会社}-${名}-${姓}

私の例で作成するとこのように作成することとなります。

項目 作成ルール
ユーザー名 kimura.yuta@example.com
メールアドレス kimura.yuta@example.com
yuta
kimura
表示名 cm-yuta-kimura

但しこの命名ルールを誤って認識していたり、作成時に入力ミスしていたりすると以下の様なユーザーを作成されてしまうケースがあります。

  • 命名ルールを誤って認識していたケース
項目 作成ルール
ユーザー名 cm-yuta-kimura
メールアドレス kimura.yuta@example.com
yuta
kimura
表示名 cm-yuta-kimura
  • 作成時に入力ミスしていたケース
項目 作成ルール
ユーザー名 kimura.yuta@example.com
メールアドレス kimura.yuta@example.com
yuta
kimura
表示名 cm-kimura-yuta

マネージメントコンソールから作成する場合には、以下の様な画面からの作成になるので作成の際に起こり得るミスかと思います。

これらのミスの発生を事前に防ぐための仕組みとして、今回CloudFormationで以下の様なパラメーターを入力してもらいユーザーを作成するように変更します。

入力されたパラメーターをLambdaに引き渡し、それぞれの項目に当てはめることでミスを防止します。

やってみる

今回テンプレートの展開に当たって以下の様な準備を行います。

  • 基本
    • Lambdaの作成
    • テンプレートの準備
  • オプション
    • CloudFormation クイック作成リンク
    • 許可セットのクイックリンクの更新

オプションについては、設定を行わなくてもユーザー作成自体は可能です。

ただ運用に当たって今後何度も利用されることやこの仕組みを利用して作成してもらうことを想定してオプションについても軽く内容を紹介できればと思っております。

Lambdaの作成

まずLambdaの作成を行います。

Lambda自体もテンプレートに組み込んでしまうこともできるのですが、組み込んでしまうとユーザー作成のたびにLambda関数が作成されてしまうので、今回は事前にLambdaを作成しておいてそれをCloudFormationから実行する仕組みにしたいと思います。

今回の作成では、例の際にも挙げた作成ルールで作成される様にしております。

項目 作成ルール
ユーザー名 ${メールアドレス}
メールアドレス ${メールアドレス}
${名}
${姓}
表示名 ${所属会社}-${名}-${姓}

以下のように作成を行います。

  • lambda_function.py
import boto3
import cfnresponse

def lambda_handler(event, context):
    if event['RequestType'] == 'Delete':
        cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
    # AWS SSO APIのクライアントを作成
    identitystore = boto3.client('identitystore')

    # ユーザー作成のための情報
    company = event['ResourceProperties']['Company']
    given_name = event['ResourceProperties']['GivenName']
    family_name = event['ResourceProperties']['FamilyName']
    email = event['ResourceProperties']['Email']

    try:
        # AWS SSOユーザーを作成
        response = identitystore.create_user(
            IdentityStoreId='<自身の組織のIdentityStoreId>',
            UserName = email,
            Name={
                'FamilyName': family_name,
                'GivenName': given_name,
            },
            DisplayName=f"{company}-{given_name}-{family_name}",
            Emails=[
                {
                    'Value': email,
                    'Primary': True
                },
            ],

        )

        # ユーザー作成成功時の処理
        print("AWS SSOユーザーが作成されました:", response)
        cfnresponse.send(event, context, cfnresponse.SUCCESS,{'Response': 'Success'})
    except Exception as e:
        # エラー時の処理
        print("ユーザーの作成に失敗しました:", e)
        cfnresponse.send(event, context, cfnresponse.SUCCESS,{'Response': 'fail'})

<自身の組織のIdentityStoreId>については各自置き換えてください。

  • cfnresponse.py
#  Copyright 2016 Amazon Web Services, Inc. or its affiliates. All Rights Reserved.
#  This file is licensed to you under the AWS Customer Agreement (the "License").
#  You may not use this file except in compliance with the License.
#  A copy of the License is located at http://aws.amazon.com/agreement/ .
#  This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied.
#  See the License for the specific language governing permissions and limitations under the License.

from __future__ import print_function
import urllib3
import json

SUCCESS = "SUCCESS"
FAILED = "FAILED"

http = urllib3.PoolManager()


def send(event, context, responseStatus, responseData, physicalResourceId=None, noEcho=False, reason=None):
    responseUrl = event['ResponseURL']

    print(responseUrl)

    responseBody = {
        'Status' : responseStatus,
        'Reason' : reason or "See the details in CloudWatch Log Stream: {}".format(context.log_stream_name),
        'PhysicalResourceId' : physicalResourceId or context.log_stream_name,
        'StackId' : event['StackId'],
        'RequestId' : event['RequestId'],
        'LogicalResourceId' : event['LogicalResourceId'],
        'NoEcho' : noEcho,
        'Data' : responseData
    }

    json_responseBody = json.dumps(responseBody)

    print("Response body:")
    print(json_responseBody)

    headers = {
        'content-type' : '',
        'content-length' : str(len(json_responseBody))
    }

    try:
        response = http.request('PUT', responseUrl, headers=headers, body=json_responseBody)
        print("Status code:", response.status)


    except Exception as e:

        print("send(..) failed executing http.request(..):", e)

テンプレートの準備

今回は以下のテンプレートを利用します。

各パラメーターには想定する値のみを許容する様に設定しております。

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  Email:
    Type: String
    Description: "Email address in valid email format"
    AllowedPattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
    ConstraintDescription: "Must be a valid email address."

  familyname:
    Type: String
    Description: "Family name (Alphanumeric only)"
    AllowedPattern: "[a-zA-Z0-9]+"
    ConstraintDescription: "Only alphanumeric characters are allowed."

  givenname:
    Type: String
    Description: "Given name (Alphanumeric only)"
    AllowedPattern: "[a-zA-Z0-9]+"
    ConstraintDescription: "Only alphanumeric characters are allowed."

  company:
    Type: String
    Description: "Company name (Alphanumeric only)"
    AllowedPattern: "[a-zA-Z0-9]+"
    ConstraintDescription: "Only alphanumeric characters are allowed."

Resources:
  InvokeLambda:
    Type: Custom::SampleLambda
    Properties:
      ServiceToken: <先ほど作成したLambdaのARN>
      Email: !Ref Email
      FamilyName: !Ref familyname
      GivenName: !Ref givenname
      Company: !Ref company

<先ほど作成したLambdaのARN>については各自置き換えてください。

動かしてみる

ここまでで基本の準備が完了しましたので、実施に動かしてユーザーを作成してみたいと思います。

今回は例に出していた以下のユーザーを作成していきたいと思います。

項目 作成ルール
ユーザー名 kimura.yuta@example.com
メールアドレス kimura.yuta@example.com
yuta
kimura
表示名 cm-yuta-kimura

パラメーターに以下の様に指定して作成します。

スタックは10秒ほどで作成することができました。

作成したユーザーを確認してみると、以下のように作成できておりました。

リソースの準備さえしてしまえば、作成にかかる時間は通常のマネコンから作成するのと変わらない位でユーザー作成を行うことができました!

作成をもっと楽にするオプションについて

先ほどのやってみるで紹介していたオプションについて簡単に触れておきたいと思います。

以下2つを組み合わせることで、この仕組みを使ってもらいやすくできるかつCloudFormationを使い慣れていない方でも操作しやすくなるかと思います。

CloudFormation クイック作成リンク

まずCloudFormationのクイック作成リンクについてです。

こちらを利用するとリンクを踏んだだけでCloudFormationの作成画面に遷移することができる様になります。

設定方法につきましては以下の記事でわかりやすく説明されておりますので、こちらをご参照ください。

許可セットのクイックリンクの更新

次に許可セットのクイックリンクの更新です。

こちらはユーザー作成専用の権限をもった許可セットに設定することを想定しております。

ユーザー作成専用の権限を持ったユーザーにログインした際のトップページを先ほど作成したCloudFormationのクイック作成リンクに指定することで、ログインするとすぐにCloudFormationの作成画面に遷移する仕組みです。

これを導入することで自然とCloudFormationからユーザーを作成してもらえる様に動線を確保します。

設定手順としては設定したい許可セットの画面から編集ボタンを選択します。

リレー状態に先ほど作成したクイック作成リンクを入力します。

以下の様に設定できたら設定完了です。

この設定をしておくとログインした際にトップページにアクセスするのではなく、CloudFormationの作成画面に遷移させることができます。

まとめ

いかがでしたでしょうか?

何か大きなミスを防ぐことができるというわけではないのですが、数百件のユーザーが作成されるケースにおいては細かなミスを事前に防げるという点で仕組みを導入する価値はあるのかなと思いました。

記事の中ではあまり触れませんでしたが、CloudFormationのパラメーターの正規表現を利用してメールアドレスの形式チェックなども行うことができます。

ユーザー作成のミスが多い場合などに導入を検討されてみていただけますと幸いです。

以上AWS事業部の木村でした。