Amazon Web Services ブログ
Amazon Finance Automationが重要な財務アプリケーションを動かすため、AWSの目的特化型データベースを使って業務データストアを構築した事例について
Amazon Finance Automation (FinAuto) は Amazon Finance Operations (FinOps) の技術組織です。この組織のミッションは、FinOps を活用してAmazon ビジネスの成長と拡大を支えることです。自動化とセルフサービス機能を活用することで業務効率を飛躍的に高めると同時に、支払いと回収を正確かつ遅滞なく行える体制を整えています。FinAuto は、FinOps 全体を俯瞰できる特別な立場にあり、この強みを活かすことで、正確性、一貫性、ガバナンスを備えたデータとサービスを提供し、多様なユースケースに対応したソリューションを展開しています。
本記事では、Amazon Finance Automation チームの取り組みをご紹介します。具体的には、 Amazon DynamoDB 、 Amazon OpenSearch Service や Amazon Neptune などのAWSの特化型データベースと AWS Lambda などのサーバーレスコンピューティングを組み合わせ、運用データストア (ODS) を構築しました。この ODS は財務取引データを保管し、ミリ秒単位の応答速度で FinOps アプリケーションをサポートしており、FinOps ビジネスにとって不可欠な基盤となっています。
(本記事は 2025/04/15 に投稿された How Amazon Finance Automation built an operational data store with AWS purpose built databases to power critical finance applications を翻訳した記事です。)
要件
Amazon では、様々なシステムから財務データが生成され、会計処理のためにそれぞれの補助元帳に保管されていますが、各システムで異なるデータモデルが使用されています。財務アナリストが効率的に業務を行えるよう、これらの補助元帳のデータを統一的に扱える共通のデータモデルが求められています。また、この統合データは、バックエンドおよびフロントエンドの業務アプリケーションから利用できるよう、API 経由でアクセスできる環境を整える必要があります。
データベースサービスを選定するにあたり、以下の主要要件に焦点を当てて検討を行いました:
- データベースサービスは、大量のトランザクションを処理できるよう、迅速なスケーリングが可能でなければなりません。上流システムでは 1 日に数億件もの財務取引イベントが生成されるため、そうした処理量に応じて適切にスケールできる能力が求められます。
- フロントエンドのユーザーアプリケーションを円滑に動作させるため、データ提供用 API はミリ秒レベルの高速なレスポンスを実現する必要があります。また、キーバリューベースでの検索にも対応している必要があります。具体的には、財務アナリストが口座番号を指定して、それに紐づくすべての取引データを瞬時に取得できるような機能が求められます。
- データベースには、各補助元帳が持つ独自のデータ属性に対応できるよう、柔軟なスキーマ構造が求められます。
- データストアは口座、請求書、クレジットノート、領収書といった財務関連エンティティを横断的に検索できる必要があります。例えば、財務アナリストが口座番号を知らなくても、口座名での検索が可能でなければなりません。
- データストアは、口座単位や数千の口座を含むビジネスチャネル単位など、様々な観点からデータを集計できる機能を備えている必要があります。たとえば、財務アナリストが特定の口座に関連する売掛金の総額を把握できるようにする必要があります。
- データベースにはアカウント間の関連性を保存できる機能が必要です。これは FinOps ならではの要件で、一人の顧客が複数の Amazon サービスをそれぞれ異なるアカウントで利用するケースに対応するためのものです。顧客への請求書を一本化し、利便性の高いサービスを提供するためには、アカウント間の関連性を正確に記録・管理できるデータベースが必要です。この関連性の情報を活用することで、複数のアカウントで発生した取引を適切にグループ化し、統合的な管理が可能となります。
ソリューション概要
多岐にわたる要件に対応するため、それぞれの要件に特化した AWS の専用データベースを複数組み合わせて利用する方針を定めました。
その中でも、主力となるデータストアとして Amazon DynamoDB を採用しました。採用の決め手となった特長は以下の通りです:
まず、キーバリュー方式での読み取りにより、高速なデータアクセスを実現できます。具体的には、特定口座の請求書や領収書の参照、口座名や状態の確認といった操作を迅速に行うことができます。また、Amazon DynamoDB は柔軟なスキーマ構造を持つため、項目ごとに異なる属性を設定できます。これにより、複数の売掛金システムそれぞれが持つ固有の属性データを適切に保存できます。私たちのシステムでは、データの読み取りを行うクライアントが地理的に分散しており、また書き込みワークロードは上流の請求システムの状況に左右されるため、負荷の予測が困難です。そこで、Amazon DynamoDB のオンデマンドキャパシティモードを採用しました。このモードでは、キャパシティの管理を自動化でき、実際の利用量に応じた課金となるため、効率的な運用が可能です。
データの集計に関しては、数千件に及ぶトランザクションを遅延や性能劣化なく集計する必要があるため、事前計算サービスを構築しました。このサービスは、Amazon DynamoDB Streams、AWS Lambda、Amazon Simple Queue Service(Amazon SQS)を組み合わせて実現しています。新しいイベントが発生すると自動的に起動し、非同期で集計処理を行います。このように事前に計算を行っておくことで、ユーザーが必要なときにミリ秒単位の応答速度でデータにアクセスすることが可能となります。
ユーザーからは、複数の属性を使って財務関連データを横断的に検索できる機能が求められていました。また、口座名の入力補完機能や、滞留債権残高が最も高い口座を上位から表示する機能なども必要とされていました。これにより、アナリストはそれらの口座に対する業務計画を立てることができます。これらの要件を満たすため、私たちは Amazon OpenSearch Service を採用しました。事前計算サービスで算出した滞留残高のデータを Amazon OpenSearch Service に保存することで、数千の口座にわたるデータの集計や並び替えを可能としました。
顧客の中には、一つの補助元帳で複数の口座を管理しているケースがあります。このような顧客からは、口座ごとに個別の連絡や請求書が届くのではなく、一元化された形での対応を望む声が寄せられていました。この要望に応えるため、財務アナリストが設定した口座間の関連性を保存し、必要に応じて検索できるシステムが必要でした。口座間の関係性は通常、親口座 “A” の下に子口座 “B” と “C” が属するような、階層構造として表現されます。Amazon Neptune は、高速で信頼性が高い、フルマネージド型のグラフデータベースで、相互に密接に関連したデータの保存と検索に最適化されています。Amazon Neptune は、私たちが作成しようとしている実際の関係性の構造を模倣したグラフ表現を生成します。このように関連データの保存とトラバーサル(データ構造の走査)に特化した機能を持つことから、私たちは Amazon Neptune を採用しました。
ソリューションアーキテクチャ
下図は、使用している各種 AWS サービスを含む、システム全体のアーキテクチャ概要を示しています。
注: 画像をクリックすると、新しいタブで拡大版を開くことができます。
複数の上流システムからのデータは、様々な方法で取り込まれ、変換された後に DynamoDB に保存されています。DynamoDB テーブルにはストリーム機能が有効化されており、複数の AWS Lambda 関数がこれらのストリームメッセージを処理して、Amazon SQS キューに送信します。これらのキューには適切なエラー処理のためのデッドレターキュー (DLQ) が設定されています。さらに別の Lambda 関数群が SQS キューからメッセージを読み取り、そのデータをAmazon OpenSearch Service のドメインにインデックス化します。
なお、この解決策は DynamoDB と OpenSearch Service 間のゼロ ETL 統合が導入される以前から本番環境で稼働していました。ゼロ ETL を採用することで、アーキテクチャと運用がより簡素化されたはずであり、可能な場合には将来的にこの統合方式への移行を検討しています。
また、アカウント間の関係性を特定・処理する独立したマイクロサービスが、それらの関係性を Amazon Neptune データベースに格納しています。ビジネスのユースケースに応じて、DynamoDB、OpenSearch Service、もしくは Neptune クラスターを通じて、API を介してデータが提供されています。
Amazon DynamoDB
上流のシステム間に接続性がないため、重複した ID が発生する可能性を防ぐために、異なるエンティティごとに新しい識別子を作成しました。この新しい ID は私たちのシステム内で一意であり、データ利用者が API を呼び出す際に使用する識別子となっています。システムでは UUID version 5 を採用しており、各トランザクションで利用可能な属性の組み合わせを用いて ID を生成しています。
以下の図は、私たちの DynamoDB ベーステーブルのデータモデルを示しています。
ソートキー属性を使用することで、KeyConditionExpression を活用し、特定の(トランザクション)ID に関連するアカウントや請求書で始まる値を検索するクエリを指定することができます。
API の要件によっては、(トランザクション)ID 以外の属性でデータを検索する必要があったため、グローバルセカンダリインデックス(GSI)を使用しています。GSI はデフォルトでスパースな特性を持っており、GSI に定義されたパーティションキーとソートキーが項目に存在しない場合、その項目は GSI には書き込まれません。このスパースインデックスは、テーブルのデータの一部のみを検索したい場合に特に有用です。上記の例では、marketplace 属性はアカウント項目にのみ存在します。’amzChannel’ を主キー、marketplace をソートキーとして GSI を作成することで、トランザクションの請求書項目を検索することなく、特定の amzChannel と marketplace に属するすべてのアカウントを迅速に検索することが可能になります。
以下の図は、私たちの DynamoDB テーブルに作成した GSI のサンプルを示しています:
また、いくつかの並び替えのユースケースにおいて、ローカルセカンダリインデックス(LSI)も活用しています。通常、1つのアカウントには数百件の決済済み請求書(一定期間にわたって作成・支払いが完了した請求書)と少数の未決済請求書が存在します。特定のアカウントの支払期限超過残高を計算する際には、そのアカウントの未決済請求書を確認する必要があります。特定のアカウントの未決済請求書のみを参照する場合、アカウントの請求書を検索し、DynamoDB の FilterExpression を使用して status=OP の請求書をフィルタリングすることも可能です。しかし、フィルター式はクエリ完了後、結果が返される前に適用されるため、同じ量の読み取りキャパシティを消費してしまいます。数千件の請求書を持つアカウントの場合、このようなクエリのレイテンシーは高くなってしまいます。LSI を使用することで、アプリケーションは異なるソートキーを選択できるようになりました。LSI には、ベーステーブルの属性の一部または大部分のコピーが含まれています。LSI 内のデータは、ベーステーブルと同じパーティションキーで整理されますが、異なるソートキーを使用します。invoiceStatus をソートキーとして LSI を作成することで、特定のアカウントの未決済請求書を迅速に検索できるようになりました。この結果、大量の決済済み請求書を持つアカウントにおいて、未決済請求書の検索レイテンシーが 10 倍改善されました。
LSI はテーブル作成時にのみ作成できるため、将来の並び替えユースケースに対応できるよう、sortKey1 や sortKeyN などのプレースホルダー属性を作成しました。
以下は、ベースとなる DynamoDB テーブルとその LSI における属性のサンプル表現です:
運用データストア(ODS)における財務データの取り込みと提供において、最も重要な要件の一つは、正確なデータ品質を保証することです。私たちのサービスは、外部顧客に送付される取引明細書(SOA)の作成に使用されるため、堅牢で信頼性の高い照合サービスが、データの正確性を保証する上で極めて重要となります。この照合ソリューションには、上流システムから提供される真正データ(信頼できる情報源)と、DynamoDB テーブルから変換され Amazon Simple Storage Service(Amazon S3)で利用可能となったデータが必要です。DynamoDB のネイティブエクスポート機能を使用して S3 にデータを転送することで、DynamoDB に対するフルテーブルスキャンを回避することができました。また、DynamoDB は増分エクスポートをサポートしているため、前回のエクスポート実行以降に変更されたデータのみをエクスポートするよう設定しました。これにより、テーブルの読み取りキャパシティや可用性に影響を与えることなくデータを転送できています。その後、Amazon EMR を使用して Spark アプリケーションを実行し、DynamoDB から S3 にエクスポートされたデータと、上流のデータストア(信頼できる情報源)からのエクスポートデータとの照合を行っています。DynamoDB からエクスポートされたデータは、照合システムへの入力としての役割だけでなく、他の下流のデータチームが財務報告を作成する際にも活用されています。
以下の図は、私たちの照合サービスの高レベルアーキテクチャを示しています:
Amazon OpenSearch Service
入力途中での検索(サジェスト検索)や集計要件に対しては、Amazon OpenSearch Service を使用しています。DynamoDB の取引データはさらに情報が追加され、Amazon OpenSearch Service に取り込まれます。DynamoDB は AWS Lambda とネイティブに連携しており、DynamoDB ストリームのイベントに応答するトリガーとして Lambda を利用できます。この Lambda 関数がストリームイベントを処理し、エンティティ固有の SQS キューにメッセージを送信します。別の Lambda 関数が SQS のメッセージを処理し、追加情報でデータを拡充した上で、OpenSearch にインデックス化します。要件の一つとして、財務アナリストに割り当てられた全アカウントの売掛金の経過期間別残高を特定する必要があります。これにより、アナリストは担当アカウントの優先順位付けが可能になります。アナリストへのアカウント割り当ては別のデータセットとして管理されており、取引データとは別に保存されています。このデータを照会し、拡充したデータを OpenSearch Service に取り込むことで、クライアントのユースケースに応じた集計クエリをミリ秒単位のレイテンシーで実行することが可能になりました。
入力途中での検索に関して、OpenSearch Service はクエリ時処理とインデックス時処理のオプションを提供しています。クエリ時処理では、クエリ実行時に保存されたフィールドのプレフィックスを検索できるフィルターを指定します。データは通常通り OpenSearch に保存されますが、クエリはプレフィックスを検索します。これは、OpenSearch が既にインデックス化されたデータのプレフィックスをその場で計算して検索を実行するため、負荷の高い処理となります。これは「文字列を含む」という種類の処理に近いものです。一方、インデックス時処理では、インデックス作成時にカスタムトークナイザーを使用して単語をより小さな部分に分割することができます。私たちはデータのインデックス作成時に edge_ngram トークナイザーを使用しました。以下のスクリーンショットは、アカウント名「Amazon」がインデックス時に複数のトークンに分割される例を示しています。これにより、入力途中での検索における検索結果の高速化を実現しました。
Amazon Neptune
前述の通り、顧客アカウント間の関係性を追跡する要件があります。一人の顧客が補助元帳内に複数のアカウントを持つことがあり、顧客からは全アカウントを統合した請求明細書の発行を求められています。また、財務アナリストは顧客の全アカウントにわたる統合された経過期間別残高を確認する必要があります。このため、アカウント間の関係性を特定し、保持する必要が生じています。各事業部門は、顧客の関連アカウントを特定するための独自のルールと属性を定義しています。私たちのシステムは、これらの定義されたルールに基づいてデータ属性を取り込み、また関連アカウントの特定と維持のために、これらの属性の変更も検知しています。グラフデータベースである Amazon Neptune を使用することで、関係性を自然な形でモデル化し、ユースケースに応じた柔軟な走査クエリを実行することが可能になりました。
代替案として、DynamoDB を使用して同様の機能を実現する方法も検討しました。DynamoDB では隣接関係リスト設計モデルを使用して関係性を保存することが可能で、ノードに親ノードへの参照やそのノードに関連するエッジを含めることができます。この方法は実現可能ではありますが、データを取得するためのクエリには関係性を辿るための繰り返しの呼び出しが必要となり、ネストされた関係性や分岐する関係性に対してはパフォーマンスが低下してしまいます。一方、Neptune ではデータベース内で走査が行われるため、1回のリクエストで関係性を取得することができます。このため、関係性の保存には Amazon Neptune を採用しました。
初期のユースケースでは、複数の個別アカウントをグループとして集約して作業する機能が必要でした。以下の図は、ノードとエッジの構造を示しています。ノードから HasParent タイプのエッジを走査することで、関連するアカウントを迅速に取得できます。クエリはグループレベルから開始して入力エッジを辿るか、あるいはアカウントレベルから開始して出力エッジを辿り、見つかったグループから入力エッジに戻ることができます。
// If starting from an account to find all related accounts, example account 1
g.V(1)
// Follow inbound/outbound edges of type "HasParent" deduping to avoid repetition
.repeat(both("HasParent").dedupe())
// Fetch nodes that are of type "Account"
.emit(hasLabel("Account"))
> [
{ id: 1, label: "Account" },
{ id: 2, label: "Account" },
{ id: 3, label: "Account" }
]
より複雑なユースケースに対応するため、新しいマクログループ化を提供できるようスキーマを拡張しています。既存のデータに新しい関係性タイプとメタデータを定義する新しいノード/エッジタイプを追加することができます。これにより、既存のクエリに軽微な修正を加えるだけで、Neptune にシステム内の関連アカウントを見つけるための新しい関係性セットの走査を指示することができます。これを繰り返すことで、同じデータに対して異なるグループ化の視点を提供することが可能です。さらに、ノードに設定されたプロパティによって結果をフィルタリングすることで、返される項目をより詳細に絞り込むことができます。
以下の図は、2 つのビジネスグループが同じ組織に関連付けられている複雑なグループ化の例を示しています。
// If starting from an account to find all related accounts, example account 1
g.V(1)
// Follow inbound/outbound edges of type "HasParent" and "SubsidiaryOf", deduping to avoid repitition
.repeat(both("HasParent", "SubsidiaryOf").dedupe())
// Fetch nodes that have are of type "Account"
.emit(hasLabel("Account"))
> [
{ id: 1, label: "Account" },
{ id: 2, label: "Account" },
{ id: 3, label: "Account" },
{ id: 4, label: "Account" }
]
アカウントが一方の顧客から別の顧客に移動するケースが発生することがあります。その場合、該当するアカウントを元の顧客のグループから切り離し、新しい顧客のグループに移動する必要が生じます。
以下の図は、組織 A のビジネスグループ 1 に属していたアカウント 1 が、新しいグループであるビジネスグループ 3 に移動する例を示しています。
結論として、Amazon Neptune を採用することで、独自のグラフデータベース管理システムを運用する複雑さを回避しながら、アカウント関係性サービスを構築することができました。このモデルにより、アカウント間の関係性を簡単に走査でき、関連アカウントを特定するというユースケースを解決し、ユーザーにより良い体験を提供することができています。グラフは毎日更新され、関係性の変更を特定しています。関係性の取得において、p90( 90 パーセンタイル値)で 250 ミリ秒未満の応答時間を実現しています。
運用の優位性について
Amazon の財務データを扱う上で、100 %のデータ品質を保証する必要があります。複数のコンポーネントを含む分散システムにおいては、信頼性と正確性の維持が重要です。そのため、私たちはフォールトトレランス(障害耐性)を基本原則としてシステムを設計しています。例えば、各 SQS キューには、処理に失敗したメッセージを捕捉するための対応するデッドレターキュー(DLQ)が設定されており、Amazon CloudWatch によるモニタリングとアラームが構成されています。さらに、API における 5XX エラーとレイテンシーメトリクスのモニタリングには、CloudWatch API Gateway(APIG)メトリクスを活用しています。
また、ビジネスユースケースに応じたカスタムメトリクスを CloudWatch に公開するため、AWS SDK for CloudWatch を使用しています。例えば、ソースシステムでのトランザクションイベント生成から、最終的に運用データストアに取り込まれるまでの所要時間を計算するため、取り込みイベントのレイテンシーメトリクスを公開しています。
まとめ
本記事では、Finance Automation チームが財務部門特有の要件を満たすため、AWS 専用データベース(DynamoDB、OpenSearch Service、Neptune)を活用して、拡張性と信頼性を備えたイベント駆動型の運用データストレージを構築した事例をご紹介しました。
DynamoDB が標準で提供する DynamoDB Streams 、グローバルセカンダリインデックス、ローカルセカンダリインデックス、S3 へのエクスポート機能、柔軟なスキーマ構造といった機能により、開発工数を大幅に削減することができました。
また、Amazon OpenSearch Service の活用により、API のレスポンス時間を維持しながら、検索機能と集計機能の要件を満たすことができました。さらに、Amazon Neptune の採用によって、口座間の関連性の保存、探索、取得処理を簡素化することができました。
加えて、AWS Lambda というサーバーレスコンピューティングを DynamoDB Streams および Amazon SQS と組み合わせることで、システム全体をシンプルな構成としました。SQS の活用により、高い耐障害性を維持しながら、データストア間の疎結合化を実現することができました。
AWS 専用データベースについて詳しく知りたい方は、Purpose-built databases のページをご覧ください。