ForgeVision Engineer Blog

フォージビジョン エンジニア ブログ

Amazon Cognito ユーザープールは DR できるか?

こんにちは、AWS チームの尾谷です。

ようやく僕のゴールデンウィークが始まりました!
久しぶりの 3 連休前のプレミアムフライデーに前職の同僚を飲みに誘って、京都まで行ってきました。

苦楽を共にした仲間と 3 年ぶりに会えて本当に楽しかったです!
その帰り道に、急に AWS の技術的な話が始まって

「Cognito ってリージョンサービスやないですかー?DR できるんすかね??」

という話になり、そのときは酔っ払っていたので、ユーザープールをエクスポート -> インポートすればいけるんちゃいますか?
程度にしか考えていなかったのですが、やってみるとシークレットがコピーできず苦戦したので、記録として残します。

考え方

AWS ドキュメントには、以下記述があったので *1、まずは、ユーザープールを送信する方法を模索することにしました。

ユーザープールは、オプション機能の設定方法に応じてユーザーデータを別の AWS リージョンに送信できます。

docs.aws.amazon.com

構成

想定した構成はこちらの通りです。
東京リージョンで稼働している環境を、災害が発生したらシドニーリージョンに 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 でリクエストをしてみます。

repost.aws

アプリケーションクライアントで、[クライアントシークレットを表示] のトグルをクリックすると、クライアントシークレットが表示されます。

クライアントシークレットを利用して、ハッシュキーを生成します。

[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 トリガー」というキーワードを教えていただきましたので、更に検証を進めていきました。

docs.aws.amazon.com

簡単に書くと、

  1. 元の Cognito ユーザープールとは別に、空のユーザープールを用意しておく
  2. 空のユーザープールに、ユーザー移行の Lambda トリガーを設定しておく
  3. ユーザーが空のユーザープールにサインインした際にユーザーが存在しないと、Lambda が元の Cognito に情報を取得しにいく

といった方法で、

ユーザープールを旧から新に移行したいときに利用する機能でした。

この方法を利用すれば、確かにユーザーは障害元で登録したユーザー名とパスワードを、DR 先で パスワードを再設定することなく 利用できますが、次の二点が解決できないと考えました。

  • MFA が引き継げない (と思われる。未検証。)
  • 災害が発生しているリソースを参照するという仕組み自体が DR 戦略として破綻している。

OIDC (OpenID Connect) 経由で接続する

Cognito は SLA 99.9% のサービスということで、1 年で 8.76 時間の障害は許容されますし、そもそも DR の想定は障害が発生したリージョンの機能が完全に使えない状態を想定すべきだと考えます。

aws.amazon.com

ただ、障害発生中も Cognito だけは利用できる、という状況が許容されるのであれば、ユーザー移行の Lambda トリガーを使うまでもなく直接シドニーリージョンの Application Load Balancer から、OIDC 設定で、東京リージョンの Cognito を参照すれば良いのでは?と考えました。

以下、re:Post の記事を参考にして

repost.aws

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 が対象とのことでした。