Findy Toolsのデータ基盤を1ヶ月前倒しで新規構築した話

はじめに

この記事はFindy Advent Calendar 2024 21日目の記事です。

adventar.org

データソリューションチーム、エンジニアの土屋(@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のデータ基盤

データの取得、加工の仕組み

データの取得

はじめに、ユーザーの行動ログの転送方法を説明します。Findy Toolsでは、ユーザーの行動ログを次のように取得しています。

  1. Next.js(フロントエンド)からRails API(バックエンド)にHttp Requestを送信
  2. 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を採用しました。

今回の構築では次のような手順でデータを送信します。

  1. CloudWatch Eventsが設定した時刻にECSを起動
  2. ECSがEmbulkを起動
  3. EmbulkがAurora MySQLからデータを取得
  4. EmbulkがBigQueryにデータを送信

データの加工

データの加工にはdbtを採用しました。dbtは、SQLを使ってデータを加工するため、データエンジニアがSQLを書くことができれば、簡単にデータを加工できます。似たツールで非エンジニアでも触りやすいGUIを持つDataformがありますが、今回はGit操作になれているメンバーが触ることを考慮してdbtを採用しました。

dbtの実行はGitHub Actions上で行っています。具体的な手順は次の通りです。

  1. GitHub ActionsのScheduleによってワークフローが発火
  2. dbtがlake層に入ったデータを加工

エラー処理、アラートの仕組み

今回作成したインフラストラクチャでは、次のようにエラー・アラートを通知しています。

Google Cloud, AWSのエラー通知の仕組み

行動ログの転送のエラー処理

行動ログの転送では、エラーの発生箇所がいくつか考えられます。

  • Next.jsからRails APIへのHttp Requestの失敗
  • Rails APIのログ出力の失敗
  • fluentbitの停止・暴走によるログ転送の失敗
  • BigQueryへの送信の失敗

今回データソリューションチームでは、BigQueryへの送信の失敗の監視を担当することになりました。(他はウェブアプリケーションの開発チームが担当)BigQueryへの送信の失敗は、Cloud Loggingに出力されるBigQuery APIの応答を監視することで実現できます。

  1. Cloud LoggingにBigQuery APIのログが出力される
  2. Cloud Monitoringでエラーのログを検知する
  3. Slackに通知を送る

データベースの転送のエラー処理

データベースの転送では、エラーの発生箇所がいくつか考えられます。

  • Aurora MySQLからデータを取得する際のエラー
  • BigQueryにデータを送信する際のエラー

これらのエラーが発生した場合、AWS ECSのログにエラーが出力されます。そのため、CloudWatch Logsに出力されるログを監視することでエラーを検知し、Slackに通知を送ることができます。

具体的には、次の手順でアラートを設定しました。

  1. CloudWatch Logsに出力されたログをCloud Watch Metrics Filterでエラー検出
  2. CloudWatch Alarmでアラートを発火
  3. SNS・Chatbot経由でSlackに通知

この手法の場合、全ての実装をTerraformで管理できるため、運用が楽です。他の手法として、CloudWatch Logsに出力されたログをLambdaでSNSに送信するという方法もあります。しかし、この方法は、Lambdaのソースコードの管理(GoやPythonなど)が必要になるため、運用が複雑になります。よって、今回はTerraformで管理する方法を採用しました。

インフラストラクチャの管理

Terraformによるリソース管理の仕組み

Findy Toolsのデータ基盤ではTerraformを用いてリソースを管理しています。 各環境は、environmentsディレクトリに分割しています。Findy Toolsのデータ基盤では、productionstagingの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 envPyEnv + PoetryDockerなどいくつか選択肢があります。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の並列化などの高速化をする予定です。

今後は作成したデータ基盤で、データの利活用を進めていく所存です。

今回の取り組みが、他のエンジニアにとっても何かしらのヒントとなれば幸いです。


ファインディでは一緒に会社を盛り上げてくれるメンバーを募集中です。興味を持っていただいた方はこちらのページからご応募お願いします。

herp.careers


  1. 調査により、Embulkの起動時間が原因であると判明しています。具体的には、JVMとJVM上のRubyランタイムの初期化の時間でした。なお、Embulk本体はJavaではありますが、プラグインはRubyで記述可能です。