この記事は約5分で読めます。
こんにちは。
アプリケーションサービス部、DevOps担当の兼安です。
本記事はこちらの記事の続きです。
今回はAmazon Cognito ユーザープールとアプリのデータベース(以下、それぞれCognito、DBと記述)を同期する方法を説明します。
本記事のターゲット
Webアプリケーションの開発経験があり、今後、Webアプリケーションをスケーラブルにしたい方を対象としています。
記事中にロードバランサー(=ALB)やAmazon EC2などが出てきますが、これらの説明は割愛していますので、AWSのコンピューティングサービスやデータベースサービスの知識があることが前提となります。
今回の題材
Cognitoはユーザープールにユーザー情報を保存します。
一方で、アプリケーションでは、しばしばユーザー情報と他のデータを結合します。
データの結合には、しばしばSQLで言うところのJOINが使われます。
したがって、私はDBにもユーザー情報を保存することが望ましいと考えています。
ただし、この方法を取るとCognitoとDBで二重管理されることになります。
この二重管理をどう解消するかが今回のテーマです。
Amazon Cognito ユーザープールとアプリケーションのデータベースの同期
今回のテーマに対して私の考える解決策は、シンプルに両方同時に更新する方法です。
CognitoはAPI/SDKを使ってユーザー情報を更新できます。
これを利用して、DBを更新する際に、Cognitoも同時に更新するようにします。
このとき、処理の順番には注意が必要です。
理由は、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
メソッドを呼び出します。
(サービスクラス自体の是非は今回の記事と対象外とさせてください。 )
<?phpnamespace 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());}}}
呼び出し方は以下の通りです。
<?phpnamespace 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課所属
2024 Japan AWS Top Engineers (Database)
2024 Japan AWS All Certifications Engineers
2025 AWS Community Builders
Certified ScrumMaster
PMP
広島在住です。今日も明日も修行中です。