CloudFormation カスタムリソースで Amazon Connect インスタンス・ユーザ作成をやってみた

2022.05.16

こんにちは、森田です。

Amazon Connect では、プレビュー版APIがあるものの、CloudFormationでは提供されていません。

そこで、CloudFormation のカスタムリソースを用いてAmazon Connect インスタンス・ユーザ作成をやってみました。

CloudFormationテンプレート

では、早速ですが、以下が CloudFormationテンプレートとなります。

launch.yml

AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  InstanceAlias:
    Type: String
    Description: New Instance Alias
  UserName:
    Type: String
    Default: admin
    Description: New User Name
  Password:
    Type: String
    Default: admin
    Description: New User Password
    NoEcho: true
  FirstName:
    Type: String
    Description: New User first name
  LastName:
    Type: String
    Description: New User last name
  Mail:
    Type: String
    Description: New User mail

Resources:

  UserPasswordParameter:
    Type: AWS::SSM::Parameter
    Properties:
      Name: !Sub 'ConnectMasterPassword-${AWS::StackName}'
      Type: String
      Value: !Ref Password
      Description: "MasterUserPassword for Connect"

  ConnectInstanceHandler:
    Type: Custom::ConnectInstanceHandler
    Properties:
      ServiceToken: !GetAtt "LambdaFunction.Arn"

  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName : !Join ['-', [!Sub '${AWS::StackName}-Lambda', !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId' ]]]]]]  
      Role: !GetAtt "LambdaExecutionRole.Arn"
      Runtime: "python3.8"
      Handler: index.lambda_handler
      Timeout: "300"
      Environment:
          Variables: 
            InstanceAlias : !Ref InstanceAlias
            FirstName : !Ref FirstName
            LastName : !Ref LastName
            Mail : !Ref Mail
            UserName : !Ref UserName
            Password : !Ref UserPasswordParameter
      Code:
        ZipFile: |
          import cfnresponse
          import sys
          import os
          import boto3
          import time

          def lambda_handler(event, context):
              if 'RequestType' not in event:
                return "Overwrite"
              
              if event['RequestType'] == 'Create':
                  create_connect(context)
                  cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                      {'Response': 'Success'})
              if event['RequestType'] == 'Delete':
                  print('Delete')
                  delete_connect()
                  cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                      {'Response': 'Success'})
              if event['RequestType'] == 'Update':
                  print('Update')
                  cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                      {'Response': 'Success'})
          def get_ssm(key):
              ssm = boto3.client('ssm', "ap-northeast-1")
              response = ssm.get_parameter(
                  Name=key,
                  WithDecryption=True,
              )
              try:
                  value = response['Parameter']['Value']
              except:
                  value = None
              return value

          def create_connect(context):
            client = boto3.client('connect')

            instance_alias = os.getenv('InstanceAlias')
            user_name = os.getenv('UserName')
            passwd = get_ssm(os.getenv('Password'))
            f_n = os.getenv('FirstName')
            l_n = os.getenv('LastName')
            mail= os.getenv('Mail')

            res = client.create_instance(
                IdentityManagementType='CONNECT_MANAGED',
                InstanceAlias=instance_alias,
                InboundCallsEnabled=True,
                OutboundCallsEnabled=True
            )
            assert res['ResponseMetadata']['HTTPStatusCode'] == 200, 'Connect インスタンスが正常に作成できませんでした'

            InstanceId = res["Id"]
            # Instance ID Save
            update_environ(
              context.invoked_function_arn, 
              InstanceId
            )


            # インスタンスがACTIVEになるまで待つ
            status = ''
            while status != 'ACTIVE':
                time.sleep(5)
                res= client.describe_instance(
                    InstanceId=InstanceId
                )
                status = res['Instance'] ['InstanceStatus']


            # RoutingProfileIdを取得する
            res = client.list_routing_profiles(
                InstanceId=InstanceId
            )
            RoutingProfileId = res['RoutingProfileSummaryList'][0]['Id']

            # SecurityProfileIdsを取得する
            res = client.list_security_profiles(
                InstanceId=InstanceId
            )
            SecurityProfileAdminId = list(filter(lambda x: x['Name']=='Admin', res['SecurityProfileSummaryList']))[0]['Id']

            res = client.create_user(
                Username=user_name,
                Password=passwd,
                IdentityInfo={
                    'FirstName': f_n,
                    'LastName': l_n,
                    'Email': mail
                },
                PhoneConfig={
                    'PhoneType': 'SOFT_PHONE'
                },
                SecurityProfileIds=[
                    SecurityProfileAdminId,
                ],
                RoutingProfileId=RoutingProfileId,
                InstanceId=InstanceId
            )

            assert res['ResponseMetadata']['HTTPStatusCode'] == 200, 'Connect ユーザが正常に作成できませんでした'

            print('Amazon Connect Setup Done!!')
            print('https://{}.my.connect.aws'.format(instance_alias))
          
          def delete_connect():
            client = boto3.client('connect')
            InstanceId = os.getenv('InstanceId')
            response = client.delete_instance(
              InstanceId=InstanceId
            )
          
          def update_environ(function_arn, InstanceId):
            client = boto3.client('lambda')
            response = client.update_function_configuration(
                FunctionName=function_arn,
                Environment={
                    'Variables': {
                        'InstanceId': InstanceId
                    }
                }
            )
                  
                    

  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: sample-lambda-policy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: arn:aws:logs:*:*:*
              - Effect: Allow
                Action:
                    - ssm:GetParameter
                Resource: "*"
              - Effect: Allow
                Action:
                    - lambda:UpdateFunctionConfiguration
                Resource: "*"
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/AmazonConnect_FullAccess

作成時と削除時のアクション

作成時

スタック作成時には、create_connect関数を実行します。こちらのインスタンス・ユーザ作成のコードについては、以下をご参照ください。

上記の記事との変更点は、パスワードをそのまま環境変数で渡さず、Parameter Storeで渡すように変更しています。

削除時

削除時は、delete_connect関数を実行します。Lambdaの環境変数より Connect インスタンスID を読み込み、Boto3で Connect インスタンス の削除を行っております。

試してみる

スタックの作成

上記のテンプレートを実際に流してみます。

パラメータでは、ユーザ名、パスワード等が求められますので、入力します。

実行後、しばらく待つと以下のようにコネクトインスタンスが確認できます。

スタックの削除

スタックの削除も行ってみます。スタックの削除後、コネクトインスタンスも削除されていることが確認できます。

最後に

以前作成したPythonスクリプトをCloudFormationとしてカプセル化することで、より扱いやすくなりました。

ただ、他のCloudFormationとして動作させるための構文は追加しているので、コードが長くなっています。

このまま CloudFormationのテンプレート を再利用するには、大変ですので、近いうちにモジュールとしてCloudFormationレジストリ登録もやってみたいと思います。