AWS

MFA必須のCognitoでDR対策を実装したい

koma

こんにちは!
ゲームをクリアしてしまうと寂しさに苛まれる一方、二周目突入するだけのエネルギーがないギリギリアラサー?こまっちゃんです。いいゲームとの出会いは一期一会、人生も一緒だなと学びを得た気がします。

さて、今回はMFAを設定しているAmazon Cognito(以下Cognito)のDR対策として、別リージョンにユーザーデータをレプリケーションし、耐障害性を確保する方法を検討したいと思います。

CognitoのDR対策について

前提として、Cognito自体は1つのリージョン内の複数のアベイラビリティゾーンで冗長化することで耐障害性を確保しています。そのため、別リージョンへのバックアップやクロスリージョンレプリケーションを行う機能は有しておらず、代替方法を検討する必要があります。

Amazon Cognito の耐障害性
Amazon Cognito の耐障害性

今回検討する内容

  • 東京から大阪リージョンにCognito(MFA有効)のユーザーデータをレプリケーションする
  • 大阪リージョンでユーザーが増えることを想定し、大阪リージョンのユーザーを東京で復元する

実装の前提

  • ユーザープールはMFAが有効化されている
  • ユーザープールにグループを設定していない
  • 大阪リージョンに、東京リージョンと同条件でCognitoを作成済み

別リージョンへのレプリケーション方法検討

別リージョンにレプリケーションする方法としては、Cognito User Profiles Export リファレンスアーキテクチャ を使用することができます。しかし、この方法は諸々の制約があり、MFAを有効化している場合は対象外となります(さらに、2025年3月に廃止予定)。

その他の方法としては、公式から以下の方法が提案されています。

  1. ユーザー移行 Lambda トリガーを使用してユーザーをインポートする
  2. CSV ファイルからユーザーをユーザープールにインポートする

それぞれの方法について、メリット・デメリットをまとめてみました。

概要メリットデメリット
ユーザーが移行先Cognitoにサインインする時、Lambdaが移行元Cognitoからユーザー情報を取得して移行するパスワードが変わらない・一人ずつ移行する必要がある
移行元Cognitoが正常に稼働している必要がある
CSV シートでユーザーを一括インポートする・一括でユーザーを移行可能
移行元Cognitoが停止していてもCSVがあれば移行可能
・パスワードはリセットされるため、再設定が必要
・MFAの再設定が必要
・インポートする時間のダウンタイムが発生

いわば、❶は異なるCognitoにユーザーを引っ越す場合に取る手法ですね。移行元のCognitoが利用できる状態である必要があり、災害発生時には機能しなくなってしまいます。一方、災害復旧後に元のCognitoへ移行する際には使用できそうです(ただし、MFA設定が有効化されている場合は、Lambdaが複数段階の受付処理を実行できないため不可)。

❷は、移行元が災害でダウンしていてもCSVさえ確保できていれば利用できるため、DR対策としては❶と比較して向いている印象です。しかし、CSV出力のタイミングによっては全ユーザーを網羅できない可能性があるため、今回はAWS Lambda(以下Lambda)を用いて直接ターゲットのCognitoにユーザーを複製しておく手法を試してみることにしました。※

※ 東京リージョンへのユーザー復元時に❷を使っています。

あわせて読みたい
Approaches for migrating users to Amazon Cognito user pools
Approaches for migrating users to Amazon Cognito user pools

想定アーキテクチャとバックアップの流れ

  1. ユーザーが東京リージョンのCognitoにサインアップする
  2. 確認後のLambdaトリガーを利用する Lambda「cognito-user-replicate-lmd」により、大阪リージョンのCognitoにユーザーが複製される
  3. 確認後のLambdaトリガーを利用するLambda「cognito-user-backup-lmd」により、大阪リージョンにユーザーがサインアップしたら、そのユーザーの情報を含むCSVを作成してS3に保存する
  4. CSVをダウンロードし、東京リージョンのCognitoにユーザーを復元する

実装

Cognitoユーザープール作成

サインインはメールアドレスを使用します。

MFAですが、Authenticatorアプリケーションを利用する方法にしてみました。

同じ内容で、東京と大阪リージョンそれぞれに作成しました。

Lambda作成

次にLambdaを作成します。今回はpython3.13を利用しています。

cognito-user-replicate-lmd

確認後のLambdaトリガーから受け取ったユーザー情報をもとに、AdminCreateUser APIを用いて大阪リージョンのCognito(ターゲットユーザープール)にユーザーを複製します。MFA情報も移行したかったのですが、移行できるAPIが見当たらなかった他、サインアップが成功すればMFAを再設定可能となるため、コードからは省いています。

本実装では、検証のため一時パスワードをログに残しています。

実運用される際は、必ずAWS Systems Manager Parameter Store 等、パスワード管理に適した場所でユーザーごとに保管することを推奨します。また、災害時にユーザーに一時パスワードとして通知できるようにAmazon SMS等を構築しておくと良いですね。

import boto3
import json
import os
import random
import string

def lambda_handler(event, context):
    cognito_client = boto3.client('cognito-idp', region_name='ap-northeast-3')
    target_user_pool_id = os.environ['TARGET_USER_POOL_ID']
    
    # イベントからユーザー属性を取得
    user_attributes = event['request']['userAttributes']
    username = user_attributes.get('email')
    
    try:
        # 新しいユーザープールでユーザーを作成
        response = cognito_client.admin_create_user(
            UserPoolId=target_user_pool_id,
            Username=username,
            UserAttributes=[
                {'Name': 'phone_number', 'Value': user_attributes.get('phone_number', '')},
                {'Name': 'email', 'Value': username},
            ],
            TemporaryPassword=generate_random_password(),
            MessageAction='SUPPRESS'
        )
        
        print(f"User {username} created successfully in the target user pool. response: {response}")
                
        return event
    
    except Exception as e:
        print(f"Error creating user or setting MFA for {username} in the target user pool: {str(e)}")
        raise e

def generate_random_password():
    length = 20
    characters = string.ascii_letters + string.digits + string.punctuation
    password = ''.join(random.choice(characters) for i in range(length))
    
    # 本来は決してログにパスワードは残さないでください
    print(f"Password {password} is created successfully")

    return password

次に、ロールの設定に移ります。

作成した東京リージョンのCognitoユーザープールにて、「拡張機能」より、確認後のLambdaトリガーを追加します。ここから先ほどのLambdaを割り当てることで、CognitoがLambdaを呼び出すための、リソースベースのポリシーステートメントが自動で設定されます。

次に、Lambdaの実行ロールにインラインポリシーを追加し、ターゲットとなる大阪リージョンのCognitoに対し、ユーザーの作成権限およびMFAの設定権限を付与します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "VisualEditor0",
            "Effect": "Allow",
            "Action": "cognito-idp:AdminCreateUser",
            "Resource": "arn:aws:cognito-idp:ap-northeast-3:<アカウントID>:userpool/<ユーザープールID>"
        }
    ]
}

これで東京リージョンの設定は完了です。

cognito-user-backup-lmd

次に、大阪リージョンでのLambdaの設定に取り掛かります。

このLambdaは、大阪リージョンで新規ユーザーが増えた場合に備えて、大阪のユーザーのバックアップをCSVで東京リージョンに移行できるようにすることを目的とします。具体的には、以下を実行するLambdaを検討します。

  1. 確認後のLambdaトリガー により、Cognitoから新規ユーザー情報を取得
  2. Cognitoから全ユーザー情報を取得
  3. CSVにユーザー情報を追加し、Amazon S3に格納
  • CSVを格納するためのS3を、あらかじめ大阪リージョンに作成しておいてください。
  • 実行時間エラーを防ぐため、実行時間はデフォルトの3秒から適切な値に変更してください。
  • CSVのヘッダーはtemplate.csv に従って設定しているので、常に最新のテンプレートを取得するようにしてください。
import boto3
import csv
import io
import os
from datetime import datetime

def lambda_handler(event, context):
    cognito_client = boto3.client('cognito-idp', region_name='ap-northeast-3')
    s3_client = boto3.client('s3')

    user_pool_id = os.environ['USER_POOL_ID']
    s3_bucket = os.environ['S3_BUCKET_NAME']

    # 全ユーザー情報を取得
    all_users = get_all_users(cognito_client, user_pool_id)

    # CSVファイルを作成
    csv_buffer = io.StringIO()
    csv_writer = csv.writer(csv_buffer)

    # ヘッダーを書き込む
    headers = [
        'profile', 'address', 'birthdate', 'gender', 'preferred_username', 
        'updated_at', 'website', 'picture', 'phone_number', 'phone_number_verified', 
        'zoneinfo', 'locale', 'email', 'email_verified', 'given_name', 'family_name', 
        'middle_name', 'name', 'nickname', 'cognito:mfa_enabled', 'cognito:username'
    ]
    csv_writer.writerow(headers)

    # ユーザー情報を書き込む
    for user in all_users:
        attributes = user.get('Attributes', '')
        email = get_attribute_value(attributes, 'email')

        row = [''] * len(headers)  # Initialize all fields with empty string
        row[8] = get_attribute_value(attributes, 'phone_number')
        row[12] = email
        row[13] = get_attribute_value(attributes, 'email_verified')
        row[19] = 'True'
        row[20] = email

        csv_writer.writerow(row)

    # S3にアップロード
    timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
    s3_key = f"{s3_bucket}_users_{timestamp}.csv"
    
    s3_client.put_object(
        Bucket=s3_bucket,
        Key=s3_key,
        Body=csv_buffer.getvalue(),
        ContentType='text/csv'
    )

    print(f"CSV file uploaded to s3://{s3_bucket}/{s3_key}")

    return event

def get_all_users(client, user_pool_id):
    users = []
    pagination_token = None

    while True:
        if pagination_token:
            response = client.list_users(
                UserPoolId=user_pool_id,
                PaginationToken=pagination_token
            )
        else:
            response = client.list_users(UserPoolId=user_pool_id)

        users.extend(response['Users'])

        pagination_token = response.get('PaginationToken')
        if not pagination_token:
            break

    return users

def get_attribute_value(attributes, name):
    return next((attr['Value'] for attr in attributes if attr['Name'] == name), None)

次に、ロールの設定に移ります。

東京リージョンのLambdaと同様にリソースベースのポリシーステートメントを付与したのち、S3にアクセスするための実行ロールを追加します。

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Sid": "VisualEditor0",
			"Effect": "Allow",
			"Action": [
				"s3:PutObject",
				"cognito-idp:ListUsers"
			],
			"Resource": [
				"arn:aws:s3:::koma-cognito-rep-users/*",
				"arn:aws:cognito-idp:ap-northeast-3:<アカウントID>:userpool/<ユーザープールID>"
			]
		}
	]
}

これで準備は完了しました。

検証

それでは、東京リージョンのCognitoにてユーザーを作成してみます。
サクッとCognitoのマネージドログインページから登録します。

無事、東京リージョンにユーザーが登録され、また大阪リージョンにもユーザーが複製されました。

東京リージョン Cognitoユーザー
大阪リージョン Cognitoユーザー

次に、大阪リージョンで新しくユーザーを追加登録します(画像下段)。

大阪リージョンのLambdaが動き、CSVが作成されていることを確認できました!

最後に、このCSVを利用して東京リージョンにユーザーを復元します。まず、Cognitoの「ユーザーをインポート」を開きます。

次に、インポートジョブを作成します。次の画像ではIAMロールは既存のものを使用していますが、「新しいIAMロールを作成」を選択して自動で作成することもできます。

大阪ユーザーで追加されたユーザーを追加することができました!

ちなみに、既存のユーザーについては次のエラーが出たため、上書きされませんでした!新しいユーザーのみ追加されるようです。

[SKIPPED] Line Number 3 - The user already existed.

最後に

いかがでしたでしょうか。
バックアップ〜復元までは無事に出来ましたが、結局パスワードやMFAは移行できていないし、懸念事項が色々と残る調査結果でした(この辺りをすっきりさせたくて題材にしたのですが、むしろ悩みが増えたとも言えます笑)。しかし、別リージョンへの退避自体は可能であることを手ずからチェックできたこと、MFA有効化していても別リージョンで同じユーザーを活用できると知ることができたことは良い経験でした。

Cognito自体はマルチAZにより耐障害性を確保する方針のため、別リージョンに退避させること自体想定されていないのかもしれませんが、同じような課題を抱える方々に、少しでも参考になりましたら幸いです。

課題

  • ユーザーインポート前のCSV内容の精査
    • インポートジョブですが、 email または phone_number で認証済みのユーザーでない場合にエラーが出てしまいました。
    • 現状、サインアップ時にバックアップをとっているので、最後に登録された人は認証前の状態でCSVにユーザー情報が登録されるので、バックアップファイルを作るタイミングは精査が必要かなと反省しています。
  • パスワードの差異
    • 復旧後、東京リージョンには以前のCognitoが存在しています。
    • 災害前に使用していたパスワードでログインできますが、大阪ですでに新しいパスワードを使用しているため、場合によってはパスワードを忘れた場合の手順に従い再設定してもらう必要があります。
    • また、復旧完了後は、大阪リージョンのCognitoユーザーを無効化しておく必要があります。
  • MFAの復旧
    • MFAデバイスをリセットする手順は公式ドキュメントに載っていません。今回の検証でも、リージョン間で別々のMFAデバイスを使用する必要があり、東京リージョンのMFAを失ってしまった場合には対応できません。
    • 一方、下記ドキュメントを参照する限り、一度SMSによるMFA再設定を経ることで、アプリケーションを使用したMFAの変更に対応はできるようです。

参考にした公式ドキュメント

AUTHOR
koma
koma
エンジニア
元々製薬業界で働いており、スクールを経てDWSに入社。主にgolangを使用したバックエンド業務に携わっているが、触ったことのない技術にも楽しく挑戦していきたいと考えている。趣味はスキューバダイビング。
記事URLをコピーしました