概要

Cognitoのユーザープールを作成するのに、AWS マネジメントコンソールを利用するのが面倒になり、AWS SDK for Pythonを利用して面倒さを解消したのですが、AWSにはCloudFormation(CFn)という素敵サービスがありますので、それを利用してさらに手間を省けないか検証してみました。

CFnでCognitoはすでに対応されていますが、MFA有効化してTOTPを選択するのに少し手間がかかりました。そのうちにもっと簡単に作成されるといいですね。(2018/11/09時点)

CloudFormation で Cognito
https://qiita.com/y13i/items/1923b47079bdf7c44eec

Amazon Cognito Now Supported by AWS CloudFormation
https://aws.amazon.com/jp/about-aws/whats-new/2017/04/amazon-cognito-now-supported-by-aws-cloudformation/

CognitoやMFA、TOTPってなんぞ?という方は下記をご参考ください。

Amazon Cognitoのワンタイムパスワード(TOTP)認証をNode.jsで試してみた
https://cloudpack.media/44521

PythonでAmazon Cognitoのユーザープールを作成してみる
https://cloudpack.media/44563

AWS CloudFormation(Cfn)とは

AWS CloudFormation (設定管理とオーケストレーション) | AWS
https://www.google.co.jp/search?q=CloudFormation

AWS CloudFormation は、クラウド環境内のすべてのインフラストラクチャリソースを記述してプロビジョニングするための共通言語を提供します。CloudFormation では、シンプルなテキストファイルを使用して、あらゆるリージョンとアカウントでアプリケーションに必要とされるすべてのリソースを、自動化された安全な方法でモデル化し、プロビジョニングできます。このファイルは、クラウド環境における真の単一ソースとして機能します。

CloudFormation超入門
https://dev.classmethod.jp/beginners/chonyumon-cloudformation/

誤解を恐れつつ一言で言えば、「自動的にAWS上で作りたいものを作ってくれる」サービスです。というか、そういう環境を用意してくれるサービスです。

CFnのテンプレート

今回作成したCFnのテンプレートです。

CFnのテンプレートはAWS マネジメントコンソールにあるデザイナーでも作成できますが、今回は利用せずに作成しました。フォーマットはJSON、YAMLが利用できますが、YAMLで作成しています。

検証用のユーザーをあわせて作成していますが、ユーザー名、パスワードはべた書きなので、あしからず。

ソースはGitHubにもアップしています。
https://github.com/kai-kou/create-cognito-user-pool-at-cloudformation

create-user-pool-template.yaml

Resources:
  # ユーザープールの作成
  UserPool:
    Type: "AWS::Cognito::UserPool"
    Properties:
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireUppercase: true
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
      UserPoolName:
        Ref: AWS::StackName
      MfaConfiguration: 'OFF'
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false
        UnusedAccountValidityDays: 7

  # ユーザープールにアプリクライアントを作成
  UserPoolClient:
    Type: "AWS::Cognito::UserPoolClient"
    Properties:
      UserPoolId:
        Ref: UserPool
      ClientName:
        Ref: AWS::StackName
      RefreshTokenValidity: 30

  # ユーザープールでMFA(TOTP)有効化
  UserPoolMfaConfig:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CognitoSetUserPoolMfaConfigFunction.Arn
      UserPoolId:
        Ref: UserPool

  # 検証用のユーザーを追加
  AdminCreateUser:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CognitoAdminCreateUserFunction.Arn
      UserPoolId:
        Ref: UserPool
      UserName: hoge
      Password: hogeHoge7!

  # 検証用のユーザーを追加
  AdminCreateUser2:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CognitoAdminCreateUserFunction.Arn
      UserPoolId:
        Ref: UserPool
      UserName: hoge2
      Password: hogeHoge7!

  # ユーザープールでMFA(TOTP)有効化するLambda関数
  CognitoSetUserPoolMfaConfigFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt CognitoFunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          import cfnresponse
          import boto3
          def handler(event, context):
            # スタック削除時にも実行されるので、処理せずに終了させる
            if event['RequestType'] == 'Delete':
              cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
              return

            # UserPoolIDを取得する
            user_pool_id = event['ResourceProperties']['UserPoolId']
            print(f'user_pool_id: {user_pool_id}')

            # MFA有効化してTOTPを指定する
            response_data = {}
            try:
              client = boto3.client('cognito-idp')
              response_data = client.set_user_pool_mfa_config(
                UserPoolId=user_pool_id,
                SoftwareTokenMfaConfiguration={
                  'Enabled': True
                },
                MfaConfiguration='ON'
              )

            except Exception as e:
              print("error: " + str(e))
              response_data = {'error': str(e)}
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
              return

            print(response_data)
            cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
      Runtime: python3.6

  # ユーザープールにテスト用ユーザーを作成するLambda関数
  CognitoAdminCreateUserFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt CognitoFunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          import cfnresponse
          import boto3
          def handler(event, context):
            print(event['RequestType'])
            # スタック削除時にも実行されるので、処理せずに終了させる
            if event['RequestType'] == 'Delete':
              cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
              return

            # UserPoolIDを取得する
            user_pool_id = event['ResourceProperties']['UserPoolId']
            print(f'user_pool_id: {user_pool_id}')

            # ユーザー名、パスワードを取得する
            username = event['ResourceProperties']['UserName']
            password = event['ResourceProperties']['Password']

            # 検証用のユーザーを作成する
            response_data = {}
            try:
              client = boto3.client('cognito-idp')
              response_data = client.admin_create_user(
                UserPoolId=user_pool_id,
                Username=username,
                TemporaryPassword=password,
                MessageAction='SUPPRESS'
              )

              # Datetime型のままなので文字列に変換する
              response_data['User']['UserCreateDate'] = \
                response_data['User']['UserCreateDate'].strftime('%c')
              response_data['User']['UserLastModifiedDate'] = \
                response_data['User']['UserLastModifiedDate'].strftime('%c')

            except Exception as e:
              print("error: " + str(e))
              response_data = {'error': str(e)}
              cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
              return

            print(response_data)
            cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)
      Runtime: python3.6

  # Lambda関数実行用のロール
  CognitoFunctionExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Path: "/"
      Policies:
      - PolicyName: root
        PolicyDocument:
          Version: '2012-10-17'
          Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: "arn:aws:logs:*:*:*"
          # Cognitoの操作権限を付与する
          - Effect: Allow
            Action:
              - cognito-idp:*
            Resource: "arn:aws:cognito-idp:*:*:userpool/*"

はい。
ざっくりとポイントだけ。

ユーザープール作成時にTOTPを利用したMFA有効化はできない

MfaConfiguration というパラメータでMFA有効化できるのですが、いまのところ有効化時にTOTPを指定するパラメータがありませんでしたので、とりあえず、OFF で作成しています。(2018/11/09時点)

パラメータについては公式のドキュメントが参考になります。

AWS::Cognito::UserPool – AWS CloudFormation
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpool.html

ユーザープール名UserPoolName の指定はRef: AWS::StackName とすることでCFnのスタック名としています。

  UserPool:
    Type: "AWS::Cognito::UserPool"
    Properties:
      Policies:
        PasswordPolicy:
          MinimumLength: 8
          RequireUppercase: true
          RequireLowercase: true
          RequireNumbers: true
          RequireSymbols: true
      UserPoolName:
        Ref: AWS::StackName
      MfaConfiguration: 'OFF'
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: false
        UnusedAccountValidityDays: 7

AWS Lambda-backed カスタムリソースを利用してMFAを有効化する

CFnにはカスタムリソースというものが用意されており、CFnが対応していないリソースを自前で管理することができます。さらにカスタムリソースを定義する際にLambda関数を利用することができます。便利ですね^^

AWS Lambda-backed カスタムリソース – AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html

Lambda 関数とカスタム リソースを関連付けた場合、この関数はカスタム リソースが作成、更新、または削除されるたびに呼び出されます。

こちらを利用して、CognitoのユーザープールでMFA有効化します。

カスタムリソースの定義

  UserPoolMfaConfig:
    Type: Custom::CustomResource
    Properties:
      ServiceToken: !GetAtt CognitoSetUserPoolMfaConfigFunction.Arn
      UserPoolId:
        Ref: UserPool

UserPoolMfaConfig はカスタムリソースの名称となり任意で指定できます。Type: Custom::CustomResource とすることでカスタムリソースと定義します。
ServiceToken に実行するLambda関数のArnを指定、UserPoolId はLambda関数に引き渡すパラメータになります。ここではRef: UserPool として、作成されたユーザープールのIDを渡しています。

Lambda関数の定義

 CognitoSetUserPoolMfaConfigFunction:
    Type: AWS::Lambda::Function
    Properties:
      Handler: index.handler
      Role: !GetAtt CognitoFunctionExecutionRole.Arn
      Code:
        ZipFile: !Sub |
          (略)
      Runtime: python3.6

実行するLambda関数は作成済みの関数も指定できますが、同一テンプレート(スタック)内で作成することもできます。(すごい!便利!

今回はAWS SDKを利用するだけでよかったので、ソースも含めて定義していますが、もし他のライブラリをインポートしたい場合は、ソースをZIP圧縮してS3へアップしたものを指定することができます。通常のAWS Lambda関数をデプロイする方法と同じです。

Lambda関数の実行に必要なロールも同一テンプレート(スタック)内で作成して指定ができます。

Handler: index.handler としていますが、index に関してはFCnで関数を作成する場合、ファイル名がindex.py となるので、変更不可のようです。

Lambda関数の詳細については下記が参考になります。

AWS Lambda 関数コード – AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html

Lambda-backedカスタムリソースの雛形
https://www.weblog-beta.com/posts/custom-resource/

Lamnda関数の実装

import cfnresponse
import boto3
def handler(event, context):
  # スタック削除時にも実行されるので、処理せずに終了させる
  if event['RequestType'] == 'Delete':
    cfnresponse.send(event, context, cfnresponse.SUCCESS, {})
    return

  # UserPoolIDを取得する
  user_pool_id = event['ResourceProperties']['UserPoolId']
  print(f'user_pool_id: {user_pool_id}')

  # MFA有効化してTOTPを指定する
  response_data = {}
  try:
    client = boto3.client('cognito-idp')
    response_data = client.set_user_pool_mfa_config(
      UserPoolId=user_pool_id,
      SoftwareTokenMfaConfiguration={
        'Enabled': True
      },
      MfaConfiguration='ON'
    )

  except Exception as e:
    print("error: " + str(e))
    response_data = {'error': str(e)}
    cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
    return

  print(response_data)
  cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data)

Lambda関数はCFnのスタック作成時だけでなく、更新や削除時にも実行されます。
今回は削除するケースに対応するのに、if event['RequestType'] == 'Delete': で、削除時には処理をスキップするようにしています。

例外のハンドリングなど他にも考慮すべき点がありますが、下記が参考になります。

CloudFormationでLambdaを実行する
https://prgrmmbl.com/2018/07/01/Lambda-With-CloudFormation.html

Lambda-backed Custom Resourceのcfn-responseモジュールを利用する上での注意点
https://dev.classmethod.jp/cloud/aws/note-about-using-delete-response-with-cfn-response-module-in-lambda-backed-custom-resource/

Cognitoのユーザープールの操作にはPythonで提供されているAWS SDKのboto3 を利用しています。SDKの利用に関しては下記をご参考ください。

PythonでAmazon Cognitoのユーザープールを作成してみる
https://cloudpack.media/44563

作成する

CFnで上記テンプレートからスタックを作成してみます。

AWS CLIで作成する

AWS CLIを利用して、CFnのスタックを作成してみます。
上記のテンプレートを適当なディレクトリに保存して実行します。

AWS CLIのインストールについては下記が参考になります。

AWS CLIのインストール
https://qiita.com/yuyj109/items/3163a84480da4c8f402c

> aws --version
aws-cli/1.16.27 Python/3.6.6 Darwin/17.7.0 botocore/1.12.17

> mkdir 任意のディレクトリ
> cd 任意のディレクトリ
> touch create-user-pool-template.yaml
> vi create-user-pool-template.yaml

create-stack – AWS CLI 1.16.51 Command Reference
https://docs.aws.amazon.com/cli/latest/reference/cloudformation/create-stack.html

AWS CloudformationをAWS CLIから使ってみる
https://techte.co/2018/01/25/cloudformation/

> aws cloudformation create-stack \
  --region ap-northeast-1 \
  --stack-name cognito-totp-mfa-user-pool \
  --template-body file://create-user-pool-template.yaml \
  --capabilities CAPABILITY_IAM

{
    "StackId": "arn:aws:cloudformation:ap-northeast-1:xxxxx:stack/cognito-totp-mfa-user-pool/3d3591e0-e3ca-11e8-bdc2-503a6ff78e2a"
}

create-stack でスタックを作成します。
ロールを作成する場合、--capabilities CAPABILITY_IAM と指定しておかないと、Requires capabilities : [CAPABILITY_IAM] と怒られるので、ご注意ください。

Requires capabilities : [CAPABILITY_IAM]
https://github.com/awslabs/serverless-application-model/issues/51

describe-stacks でスタックの情報が確認できます。

> aws cloudformation describe-stacks \
  --stack-name cognito-totp-mfa-user-pool

{
    "Stacks": [
        {
            "StackId": "arn:aws:cloudformation:ap-northeast-1:xxxxx:stack/cognito-totp-mfa-user-pool/752fbbe0-e3d2-11e8-8ab5-50a686699882",
            "StackName": "cognito-totp-mfa-user-pool",
            "CreationTime": "2018-11-09T03:49:28.365Z",
            "RollbackConfiguration": {},
            "StackStatus": "CREATE_IN_PROGRESS",
            "DisableRollback": false,
            "NotificationARNs": [],
            "Capabilities": [
                "CAPABILITY_IAM"
            ],
            "Tags": [],
            "EnableTerminationProtection": false
        }
    ]
}

AWS マネジメントコンソールでもスタックが作成ことが確認できます。

AWS マネジメントコンソールで作成する

最近CloudFormationの操作UIが新しくなりプレビュー版が利用できるようになったみたいです。LambdaのUIっぽくて良い感じです。せっかくなので新しいUIで試してみます。

ローカルからファイルを指定するとS3にアップロードされます。

最後に、テンプレート内でロール作成の定義があるから気をつけてねと確認がでてきます。
AWS CLIで作成したときの--capabilities CAPABILITY_IAM と同じ確認ですね。

ユーザープールの確認

Cognitoでユーザープールが作成されたか確認してみます。

はい。
ちゃんとMFA有効化されて、ユーザーも作成されています。

やったぜ。

最後に、スタックを削除して、関連するリソースが削除されることを確認しておきます。
注意点としては、テンプレートで定義した各リソースは削除されますが、S3にアップロードされたテンプレートファイルや、Lambda関数のログは残ったままになりますので、そちらは手動で削除することになります。(未確認)

> aws cloudformation delete-stack \
  --stack-name cognito-totp-mfa-user-pool

> aws cloudformation describe-stacks \
  --stack-name cognito-totp-mfa-user-pool

An error occurred (ValidationError) when calling the DescribeStacks operation: Stack with id cognito-totp-mfa-user-pool does not exist

まとめ

CFnを利用することで、AWS マネジメントコンソールやスクリプトを書くことなくAWSのリソースを作成・管理できることがわかりました。カスタムリソースを利用することで、より柔軟なリソース管理もできるので、応用がとても効きます。

いままで使ったことがなかったのですが、環境構築の再現性も非常に高いので、AWSを利用するのに必須といっていいかもしれません。

参考

CloudFormation で Cognito
https://qiita.com/y13i/items/1923b47079bdf7c44eec

Amazon Cognito Now Supported by AWS CloudFormation
https://aws.amazon.com/jp/about-aws/whats-new/2017/04/amazon-cognito-now-supported-by-aws-cloudformation/

Amazon Cognito Resource Types Reference
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-reference-cognito.html

Lambda-backedカスタムリソースの雛形
https://www.weblog-beta.com/posts/custom-resource/

Amazon Cognitoのワンタイムパスワード(TOTP)認証をNode.jsで試してみた
https://cloudpack.media/44521

PythonでAmazon Cognitoのユーザープールを作成してみる
https://cloudpack.media/44563

AWS CloudFormation (設定管理とオーケストレーション) | AWS
https://www.google.co.jp/search?q=CloudFormation

CloudFormation超入門
https://dev.classmethod.jp/beginners/chonyumon-cloudformation/

AWS::Cognito::UserPool – AWS CloudFormation
https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-cognito-userpool.html

AWS Lambda-backed カスタムリソース – AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/template-custom-resources-lambda.html

AWS Lambda 関数コード – AWS CloudFormation
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html

Lambda-backedカスタムリソースの雛形
https://www.weblog-beta.com/posts/custom-resource/

CloudFormationでLambdaを実行する
https://prgrmmbl.com/2018/07/01/Lambda-With-CloudFormation.html

Lambda-backed Custom Resourceのcfn-responseモジュールを利用する上での注意点
https://dev.classmethod.jp/cloud/aws/note-about-using-delete-response-with-cfn-response-module-in-lambda-backed-custom-resource/

AWS CLIのインストール
https://qiita.com/yuyj109/items/3163a84480da4c8f402c

Requires capabilities : [CAPABILITY_IAM]
https://github.com/awslabs/serverless-application-model/issues/51

create-stack – AWS CLI 1.16.51 Command Reference
https://docs.aws.amazon.com/cli/latest/reference/cloudformation/create-stack.html

元記事はこちら

AWS CloudFormationでCognitoユーザープールをMFAのTOTPを有効にして作成する