Cognitoだけじゃ足りなかった!DynamoDBにユーザーデータを自動作成する仕組みと、その理由
背景
「認証システムなら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へのデータ作成」を連動させることができます。
データ作成の流れ
- ユーザーがメールアドレスの確認リンクをクリックします。
- Cognitoが「このユーザーは本物です」と確定します。
- Cognitoが「Post Confirmation」の合図を送り、私たちのLambda関数が起動します。
- 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という非常に低い作成コスト