Amazon Cognitoで構築するスケーラブルなWebアプリケーション④ - Amazon Cognito ユーザープールとアプリのデータベースの同期

記事タイトルとURLをコピーする

こんにちは。
アプリケーションサービス部、DevOps担当の兼安です。

本記事はこちらの記事の続きです。

blog.serverworks.co.jp

今回は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として実装しました。
呼び出すときはUserServicesyncUserメソッドを呼び出します。
(サービスクラス自体の是非は今回の記事と対象外とさせてください。 )

<?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でユーザー情報を二重管理する理由はもう一つあります。

docs.aws.amazon.com

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
広島在住です。今日も明日も修行中です。