はじめに
この記事はFindy Advent Calendar 2024 21日目の記事です。
データソリューションチーム、エンジニアの土屋(@shunsock)です。本日は、Findy Toolsのデータ基盤を構築したので、その内容を共有します。
Findy Toolsは、2024年1月23日にリリースされた開発ツールのレビューサイトです。利用者は開発者向けツールのレビューやアーキテクチャの記事を閲覧、投稿できます。
Findy Toolsのデータ基盤のシステム設計の紹介
システム設計の目標、要件
今回の構築では、「Findy Toolsを訪れたユーザーの行動ログとデモグラフィックデータを組み合わせて分析可能にする」という目標がありました。Findy Toolsではユーザーの行動ログにユーザーidやリファラーを保存しています。また、ユーザー情報をはじめとしたデータを保存するデータベースがあります。今回は、これらを繋ぎ合わせた分析を可能にすることが目標です。
この目標を分解していきます。
まず、分析可能にするためには、分析対象であるデータが保存されている必要があります。行動ログとデモグラフィックデータは別々の場所にあるので、それぞれを集める必要があります。
また、データ分析をする主体はビジネスサイドの方で、BIを通じてデータを閲覧します。SQLのロジックついてはデータエンジニアが管理します。従って、データの加工が必要です。さらに、日々データは更新されます。よって加工されたデータも更新する必要があります。
以上をまとめると次のシステムが必要であることがわかります。
- 分析対象のデータを一定期間で取得する処理
- 取得したデータを一定期間で加工する処理
システムには既存のリソースなどの前提や組織方針などの制約があります。ファインディの場合、ウェブアプリケーションがAWS、データ基盤・機械学習基盤をGoogle Cloudでそれぞれ稼働しています。今回扱う行動ログデータとデモグラフィックデータはそれぞれ、ECSとRDS(Aurora MySQL)にありました。また、データウェアハウスはBigQueryであることが必須要件でした。これは、社内の分析担当者がBigQueryのUI/UXに慣れていることに起因します。最後に同期の期間ですが、即時性を求められるデータは今のところないため、1日ごとのバッチ処理でデータを転送することにしました。
以上の前提と制約からシステムの要件が次であるとわかります。
- データの転送処理
- 行動ログの転送: ECSのコンテナに出力されている行動ログをBigQueryに1日ごと転送 ※
- データベースの転送: RDSのデモグラフィックデータをBigQueryを1日ごとに転送
- データの整形
- BigQueryに保存されたデータを1日ごとに加工処理を実行
※ 実際には、これより短い間隔で送信されるシステムになりました。
システム設計図
次の写真がFindy Toolsのデータ基盤のシステム設計図です。
データの取得、加工の仕組み
データの取得
はじめに、ユーザーの行動ログの転送方法を説明します。Findy Toolsでは、ユーザーの行動ログを次のように取得しています。
- Next.js(フロントエンド)からRails API(バックエンド)にHttp Requestを送信
- Rails APIがログを出力
よって、このログをBigQueryに転送する手法を検討しました。工数がかからないことと送信に必要な追加費用がBigQuery APIのみであることからfluentbitを採用しました。fluentbitは、ログを収集し、閾値やバッファの条件に応じて送信するソフトウェアです。
次に、データベースの転送方法を説明します。Findy Toolsの場合、データベースとしてAWS Aurora MySQLを採用しています。
Aurora MySQLからBigQueryに送信する手法はいくつかありました。
方法 | 構築難易度 | コスト | メリット | 採用判断 | 理由 |
---|---|---|---|---|---|
Private Linkを使う | 高 | 中 | セキュリティ強化、直接送信可能 | 不採用 | 今回のケースでは受けられる恩恵が小さい。 |
Private SubnetにECSを立てる | 中 | 低 | シンプル構成、コスト効率が良い | 採用 | 実現可能性が高く、コスト効率が良い。 |
3rd Party ETLツールを使う | 低 | 高 | 導入が楽 | 不採用 | 費用が高い。 |
Private Linkを使う方法は構築難易度が高いものの、今回のケースでは受けられる恩恵が小さいことから採用しませんでした。また、3rd PartyのETLツールを使う方法は相当な費用が必要と判明し、断念しました。よって、Private SubnetにECSを立てて送信する方法を採用しました。
ECS上でデータの転送するソフトウェアは社内で実績のあったEmbulkを採用しました。
今回の構築では次のような手順でデータを送信します。
- CloudWatch Eventsが設定した時刻にECSを起動
- ECSがEmbulkを起動
- EmbulkがAurora MySQLからデータを取得
- EmbulkがBigQueryにデータを送信
データの加工
データの加工にはdbtを採用しました。dbtは、SQLを使ってデータを加工するため、データエンジニアがSQLを書くことができれば、簡単にデータを加工できます。似たツールで非エンジニアでも触りやすいGUIを持つDataformがありますが、今回はGit操作になれているメンバーが触ることを考慮してdbtを採用しました。
dbtの実行はGitHub Actions上で行っています。具体的な手順は次の通りです。
- GitHub ActionsのScheduleによってワークフローが発火
- dbtがlake層に入ったデータを加工
エラー処理、アラートの仕組み
今回作成したインフラストラクチャでは、次のようにエラー・アラートを通知しています。
行動ログの転送のエラー処理
行動ログの転送では、エラーの発生箇所がいくつか考えられます。
- Next.jsからRails APIへのHttp Requestの失敗
- Rails APIのログ出力の失敗
- fluentbitの停止・暴走によるログ転送の失敗
- BigQueryへの送信の失敗
今回データソリューションチームでは、BigQueryへの送信の失敗の監視を担当することになりました。(他はウェブアプリケーションの開発チームが担当)BigQueryへの送信の失敗は、Cloud Loggingに出力されるBigQuery APIの応答を監視することで実現できます。
- Cloud LoggingにBigQuery APIのログが出力される
- Cloud Monitoringでエラーのログを検知する
- Slackに通知を送る
データベースの転送のエラー処理
データベースの転送では、エラーの発生箇所がいくつか考えられます。
- Aurora MySQLからデータを取得する際のエラー
- BigQueryにデータを送信する際のエラー
これらのエラーが発生した場合、AWS ECSのログにエラーが出力されます。そのため、CloudWatch Logsに出力されるログを監視することでエラーを検知し、Slackに通知を送ることができます。
具体的には、次の手順でアラートを設定しました。
- CloudWatch Logsに出力されたログをCloud Watch Metrics Filterでエラー検出
- CloudWatch Alarmでアラートを発火
- SNS・Chatbot経由でSlackに通知
この手法の場合、全ての実装をTerraformで管理できるため、運用が楽です。他の手法として、CloudWatch Logsに出力されたログをLambdaでSNSに送信するという方法もあります。しかし、この方法は、Lambdaのソースコードの管理(GoやPythonなど)が必要になるため、運用が複雑になります。よって、今回はTerraformで管理する方法を採用しました。
インフラストラクチャの管理
Terraformによるリソース管理の仕組み
Findy Toolsのデータ基盤ではTerraformを用いてリソースを管理しています。
各環境は、environments
ディレクトリに分割しています。Findy Toolsのデータ基盤では、production
とstaging
の2つの環境を管理しています。
Terraformのワークスペースは、機能ごとに分割しています。例えば、Embulkのバッチ処理に関連するリソースは次のように管理しています。
. ├── environments │ ├── production │ │ └── embulk │ └── staging │ └── embulk └── modules └── embulk
このように管理することで、機能内の変更(ここではembulk
ワークスペース)が他の機能(他のワークスペース)に直接影響を与えないため、運用が安定します。つまり、機能の変更や破棄が容易になります。
一方で、共通のリソースが必要になることがあります。その場合は、プロバイダのモジュールを作成して、共通のリソースを管理します。例えば、Google Cloudのリソースは次のように管理しています。
. ├── environments │ ├── production │ │ ├── embulk │ │ └── google │ └── staging │ ├── embulk │ └── google └── modules ├── embulk └── google
ここで作成したリソースは複数のワークスペースで共有できるため、運用には注意が必要です。
uvによるPythonパッケージ管理の仕組み
今回の構築では、データ加工ツールとしてdbt(Data Build Tool)を採用しました。dbtはETLツールのTransformationの部分を担当します。
dbt-coreはPythonで書かれており、周辺ツールもPythonのエコシステムで提供されています。そのため、Pythonのパッケージ管理ツールの選定が必要でした。
今回は、Pythonのパッケージ管理ツールであるuvを採用しました。Pythonのパッケージ管理をする方法はvirtual env
やPyEnv + Poetry
、Docker
などいくつか選択肢があります。uvを利用するとPythonのインタープリタとパッケージを1つのツールで管理できるので、環境構築が非常に楽です。
前述したものの中だとDocker
もインタープリタとパッケージを1つのツールで管理可能です。今回は、Dockerfile
のメンテナンスが必要であること、やりたいことがシンプルであることから、採用を見送りました。
$ # たったこれだけで環境構築ができる🎉 $ curl -LsSf https://astral.sh/uv/0.5.7/install.sh | sh $ uv python install 3.12.6 $ uv python pin 3.12.6 $ uv sync
開発時に発生した課題と解決法
EmbulkのDockerをローカルで動かすことが難しい
EmbulkのリポジトリではDocker Containerを管理しています。開発当初、AWSのPrivate Subnetと通信する必要がありローカルで動かすことが難しいという課題がありました。
すぐに思いつく対策として、Docker Composeを使ってDBのMockを立ち上げる手が考えられます。しかし、今回はあえてローカル環境を作らない選択をしました。ローカル環境を作らないことにより、その分の開発工数を削減できます。
前述の選択により基本的にステージング環境で管理することになりました。ここで課題になるのが、ステージング環境のECRやデプロイやECSの実行に手間がかかることです。そこでコマンドを作成し、デプロイと実行に手間がかからないようにしました。
ステージング環境へのデプロイは次のコマンドで実行します。
$ ./command/push_image_to_ecr staging [NOTICE] 🚀 ステージング環境にDockerイメージをpushします [NOTICE] 🚪 ECRとDockerにログインします Login Succeeded [NOTICE] ✅ ECR にログインしました [NOTICE] 🛠 Dockerイメージを作成します [+] Building 1.4s (19/19) FINISHED ... [NOTICE] ✅ Dockerイメージの作成が完了しました [NOTICE] 🏷 イメージにタグを付与します [NOTICE] ✅ イメージにタグの付与が完了しました [NOTICE] 🚀 ECRにDockerイメージをpushします ... [NOTICE] ✅ ECR へのDockerイメージのpushが完了しました [NOTICE] 🎉 DockerイメージのビルドとECRへのpushが完了しました
ステージング環境での実行は次のコマンドで実行します。
$ ./command/run_embulk_on_ecs [NOTICE] 🚀 EmbulkのECSタスクを起動します { "tasks": [ ... ] } [NOTICE] ✅ EmbulkのECSタスクの起動に成功しました [NOTICE] 🎉 EmbulkのECSタスクの起動が完了しました
また、Embulkで新しいテーブルを処理するためにはliquid.yml
ファイルを新たに作成する必要があります。こちらも手間を省くために独自コマンドを作成しています。
$ ./command/generate_conf new_table [NOTICE] 🔍 テンプレートファイルと出力先ディレクトリの存在確認 [NOTICE] ✅ テンプレートファイルと出力先ディレクトリの存在確認が完了しました [NOTICE] 🚀 Embulkの設定ファイルを生成しています: new_table [NOTICE] ✅ Embulkの設定ファイルの生成が完了しました: new_table [NOTICE] 🎉 Embulkの設定ファイルを生成しました。ファイル名: new_table.liquid.yml
本番の手動デプロイも時間がかかります。Embulkのリポジトリではデプロイを自動化しています。
main
branchにPull Requestを作成した時点で、stgへのデプロイmain
branchにMergeした時点で、本番環境にデプロイ
uvとdbtを組み合わせるとコマンドが長く入力の負担が大きい
uv
は非常に便利です。しかし、dbt
を組み合わせて使うとコマンドが長くなる問題がありました。
# コマンドの全体像 # 実際の開発の時は、modelを指定するので、これよりも長くなります $ uv run dbt debug --profile *** --target dev --project-dir ./tools
そこで、Shell Script, Taskfileを使ってラッパーを作成し、コマンドを短くしました。
# コマンドの全体像 # 実際の開発の時は、modelを指定するので、これよりも長くなります $ uv run dbt debug --profile *** --target dev --project-dir ./tools # Shell Scriptとして実行 # 使い慣れている方が多い、Shell Scriptでラッパーを作成 # 少し短くなります🙌 $ bash scripts/dbt_wrapper.sh debug # Go Taskで実行 # 元のコマンドに比べ、半分以下にすることができました🎉 $ task dbt -- debug
開発を振り返って
今回の取り組みでは、持続的開発が可能なデータ基盤とその開発環境を作成できました。また、当初の見積もりよりも一ヶ月早く実装できました。
今回のデータ基盤構築プロジェクトを振り返って、プロジェクト成功の鍵がいくつかあったと考えています。
実績のあるツールの活用による開発の効率化
構築にあたって、既存の知見が多いツールや実績のある技術を積極的に採用しました。それぞれが成熟したエコシステムを持っているため、課題が発生した際に素早く調査・解決できたのが大きな利点でした。
社内エンジニアとの連携
データ基盤構築は多くの技術やチームが関わるため、横のつながりを活用することが非常に重要でした。ファインディは横のつながりが強く、相談しやすい環境です。初見の技術で課題に直面した際、SREチームやデータソリューションチームのメンバーに相談することで、迅速に解決できました。特にTerraformやAWSの運用においては、社内のエンジニアの知見を活用したことで、手戻りや長時間の調査を防ぐことができました。
ラッパーや便利ツールの作成
ラッパーやコマンドの短縮化により、日常的な作業の効率が大幅に向上しました。日常の作業効率を上げた分、アーキテクチャの議論や意思決定に時間を割くことができたので、これらのツールに投資した時間は、長期的に見ると価値のあるものでした。
学びと次への展望
データ基盤のような横断的なプロジェクトでは、他チームとの連携やツールの選定が効率性に寄与すると学びました。また、技術としては、AWSの通知システムの構築手法やEmbulkの実用上の課題を認知したことが学びでした。特に、Embulkの起動時間が長いことは想定外でした。1 今後、転送するテーブル数が増えた場合は、ECSの並列化などの高速化をする予定です。
今後は作成したデータ基盤で、データの利活用を進めていく所存です。
今回の取り組みが、他のエンジニアにとっても何かしらのヒントとなれば幸いです。
ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。
- 調査により、Embulkの起動時間が原因であると判明しています。具体的には、JVMとJVM上のRubyランタイムの初期化の時間でした。なお、Embulk本体はJavaではありますが、プラグインはRubyで記述可能です。↩