こんにちは。
アプリケーションサービス部、DevOps担当の兼安です。
本記事はこちらの記事の続きです。
今回はAmazon Cognito ユーザープールとアプリのデータベース(以下、それぞれCognito、DBと記述)を同期する方法を説明します。
本記事のターゲット
Webアプリケーションの開発経験があり、今後、Webアプリケーションをスケーラブルにしたい方を対象としています。
記事中にロードバランサー(=ALB)やAmazon EC2などが出てきますが、これらの説明は割愛していますので、AWSのコンピューティングサービスやデータベースサービスの知識があることが前提となります。
今回の題材
Cognitoはユーザープールにユーザー情報を保存します。
一方で、アプリケーションでは、しばしばユーザー情報と他のデータを結合します。
データの結合には、しばしばSQLで言うところのJOINが使われます。
したがって、私はDBにもユーザー情報を保存することが望ましいと考えています。
ただし、この方法を取るとCognitoとDBで二重管理されることになります。
この二重管理をどう解消するかが今回のテーマです。
Amazon Cognito ユーザープールとアプリケーションのデータベースの同期
今回のテーマに対して私の考える解決策は、シンプルに両方同時に更新する方法です。
CognitoはAPI/SDKを使ってユーザー情報を更新できます。
これを利用して、DBを更新する際に、Cognitoも同時に更新するようにします。
flowchart TD A[トランザクション開始] B[データベースのユーザー情報更新] C[Amazon Cognitoユーザープールの更新] D[コミット] E[ロールバック] A --> B B -->|成功| C B -->|失敗| E C -->|成功| D C -->|失敗| E
このとき、処理の順番には注意が必要です。
理由は、API/SDKを使った更新はロールバックができないためです。
まず、DBを更新し、成功したらCognitoを更新します。
Cognitoの更新を成功させてからコミットします。
このやり方は、更新処理やメール通知の実装などでも見られます。
DB以外の処理を最後に持ってきて、その成功を確認してからコミットすることで、DB、外部サービス、メール通知などとの整合性を保つことができます。
サンプルコード
では、実際にサンプルコードを紹介します。
まずは、DBにユーザー情報を保存するテーブルを作成します。
cognito_sub
はAmazon Cognitoのユーザー識別子(sub)を保存し、Amazon CognitoのユーザープールとDBを紐付けます。
この部分については、前回の記事を参照してください。
CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- アプリ内のユーザーID(内部管理用) cognito_sub UUID NOT NULL UNIQUE, -- Cognitoのユーザー識別子(sub) email VARCHAR(255) NOT NULL UNIQUE, -- メールアドレス username VARCHAR(100) NOT NULL, -- ユーザー名 first_name VARCHAR(100), -- 名(オプション) last_name VARCHAR(100), -- 姓(オプション) phone_number VARCHAR(20) UNIQUE, -- 電話番号(オプション) user_class VARCHAR(50) DEFAULT 'USER', -- ユーザークラス(USER, ADMINなど) user_status VARCHAR(50) DEFAULT 'ACTIVE', -- ユーザーステータス(ACTIVE, DISABLEDなど) created_at TIMESTAMPTZ DEFAULT now(), -- 作成日時 updated_at TIMESTAMPTZ DEFAULT now() -- 更新日時 );
DBとCognitoの同期処理を行うプログラムが以下です。
プログラムはPHPのLaravelを使っています。
Laravelのモデルを使ってDBの操作を行い、AWS SDK for PHPを使ってAmazon Cognitoのユーザープールの操作を行います。
これをUserService
として実装しました。
呼び出すときはUserService
のsyncUser
メソッドを呼び出します。
(サービスクラス自体の是非は今回の記事と対象外とさせてください。 )
<?php namespace App\Services; use Aws\CognitoIdentityProvider\CognitoIdentityProviderClient; use Illuminate\Support\Facades\DB; use App\Models\User; use Exception; class UserService { protected $cognitoClient; protected $userPoolId; public function __construct() { $this->cognitoClient = new CognitoIdentityProviderClient([ 'region' => env('AWS_DEFAULT_REGION'), 'version' => '2016-04-18', // 'credentials' => [ // 'key' => env('AWS_ACCESS_KEY_ID'), // 'secret' => env('AWS_SECRET_ACCESS_KEY'), // ], ]); $this->userPoolId = env('AWS_COGNITO_USER_POOL_ID'); } /** * ユーザー情報を DB & Cognito の両方に更新 * @param User $user Laravelのユーザーモデル * @param array $data 更新するユーザー情報 */ public function syncUser(User $user, array $data) { DB::beginTransaction(); // トランザクション開始 try { // 1. DBのユーザー情報を更新、なければ作成 $user->updateOrInsert([ 'cognito_sub' => $data['sub'], ], [ 'email' => $data['email'], 'username' => $data['username'] ?? '', 'first_name' => $data['first_name'] ?? '', 'last_name' => $data['last_name'] ?? '', 'phone_number' => $data['phone_number'] ?? '', ]); // 2. Cognito のユーザー情報を更新 $updateAttributes = []; if (!empty($data['username'])) { $updateAttributes[] = ['Name' => 'preferred_username', 'Value' => $data['username']]; } if (!empty($updateAttributes)) { $response = $this->cognitoClient->listUsers([ 'UserPoolId' => $this->userPoolId, 'Filter' => 'sub = "' . $data['sub'] . '"', ]); if (empty($response['Users'])) { throw new Exception("User not found."); } $this->cognitoClient->adminUpdateUserAttributes([ 'UserPoolId' => $this->userPoolId, 'Username' => $response['Users'][0]['Username'], 'UserAttributes' => $updateAttributes, ]); } // 3. 両方の更新が成功してはじめてコミット DB::commit(); return true; } catch (Exception $e) { DB::rollBack(); throw new Exception("ユーザー更新に失敗しました: " . $e->getMessage()); } } }
呼び出し方は以下の通りです。
<?php namespace App\Console\Commands; use Illuminate\Console\Command; use App\Services\UserService; class DualManagementCommand extends Command { /** * The name and signature of the console command. * * @var string */ protected $signature = 'app:dual-management-command {sub} {email}'; /** * The console command description. * * @var string */ protected $description = 'DBとCognitoの二重管理のコードをテストするコマンド'; /** * Execute the console command. */ public function handle() { $userService = new \App\Services\UserService(); $user = new \App\Models\User(); $userService->syncUser($user, [ 'sub' => $this->argument('sub'), 'email' => $this->argument('email'), 'username' => 'Satoshi Kaneyasu', 'first_name' => 'Kaneyasu', 'last_name' => 'Kaneyasu', 'phone_number' => '999-1234-5678', ] ); } }
credentials
の部分をコメントアウトしているのは、本プログラムをAmazon EC2で実行することを想定しているからです。
Amazon EC2で動かせば、IAMロールをEC2インスタンスにアタッチすることで、credentials
の設定を省略できます。
ユーザー情報を二重管理するもう一つの理由
CognitoとDBでユーザー情報を二重管理する理由はもう一つあります。
Amazon CognitoにはAPI/SDKによるリクエストに対するクォータ(制限)があります。
したがって、ユーザー情報を取得するのにあまりにも頻繁にリクエストを送ると、Amazon Cognitoによる制限に引っかかる可能性があります。
この問題に遭遇する確率を下げるためにも、DBにもユーザー情報を保存することが望ましいと考えています。
さらにいうと、ログインユーザーのユーザー名やメールアドレスなど、頻繁に使う情報はAmazon Elasticacheなどのインメモリデータベースにキャッシュすると、クォータ問題の回避と高速化が期待できます。
このあたりについては、別の機会に詳しく説明したいと思います。
次回の内容
今回は、Amazon Cognitoのユーザープールとアプリケーションのデータベースの同期方法を説明しました。
次回は、この記事のタイトルにあるスケーラビリティの話に戻ります。
私はスケーラビリティと言えばサーバーレスのAWS Lambdaだと思っているので、次回はAmazon CognitoとAWS Lambdaの連携方法を説明します。
兼安 聡(執筆記事の一覧)
アプリケーションサービス部 DS3課所属
2025 Japan AWS Top Engineers (AI/ML Data Engineer)
2025 Japan AWS All Certifications Engineers
2025 AWS Community Builders
Certified ScrumMaster
PMP
広島在住です。今日も明日も修行中です。