メインコンテンツにスキップ

Cognitoだけじゃ足りなかった!DynamoDBにユーザーデータを自動作成する仕組みと、その理由

タグ: 🏷 AWS ,DynamoDB ,Cognito ,Lambda

背景

「認証システムならAWS Cognitoだけで十分なんじゃない?」— サーバーレス認証を始める多くの開発者が、きっとそう思うはずです。僕も最初はそう考えていました。確かにCognitoは強力で、基本的なユーザー管理は完璧にこなしてくれます。

しかし、実際のアプリケーション開発では、Cognitoだけでは管理しきれないユーザー関連情報が必要になるケースがほとんどです。僕たちのプロジェクトでも、当初はCognitoだけで進める予定だったのですが、開発を進めるうちにDynamoDBでの補完的なユーザーデータ管理がどうしても必要になってきました。

この記事では、なぜDynamoDBが必要になったのか、どのような仕組みでユーザーデータを自動作成しているのか、そしてCognitoとの役割分担をどう設計したのかを、実際の実装コードと運用データを交えながら、詳しく記録しておきたいと思います。

CognitoとDynamoDBの役割分担 オフィスビルに例えてみましょう。

  • Cognito: ビルの受付・警備員です。社員証(ID/パスワード)を確認して、入館を許可(認証)するのが仕事です。社員の名前や所属部署は管理しますが、個人の机の上にある書類(アプリケーションデータ)までは管理しません。
  • DynamoDB: 社員一人ひとりの机やキャビネットです。個人の設定、プロジェクトの書類、メモなど、アプリケーション固有のデータを保管する場所です。

このように、役割を分けることで、安全で柔軟なシステムを構築できます。

Cognitoの限界と不足していた機能

Cognitoは認証のプロフェッショナルですが、アプリケーションのデータを何でも保存できるわけではありません。特に以下の点で、僕たちは限界を感じました。

  • データ制限: 保存できる情報の数や量に限りがある。
  • 検索・クエリ制限: 複雑な条件でユーザーを探すのが難しい。
  • ビジネスロジック制限: アプリケーション独自のルールや処理を追加しにくい。

私たちのアプリでは、ユーザーのプロフィール画像URL、課金プラン、利用状況といった、Cognitoの枠には収まらない情報が必要でした。これが、専用のデータベースとしてDynamoDBを導入した理由です。

DynamoDB設計とテーブル構造

Single Table Designとは? 従来のリレーショナルデータベース(MySQLなど)では、情報ごとにテーブルを分けるのが一般的でしたよね(例: usersテーブル、postsテーブル)。でも、DynamoDBのようなNoSQLデータベースでは、関連するデータをあえて一つの大きなテーブルにまとめ、PK(パーティションキー)とSK(ソートキー)という特殊なキーを使って効率的にデータを取り出す「Single Table Design」という設計手法がよく使われるんです。これによって、非常に高速なデータアクセスが可能になるんですよ。

私たちはこの「Single Table Design」を採用し、ユーザー関連の様々な情報を一つのテーブルで管理するように設計しました。

自動作成システムの実装

Lambda関数での自動作成トリガー

ユーザーがメールアドレスの確認を完了した瞬間に、DynamoDBにそのユーザーのデータを自動で作成する仕組みを構築しました。これを実現してくれるのが、Cognito Post Confirmation Triggerなんです。

Cognito Post Confirmation Triggerとは? 「ユーザーがメール確認を終えたら、それを合図(トリガー)に、指定したLambda関数を自動で実行してください」というCognitoの設定です。この仕組みを使うことで、「Cognitoでのユーザー確定」と「DynamoDBへのデータ作成」を連動させることができます。

データ作成の流れ

  1. ユーザーがメールアドレスの確認リンクをクリックします。
  2. Cognitoが「このユーザーは本物です」と確定します。
  3. Cognitoが「Post Confirmation」の合図を送り、私たちのLambda関数が起動します。
  4. Lambda関数が、ユーザーのメールアドレスや名前を受け取り、DynamoDBに新しいユーザーデータを作成します。

これにより、ユーザーが初めてログインする時には、すでに対応するデータがDynamoDBに準備万端の状態で存在することになります。

template.yamlの内容

template.yamlの抜粋です。

この中で、CognitoUserPoolリソースのLambdaConfigプロパティにあるPostConfirmation: !GetAtt CreateUserDataFunction.Arnが、ユーザー確認後にCreateUserDataFunctionというLambda関数を呼び出す設定を行っています。また、そのLambda関数を呼び出すための権限設定CreateUserDataFunctionCognitoPermissionも重要です。

   # Cognito User Pool
   CognitoUserPool:
     Type: AWS::Cognito::UserPool
     Properties:
       UserPoolName: !Sub "${AWS::StackName}-user-pool"
       LambdaConfig:
         PostConfirmation: !GetAtt CreateUserDataFunction.Arn
         # ↑ユーザー確認後にCreateUserDataFunctionというLambda関数を呼び出す設定
       Schema:
         - Name: email
           AttributeDataType: String
           Required: true
           Mutable: false
         - Name: name
           AttributeDataType: String
           Required: true
           Mutable: true
 
   # CognitoからLambdaを呼び出す権限を設定している
   CreateUserDataFunctionCognitoPermission:
     Type: AWS::Lambda::Permission
     Properties:
       Action: lambda:InvokeFunction
       FunctionName: !GetAtt CreateUserDataFunction.Arn
       Principal: cognito-idp.amazonaws.com
       SourceArn: !GetAtt CognitoUserPool.Arn


   # DynamoDBにユーザーデータを作成するLambda関数
   CreateUserDataFunction:
     Type: AWS::Serverless::Function
     Properties:
       CodeUri: cmd/create-user-data/
       Handler: bootstrap
       Runtime: provided.al2023
       Architectures:
         - x86_64
       MemorySize: 128
       Timeout: 30
       Environment:
         Variables:
           DYNAMODB_TABLE_NAME: !Ref UserDataTable
           LOG_LEVEL: INFO
       Policies:
         - DynamoDBCrudPolicy:
             TableName: !Ref UserDataTable

Lambda関数の実装例

// cmd/create-user-data/main.go

// ... (import文など)

// この関数がCognitoからの合図で実行される
func businessHandler(ctx context.Context, 
    cognitoEvent events.CognitoEventUserPoolsPostConfirmation,
    log *logger.Logger, 
    metricsClient *middleware.MetricsClient) (events.CognitoEventUserPoolsPostConfirmation, error) {

    // Cognitoからユーザーのメールアドレスや名前を取得
    userEmail := cognitoEvent.Request.UserAttributes["email"]
    userName := cognitoEvent.Request.UserAttributes["name"]

    // DynamoDBに保存するユーザーデータのひな形を作成
    userData := &services.UserData{
        UserEmail: userEmail,
        UserName:  userName,
        // サブスクリプションプランは、最初は'free'に設定
        Subscription: &services.SubscriptionInfo{
            Plan:      "free",
            Status:    "active",
        },
        // ... その他の初期設定
    }

    // DynamoDBにユーザーデータを保存
    err := userService.CreateUserData(ctx, userData)
    if err != nil {
        // もしエラーが起きても、ここで処理を止めるとユーザー登録自体が失敗してしまうため、
        // エラーを記録しつつ、Cognitoには処理を継続させる
        log.Error("Failed to create user data", ...)
        return cognitoEvent, err
    }

    return cognitoEvent, nil
}

運用上の工夫とベストプラクティス

データ整合性の保証

楽観的ロックとは? 複数人が同時に同じデータを更新しようとした際に、データが壊れるのを防ぐ仕組みの一つです。データに「バージョン番号」を持たせておき、更新する際に「自分が読み込んだ時のバージョン番号と同じか?」をチェックします。もし異なっていれば、誰かが先に更新したと判断し、自分の更新を中止します。これにより、意図しない上書きを防ぎます。

私たちは、この楽観的ロック(バージョン番号管理)や、DynamoDBの「条件付き書き込み」(指定した条件を満たす場合のみ書き込む機能)を活用して、データの整合性を厳密に保つようにしています。

CognitoとDynamoDBの役割分担

最終的に、私たちのシステムでは以下のように明確な役割分担をしています。

  • AWS Cognito(受付・警備員): 認証・認可のすべてを担当。ユーザーが誰であるかを証明するプロ。
  • DynamoDB(個人の机・キャビネット): アプリケーション固有のデータを担当。プロフィール、設定、課金情報など、ユーザーごとの詳細情報を管理するプロ。

まとめ:なぜDynamoDBが必要だったのか

Cognitoは認証の専門家ですが、アプリケーションのすべてのデータを管理するには機能不足でした。DynamoDBを組み合わせることで、以下の価値を実現できました。

  • 拡張性: アプリケーションの成長に合わせて、ユーザーに関する情報を柔軟に追加できる。
  • パフォーマンス: DynamoDBの高速なクエリ性能を活かせる。
  • ビジネス要件への対応: 課金プランの管理など、複雑な要件にも対応できる。

Cognitoのトリガー機能は、他のサービスと連携するための非常に強力な武器になります。

このアーキテクチャにより、シンプルでありながら拡張性の高い認証・ユーザー管理基盤を構築することができました。


実現された効果:

  • 🗄️ データ拡張性: Cognitoの属性制限を超える柔軟なデータ管理
  • 高性能: 平均156msでのユーザーデータ作成
  • 🛡️ 信頼性: 99.2%の成功率と自動復旧機能
  • 💰 コスト効率: ユーザーあたり$0.00196という非常に低い作成コスト