こんにちは、AWS チームの尾谷です。
ようやく僕のゴールデンウィークが始まりました!
久しぶりの 3 連休前のプレミアムフライデーに前職の同僚を飲みに誘って、京都まで行ってきました。
苦楽を共にした仲間と 3 年ぶりに会えて本当に楽しかったです!
その帰り道に、急に AWS の技術的な話が始まって
「Cognito ってリージョンサービスやないですかー?DR できるんすかね??」
という話になり、そのときは酔っ払っていたので、ユーザープールをエクスポート -> インポートすればいけるんちゃいますか?
程度にしか考えていなかったのですが、やってみるとシークレットがコピーできず苦戦したので、記録として残します。
考え方
AWS ドキュメントには、以下記述があったので *1、まずは、ユーザープールを送信する方法を模索することにしました。
ユーザープールは、オプション機能の設定方法に応じてユーザーデータを別の AWS リージョンに送信できます。
構成
想定した構成はこちらの通りです。
東京リージョンで稼働している環境を、災害が発生したらシドニーリージョンに IaC でデプロイし、Route 53 のレコードを切り替えて Disaster Recovery する。という構成を想定しました。
サイトを用意
こんな感じで、シンプルなページを作成しました。
- 東京リージョン
- シドニーリージョン
Cognito ユーザープール
Amplify Auth を構築してから約 1 年ぶりに Cognito を触ったのですが、UI が変わっていました。
設定に必要な項目は変わっていなかったので、サッと設定できました。
[ユーザープールを作成] ボタンをクリックして、
[Cognito ユーザープール] が選択されたまま、□ E メールのチェックボックスにチェックを入れて、[次へ] ボタンをクリック。
[○ MFA なし] を選択しました。
E メールは Amazon SES ではなく、Cognito で送信を選択しました。
認証用のサブドメインは適当に「otani」で。
アプリケーションクライアント
アプリケーションクライアントを開き、コールバック URL を指定しました。
東京とシドニーで設定が異なるのはサブドメインだけです。
- 東京リージョン (otani-test-cognito)
- シドニーリージョン (otani-test-cognito2)
Application Load Balancer
ALB の認証を追加します。
http (80) ポートを https (443) にリダイレクトして、
デフォルトルールに認証を追加。
認証画面が表示されるようになりました。
クライアントシークレット
この段落で記載する内容は、採用できなかったのと本題から逸れるので適宜、読み飛ばしていただければと思います。
ALB の認証で Cognito を利用する場合は、こちらに記載 の通り Cognito ユーザープールでクライアントシークレットを有効にする必要がありました。
Cognito でクライアントシークレットを有効にすると、AWS CLI が通らなくなり、ハッシュキーの生成という一手間が必要になりました。
re:Post の記事を参考に python でリクエストをしてみます。
アプリケーションクライアントで、[クライアントシークレットを表示] のトグルをクリックすると、クライアントシークレットが表示されます。
クライアントシークレットを利用して、ハッシュキーを生成します。
[cloudshell-user@ip-XXX-XXX-XXX-XXX ~]$ python3 cognito_hash_generate.py "<メールアドレス>" <クライアントシークレット> HASH: <生成されたハッシュキー> [cloudshell-u-ser@ip-XXX-XXX-XXX-XXX ~]$
情報が戻りました。
[cloudshell-user@ip-XXX-XXX-XXX-XXX ~]$ aws cognito-idp forgot-password --client-id ${CLIENT_ID} --username ${USER_EMAIL} --secret-hash ${HASH_KEY} { "CodeDeliveryDetails": { "Destination": "k***@f***", "DeliveryMedium": "EMAIL", "AttributeName": "email" } } [cloudshell-user@ip-10-2-10-232 ~]$
ユーザー移行の Lambda トリガー
裏どりのため AWS サポートにも質問してみましたが、リージョン間の Cognito をレプリケーションする機能はないようです。
レプリケーションではありませんが、代わりに「ユーザー移行の Lambda トリガー」というキーワードを教えていただきましたので、更に検証を進めていきました。
簡単に書くと、
- 元の Cognito ユーザープールとは別に、空のユーザープールを用意しておく
- 空のユーザープールに、ユーザー移行の Lambda トリガーを設定しておく
- ユーザーが空のユーザープールにサインインした際にユーザーが存在しないと、Lambda が元の Cognito に情報を取得しにいく
といった方法で、
ユーザープールを旧から新に移行したいときに利用する機能でした。
この方法を利用すれば、確かにユーザーは障害元で登録したユーザー名とパスワードを、DR 先で パスワードを再設定することなく 利用できますが、次の二点が解決できないと考えました。
- MFA が引き継げない (と思われる。未検証。)
- 災害が発生しているリソースを参照するという仕組み自体が DR 戦略として破綻している。
OIDC (OpenID Connect) 経由で接続する
Cognito は SLA 99.9% のサービスということで、1 年で 8.76 時間の障害は許容されますし、そもそも DR の想定は障害が発生したリージョンの機能が完全に使えない状態を想定すべきだと考えます。
ただ、障害発生中も Cognito だけは利用できる、という状況が許容されるのであれば、ユーザー移行の Lambda トリガーを使うまでもなく直接シドニーリージョンの Application Load Balancer から、OIDC 設定で、東京リージョンの Cognito を参照すれば良いのでは?と考えました。
以下、re:Post の記事を参考にして
OIDC 設定をしてみました。
すると、シドニーリージョンから東京リージョンの Cognito 認証が使えました!
Lambda で DR 環境にユーザーを登録する
以上より、まる一日使っていろいろ検証してきましたが結論、シークレットを移行するのは難しいと判断しました。
あらかじめ、DR 先のリージョンにも Cognito だけ構築しておき、平常時からユーザーが登録されたタイミングで Lambda をトリガーして、DR 先の Cognito ユーザープールにユーザー ID だけ登録しておくのが良さそうだと感じました。
災害が発生した際は、パスワードと MFA の設定が必要です。
東京リージョンに Lambda をコーディング
こちらのドキュメント を参考に Lambda を設定しました。
ランダムな文字列の生成は、@Scstechr さんの Qiita 記事を利用させていただきました。
エラーハンドリングはしていません。
import json import boto3 import random, string def lambda_handler(event, context): username = event['request']['userAttributes']['email'] password = '!' + str(randomname(10)) email = event['request']['userAttributes']['email'] user_pool_id = '<シドニーリージョンのユーザープール>' client_id = 'シドニーリージョンのアプリケーションクライアントID' session = boto3.Session(region_name="ap-southeast-2") cognito_idp = session.client('cognito-idp') cognito_idp.admin_create_user( UserPoolId=user_pool_id, Username=username, TemporaryPassword=password, UserAttributes=[{'Name': 'email', 'Value': email}], MessageAction='SUPPRESS' ) return event def randomname(n): randlst = [random.choice(string.ascii_letters + string.digits) for i in range(n)] return ''.join(randlst)
IAM ロールはシドニーリージョンの Cognito ユーザープールを対象とし、admin_create_user アクションのみ許可しました。 下図のように「確認後 Lambda トリガー」を設定しました。
動作検証
東京リージョンでサインアップします。
検証コードがメールで届くので、入力してサインアップを完了します。
すると、東京リージョンの Cognito ユーザープールには 検証済みのユーザーが追加され、
シドニーリージョンの Cognito ユーザープールには未検証のユーザーが作成されます。
繰り返しになりますが、本提案は DR 発生時に CloudFormation や Terraform などの IaC を使って別リージョンに環境を自動デプロイする想定で、Cognito だけ両リージョンで予め稼働させる必要があります。
また、デプロイが正常に完了したタイミングで以下を利用者にメール配信する仕組みを入れておくと親切だと感じました。
- DR が完了して、別リージョンで動作していることをお知らせ
- パスワードを再設定してもらう必要があることを促す
この方法もパスワードを再度設定してもらうデメリットとは別に、ユーザーが DR 環境で新たにパスワードを設定すると、元のユーザープールと乖離が発生するという問題をはらみます。
DR 発生後は、元のユーザープールは捨ててしまって、ユーザー移行 Lambda を設定したユーザープールを新たに作り直すといった対応が必要になりそうで、なかなか難しいですね。。
今後、Cognito ユーザープールにリージョン間のレプリケーション機能が実装される可能性があると考えますが、現段階ではこちらの方法が良いのではないかと思っています。もっと良い方法があれば是非ご教示ください!
都度、追記していきたいと思います。
以上、最後までお読みくださりありがとうございます。
*1:こちらは僕の解釈が誤っていて、Amazon SES と Amazon Pinpoint が対象とのことでした。