こんにちは。TVer でデータ分析をしている高橋です。
こちらは TVer Advent Calendar 2024 の3日目の記事です。
2日目の記事は @takanamito さんの connect-goでHTTP GETリクエストを受け取る でした。
この記事では分析環境を効率化するために弊社で活用している、Terraform と GitHub を使ったデータマート基盤をご紹介します。
開発のきっかけ
これまで分析業務は、データレイクに集約された生ログを都度前処理し、個別の集計作業を行っていました。クエリ作成のたびに手作業でロジックを組み立てるか過去のクエリからロジックをコピペするような状況で、あまり作業効率が良いとは言えません。
時々前処理が刷新されることがあり、全員で過去のクエリを全て修正するというマンパワーに頼った運用をしていました。
またスケジュールクエリの管理もコンソール上での手作業が中心で、更新作業はコンフルやスクショに証跡を残しながら行っており何とも手間がかかる状態でした。
......
そのような状況の中、昨年からダッシュボード構築などのニーズが高まり、コスト観点からデータマート開発が求められました。そこでコスト観点とともに前述の作業効率化と品質向上を目指し、汎用的なデータマート基盤を構築することにしました。
サンプルのリポジトリはこちらです。
基盤の構成と運用
以下はディレクトリ構成です。
├─.github │ └─workflows ├─scheduled_query │ └─dataset.sample_mart ├─scripts ├─table_valued_function ├─terraform │ ├─environments │ │ ├─devintegrate │ │ ├─production │ │ └─staging │ └─modules ├─user_defined_function └─view
スケジュールクエリとテーブルを作成する流れを例に、具体的な手順を以下に示します。
- ディレクトリ作成
scheduled_query
配下にリソースごとのディレクトリを作成(例:dataset.sample_mart
)。 - 必要ファイルの作成
query.sql
: 定期実行するクエリsettings.json
: スケジュールクエリやテーブルの設定schema.json
: テーブルスキーマ(テーブルを作成する場合のみ)
- Terraform コマンド実行
cd terraform terraform init -reconfigure -backend-config=./environments/devintegrate/dev.tfbackend terraform apply -var-file ./environments/devintegrate/terraform.tfvars
工夫したこと
Terraform の知識不要で簡単リソース作成
query.sql
、settings.json
、schema.json
の3つのファイルを用意するだけでリソース作成が可能です。Terraform の Data Sourceを活用し、ディレクトリ配下の設定情報を動的に読み込む仕組みを導入してこれを実現しています。
以下は、settings.json
を収集するスクリプトの例です。
BASE_PATH=$1 declare -A queries for settings_file in $(find "$BASE_PATH" -name 'settings.json'); do key=$(basename "$(dirname "$settings_file")") content=$(jq 'if has("dataset_id") then .dataset_id |= if type == "object" then . else {prd: ., stg: ., dev: .} end else . end | @json' "$settings_file") queries["$key"]=$content done result="{" for key in "${!queries[@]}"; do result+="\"$key\": ${queries[$key]}," done result="${result%,}}" echo $result
Terraform でスクリプトを実行し、リソース情報をローカル変数として扱えるようにしています。
data "external" "scheduled_query_config" { program = ["bash", "${path.module}/../scripts/list_queries.sh", "${path.module}/../scheduled_query"] } locals { scheduled_queries = { for k, v in data.external.scheduled_query_config.result : k => jsondecode(v) } }
環境ごとの設定管理を簡略化
environments
ディレクトリに各環境の設定をまとめ、共通するリソースは再利用可能にしています。これにより、開発・検証・本番といった複数環境を扱う場合でも効率よくメンテナンスを行うことが可能になっています。
例: devintegrate
環境の設定ファイル
# dev.tfbackend bucket = "sample-datamart-bucket-dev" prefix = "sample-datamart" # terraform.tfvars environment = "dev" project = "project-dev" service_account_email = "datamart-creator@project-dev.iam.gserviceaccount.com"
GitHub Actions を活用した CI/CD の自動化
GitHub Actions を利用して以下のような作業を自動化しています。
terraform plan
の結果を出力
プルリクエストを作成すると、GitHub Actions がterraform plan
の実行と対応する Actions のリンクをプルリクエストに投稿します。これにより差分確認が容易になりレビューがスムーズになります。main
ブランチへのマージ後、自動で環境ごとのブランチへプルリクエスト作成
開発ブランチからmain
ブランチに変更がマージされると、GitHub Actions が自動で各環境に対応するブランチ(例:devintegrate
,staging
,production
)へのプルリクエストを作成します。- 各環境へのプルリクエストマージ時に
terraform apply
を自動実行
環境ごとのブランチへのプルリクエストがマージされると、GitHub Actions がterraform apply
を実行して該当環境に変更を反映します。これにより簡単にデプロイ作業を行うことができます。
また、既存リソースを Terraform 管理したい場合もあります。これは import ブロックを使って取り込みを自動化しました。
各環境の terraform.tfvars
にインポートするリソース名とidを記述し、main.tf
の import
ブロックで呼び出します。
ちなみに import
ブロック内での for_each は Terraform 1.7 以降で利用可能です。
# terraform/environments/production/terraform.tfvars ~~~ import_resources = { "table": [ { "resource_name": "dataset.sample_mart", "id": "projects/{project}/datasets/{dataset_id}/tables/{table_id}" } ], "view": [ {} ], "scheduled_query": [ {} ], "tvf": [ {} ] } # terraform/main.tf import { for_each = { for v in var.import_resources.table : v.resource_name => v if length(var.import_resources.table) > 0 } id = each.value.id to = module.bigquery_resources.google_bigquery_table.tables[each.value.resource_name] }
導入の効果
以下の表は、導入前後の作業比較です。導入前に比べて多くの作業が効率化されました。やったー!
作業内容 | 導入前 | 導入後 |
---|---|---|
スケジュールクエリやViewの更新 | 更新前後のクエリをコンフルに貼り付けてレビュー。レビュー通過後クエリをコンソールにコピペ | 開発ブランチで更新してプルリク作成。レビュー通過後マージ |
汎用的な処理の管理 | 以前のクエリからコピペ or Redash のスニペットを利用 | UDF や TVF を利用 |
汎用処理を更新 | 関連クエリを全て手作業で修正 | プルリク→マージ |
また、スケジュールクエリの管理が容易になったことで分析用の中間テーブルが数多く生成され、こちらも作業効率化やコスト削減にも貢献しています。
課題
このように様々な作業の効率化が進んでいる一方で、この基盤には以下の課題が残っています。
依存関係が管理できない
例えば新規にテーブルとそれを参照するViewを同時に作成する場合、先に View の生成が実行されて参照先が見つからずエラーになることがあります。
本来であれば Terraformの depends_on 引数を使って依存関係を指定すれば解決できますが、この引数は静的参照しか受け付けておらず利用できません。
このため現状は GitHub Actions でエラー発生時に再実行することで対応していますが、これはあくまで暫定的な方法です。
また、中間テーブルをもとに別の中間テーブルを作成するようなケースでは依存関係が複雑化し、管理が困難になる点も課題です。
デプロイ前の確認が不十分
現在の CI/CD パイプラインでは、terraform plan
の出力と terraform apply
の実行確認しか行っておらず、以下の観点での確認は出来ていません。
- SQLクエリの構文的な正しさ 構文が間違った状態のままデプロイし、実行エラーで初めて気付くといったことがしばしばありました。
- SQLクエリの出力内容の正しさ SQLの実行環境がないため、前処理が正しく行えているかや、確からしい集計値が得られているかなどの確認ができません。
これらの課題に対応するために、現在も継続して検討を行っています。
例えば以下のような方針を検討しています。
- クエリをコピペ実行可能な状態で管理
query.sql
にはコピペ実行可能な形式で記述し、デプロイ時に変数を上書きすることでクエリ実行のハードルを下げる
- Redash との連携
- プルリクエストに
query.sql
が含まれている場合、Redash 上にそのクエリを自動生成する
- プルリクエストに
- dataformに乗り換える
- 上記に加え依存関係の管理も可能であり、現在仕様調査を進めています
おわりに
データマートの管理方法は、世の中に多種多様な手法が存在します。本記事では、その中の一例として弊社が採用した基盤の構築と運用方法をご紹介しました。
この記事がみなさんの運用負荷を軽減し、より効率的なデータマート管理のヒントとなれば幸いです。
明日の記事は @_kurose さんの「BigQueryのExternal Tableのスキーマ変更に対応する方法の一つ」です!お楽しみに!