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

こんにちは!
ゲームをクリアしてしまうと寂しさに苛まれる一方、二周目突入するだけのエネルギーがないギリギリアラサー?こまっちゃんです。いいゲームとの出会いは一期一会、人生も一緒だなと学びを得た気がします。
さて、今回はMFAを設定しているAmazon Cognito(以下Cognito)のDR対策として、別リージョンにユーザーデータをレプリケーションし、耐障害性を確保する方法を検討したいと思います。
CognitoのDR対策について
前提として、Cognito自体は1つのリージョン内の複数のアベイラビリティゾーンで冗長化することで耐障害性を確保しています。そのため、別リージョンへのバックアップやクロスリージョンレプリケーションを行う機能は有しておらず、代替方法を検討する必要があります。

今回検討する内容
- 東京から大阪リージョンにCognito(MFA有効)のユーザーデータをレプリケーションする
- 大阪リージョンでユーザーが増えることを想定し、大阪リージョンのユーザーを東京で復元する
実装の前提
- ユーザープールはMFAが有効化されている
- ユーザープールにグループを設定していない
- 大阪リージョンに、東京リージョンと同条件でCognitoを作成済み
別リージョンへのレプリケーション方法検討
別リージョンにレプリケーションする方法としては、Cognito User Profiles Export リファレンスアーキテクチャ を使用することができます。しかし、この方法は諸々の制約があり、MFAを有効化している場合は対象外となります(さらに、2025年3月に廃止予定)。
その他の方法としては、公式から以下の方法が提案されています。
それぞれの方法について、メリット・デメリットをまとめてみました。
概要 | メリット | デメリット | |
---|---|---|---|
❶ | ユーザーが移行先Cognitoにサインインする時、Lambdaが移行元Cognitoからユーザー情報を取得して移行する | パスワードが変わらない | ・一人ずつ移行する必要がある ・移行元Cognitoが正常に稼働している必要がある |
❷ | CSV シートでユーザーを一括インポートする | ・一括でユーザーを移行可能 ・移行元Cognitoが停止していてもCSVがあれば移行可能 | ・パスワードはリセットされるため、再設定が必要 ・MFAの再設定が必要 ・インポートする時間のダウンタイムが発生 |
いわば、❶は異なるCognitoにユーザーを引っ越す場合に取る手法ですね。移行元のCognitoが利用できる状態である必要があり、災害発生時には機能しなくなってしまいます。一方、災害復旧後に元のCognitoへ移行する際には使用できそうです(ただし、MFA設定が有効化されている場合は、Lambdaが複数段階の受付処理を実行できないため不可)。
❷は、移行元が災害でダウンしていてもCSVさえ確保できていれば利用できるため、DR対策としては❶と比較して向いている印象です。しかし、CSV出力のタイミングによっては全ユーザーを網羅できない可能性があるため、今回はAWS Lambda(以下Lambda)を用いて直接ターゲットのCognitoにユーザーを複製しておく手法を試してみることにしました。※
※ 東京リージョンへのユーザー復元時に❷を使っています。

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

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

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

同じ内容で、東京と大阪リージョンそれぞれに作成しました。
Lambda作成
次にLambdaを作成します。今回はpython3.13を利用しています。
cognito-user-replicate-lmd
確認後のLambdaトリガーから受け取ったユーザー情報をもとに、AdminCreateUser APIを用いて大阪リージョンのCognito(ターゲットユーザープール)にユーザーを複製します。MFA情報も移行したかったのですが、移行できるAPIが見当たらなかった他、サインアップが成功すればMFAを再設定可能となるため、コードからは省いています。
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を検討します。
- 確認後のLambdaトリガー により、Cognitoから新規ユーザー情報を取得
- Cognitoから全ユーザー情報を取得
- CSVにユーザー情報を追加し、Amazon S3に格納
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のマネージドログインページから登録します。

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


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

大阪リージョンの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の変更に対応はできるようです。