「読み込みのたびにDBが重くなって、オートスケーリングしてもボトルネックが解消されない」という経験はないだろうか。オンプレ時代に1台のRDBMSで読み書きを一元管理していた設計をそのままクラウドに持ち込むと、スケールの壁に当たりやすい。
CQRS(Command Query Responsibility Segregation:コマンドクエリ責任分離)は、このボトルネックを根本から解消するアーキテクチャパターンだ。「書き込み」と「読み込み」を別々のモデルとして独立させることで、それぞれを最適な技術スタックと負荷容量でスケールできるようになる。
この記事では、CQRSの概念から、AWSサービスを使った実装パターン、導入時の注意点まで、オンプレのRDB管理経験がある人にも伝わるよう順を追って解説する。
なぜオンプレのDB設計がクラウドでスケールしないのか
オンプレでは、OracleやSQL Serverなどのエンタープライズ向けRDBMSに、読み書きのすべてを集中させるのが一般的だった。増強が必要になれば、より強力なサーバーに乗り換える(スケールアップ)か、高価なクラスタリングライセンスを追加する方法が主流だった。
クラウドでも同じ発想でRDSを選ぶエンジニアは多い。しかし、現代のWebサービスではアクセスパターンが偏っている。商品カタログのような参照系は、受注処理のような更新系の10倍以上のリクエストになることがざらにある。1つのDB接続プールに読み書きが混在すると、重いクエリが書き込みをブロックし、インスタンスをいくら増強しても解消できないケースが起きる。
根本的な解決策は「読み込み専用のモデル」と「書き込み専用のモデル」を分離することだ。これがCQRSの出発点になる。
CQRSとは何か(基本概念)
CQRSはBertrand Meyerが提唱したCQS(Command-Query Separation)原則をアーキテクチャレベルまで拡張したパターンで、Greg YoungとUdi Dahanが体系化した。
中心にある考え方はシンプルだ。
・コマンド(Command): データを変更する操作(Insert・Update・Delete)。戻り値を返さず、副作用(状態の変更)に専念する。
・クエリ(Query): データを参照する操作(Select)。副作用を持たず、現在の状態を返すことに専念する。
この2つを同一のモデル・同一のDB・同一のAPIエンドポイントで処理するのをやめ、完全に別のパイプラインとして設計することがCQRSの本質だ。
| 側面 | コマンドサイド(Write) | クエリサイド(Read) |
|---|---|---|
| 目的 | データの変更・生成 | データの参照 |
| 重視すること | 整合性・トランザクション | パフォーマンス・スケール |
| AWSサービス例 | RDS(プライマリ)、DynamoDB | RDS(リードレプリカ)、ElastiCache、OpenSearch |
| スケール方式 | 垂直スケール(サイズアップ) | 水平スケール(レプリカ追加) |
AWSでのCQRS実装パターン3選
1. RDS プライマリ + Read Replica(最シンプルな出発点)
既存のRDSを使っているチームにとって移行コストが最も低いパターンだ。書き込みはプライマリインスタンスに送り、参照系はRead Replicaへ向ける。Amazon Auroraであれば最大15台のリードレプリカを追加でき、参照負荷が高まっても独立してスケールできる。
・向いているケース: RDB中心のバックエンドで、まずCQRSを試してみたいチーム
・注意点: プライマリとレプリカの同期に数秒の遅延が生じる。書き込み直後に参照する処理(例: 登録直後に一覧を返す)では結果整合性を前提とした設計が必要になる
2. DynamoDB + ElastiCache for Redis(高スループット向け)
書き込みはAmazon DynamoDBのテーブルへ、参照頻度の高いデータはAmazon ElastiCache for Redisにキャッシュする構成だ。DynamoDBは書き込みスループットを細かく制御でき、Redisはサブミリ秒の応答速度を持つ。商品情報や在庫数のような「頻繁に読まれるが更新は少ない」データに向いている。
・向いているケース: EC系・カタログ系のサービスで、参照が書き込みの数十倍以上ある場合
・注意点: キャッシュの無効化タイミングの設計が煩雑になりやすい
3. イベント駆動型(DynamoDB Streams + Lambda + OpenSearch)
最も本格的なCQRS実装のパターンだ。コマンドサイドはDynamoDBへの書き込みで完結させ、DynamoDB Streamsがその変更イベントを拾う。Lambdaがそれを処理してAmazon OpenSearch Serviceの読み込み専用インデックスに反映する。クエリサイドはOpenSearchへの検索・集計に徹するため、全文検索や複雑な集計が高速に行える。
・向いているケース: 受注管理・ログ分析・在庫管理など、書き込みと検索の要件が大きく異なるシステム
・注意点: 構成要素が増えるため初期設計のコストが高い。小さなシステムには過剰な場合がある
ステップバイステップで実装する(パターン3の例)
1. DynamoDBテーブルをコマンドサイドとして作成する
書き込み用のDynamoDBテーブルを東京リージョン(ap-northeast-1)に作成する。ポイントはDynamoDB Streamsを有効化しておくことだ。
# AWS CLI(コマンドサイドのDynamoDBテーブル作成) aws dynamodb create-table \ --table-name Orders \ --attribute-definitions AttributeName=orderId,AttributeType=S \ --key-schema AttributeName=orderId,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --stream-specification StreamEnabled=true,StreamViewType=NEW_IMAGE \ --region ap-northeast-1
2. Lambda関数でStreamをOpenSearchに同期する
DynamoDB Streamsのトリガーを設定し、Lambda関数がINSERT・MODIFY・REMOVEイベントを受け取ってOpenSearchインデックスに書き込む。処理の流れは以下のとおりだ。
# Python(Lambda関数のイメージ。認証情報は環境変数で管理すること) import boto3 from opensearchpy import OpenSearch def handler(event, context): for record in event['Records']: event_name = record['eventName'] # INSERT / MODIFY / REMOVE new_image = record['dynamodb'].get('NewImage', {}) if event_name in ['INSERT', 'MODIFY']: # OpenSearchのインデックスに登録・更新 doc = deserialize(new_image) os_client.index(index='orders', id=doc['orderId'], body=doc) elif event_name == 'REMOVE': order_id = record['dynamodb']['Keys']['orderId']['S'] os_client.delete(index='orders', id=order_id)
3. コマンドAPIとクエリAPIを分離する
APIレイヤーもコマンドとクエリで分ける。Amazon API GatewayまたはAWS Lambda URLでエンドポイントを2系統用意するのが基本だ。
・コマンドAPI: POST /orders → DynamoDBへ書き込み、201を返す(レスポンスボディは最小限)
・クエリAPI: GET /orders?status=pending → OpenSearchへ問い合わせ、検索結果を返す
この分離により、検索APIが高負荷になってもDynamoDBの書き込み性能に影響しない。
料金の仕組みとコスト設計のポイント
CQRSは構成要素が増えるため、料金設計を事前に把握しておくことが重要だ(以下は2026年3月時点の東京リージョンでの概算)。
・Amazon DynamoDB(オンデマンドモード): 書き込みリクエストユニット(WRU)が1Mリクエストあたり$1.4125、読み込みリクエストユニット(RRU)は$0.285
・Amazon ElastiCache for Redis: cache.t3.microで1インスタンスあたり月額$13~
・Amazon OpenSearch Service: t3.small.searchで1インスタンスあたり月額$25~。データ量とリクエスト数に応じて増加
・AWS Lambda(同期処理): 1Mリクエスト+128MB×1秒の場合、月数十円~数百円程度
コスト増を抑えるポイントは、「Read Replicaを増やすのとOpenSearchを立てるのでは、どちらが安いか」をデータ量とクエリ種別で比較することだ。全文検索や複雑な集計が不要であれば、RDS Read Replicaのほうがシンプルでコストも低い場合が多い。
応用・実務Tips
【Tips 1】イベントソーシングとの組み合わせ
CQRSをさらに発展させると、イベントソーシングと組み合わせることが多い。イベントソーシングではデータの現在状態を保存する代わりに「何が起きたか」というイベントのログを積み上げ、そこから現在状態を再生成する。DynamoDB StreamsをイベントログとしてS3やKinesisに保存することで、特定時点の状態への巻き戻しや監査証跡が実現できる。当サイトで解説しているサガパターンとも組み合わせやすい構成だ。
【Tips 2】結果整合性(Eventual Consistency)を前提に設計する
コマンドサイドへの書き込みが成功してから、クエリサイドに反映されるまでには数秒程度の遅延が生じる。「注文を登録した直後に一覧を取得したら、まだ反映されていなかった」というのは仕様の範囲内だ。フロントエンドでは「登録完了後は数秒後にリロードする」「書き込み後はUI上で差し込み表示する」といった設計で対処するのが現実的だ。
【Tips 3】GraphQL APIとの相性が良い
GraphQLの「Query」と「Mutation」という概念は、CQRSのクエリ・コマンドと自然に対応する。AWS AppSyncを使えば、MutationはDynamoDBに書き込み、QueryはDynamoDB(またはOpenSearch)から取得するという設計がスキーマ定義で宣言的に記述できる。REST APIより複雑なユースケースでも、CQRSの設計意図をAPIに反映しやすい。
よくあるトラブルと対処法
【トラブル1】同期遅延によるデータ不整合の検知遅れ
DynamoDB Streamsのバッチ処理が詰まると、コマンドサイドとクエリサイドの差が数分以上開く場合がある。Lambda関数のエラーハンドリングとDeadLetterQueue(DLQ)を必ず設定し、処理失敗時にアラートが上がる体制を作っておくことが重要だ。Amazon CloudWatchアラームでLambdaのエラー率と実行時間を監視することを忘れないようにしてほしい。
【トラブル2】小規模システムへの過剰適用
月間アクセスが数万PV程度の社内ツールや管理画面に、フルスタックのCQRSを導入してしまうと、構築・運用コストが収益に見合わない。RDS Read Replicaで十分なシステムに対してOpenSearchを立てるのは過剰投資だ。CQRSを導入する前に「読み書きの比率は何対何か」「クエリに全文検索や複雑な集計が必要か」を確認してから選択してほしい。
【トラブル3】コマンドモデルとクエリモデルの設計乖離
コマンドサイドはDBの正規化を優先し、クエリサイドは非正規化(読み込みに最適化したデータ構造)を使うことが多い。設計を担当するエンジニアが異なると、2つのモデルが独立して進化して整合が取れなくなる。初期設計の段階で「クエリサイドのデータ構造はどう生成するか」まで含めてドキュメント化しておくことをすすめる。
本記事のまとめ
CQRSは、読み書きを分離することでオンプレ時代の「1DBに集中させる設計」から脱却し、クラウドの水平スケーリングを最大限に活かすためのアーキテクチャパターンだ。
| 実装パターン | 向いているシステム | 主なAWSサービス |
|---|---|---|
| RDS + Read Replica | 既存RDB移行・中規模 | Amazon RDS / Aurora |
| DynamoDB + ElastiCache | 高スループット・カタログ系 | Amazon DynamoDB + ElastiCache for Redis |
| イベント駆動型 | 検索・集計要件が複雑 | DynamoDB Streams + AWS Lambda + Amazon OpenSearch |
導入の際は「システムの読み書き比率」と「クエリの複雑さ」を最初に評価し、過剰設計にならないパターンを選ぶことが実務では重要だ。クラウド上のLinuxサーバー構築(Amazon EC2)の基礎については、姉妹サイトLinuxMaster.JPでも詳しく解説している。
CQRSや分散アーキテクチャを現場で活かすには?
設計パターンは知っていても、実際の構成選定やコスト最適化で悩むことは多い。
オンプレの経験を活かしながら、現場で使えるクラウドスキルを体系的に身につけたい方へ、メルマガで実践的なクラウド活用ノウハウをお届けしています。
