dbt × BigQueryで簡単なデータ基盤を作ってみた

データエンジニアリングに興味が湧いてきたので、趣味のデータで簡単なデータ基盤を作ってみました。

## 利用データ

今回はグライダーのフライトログデータを気象データと組み合わせるシンプルなデータ基盤を作ってみます。

グライダーに専用のGPSを載せて飛ぶと、飛行した軌跡（3次元座標 × 時間）が記録され、そのデータは[IGCファイル](https://xp-soaring.github.io/igc_file_format/index.html)という標準のフォーマットとして出力することができます。グライダーは上昇気流にうまく乗ることで長時間飛行を続けることができますが、その上昇気流の発生条件は気象要素（気温、風向、風速、日照時間など）に大きく依存します。フライトログデータを気象データと紐づけて分析すれば、どんな気象条件でどのようなポイントに上昇気流が発生しやすいか、またそれを踏まえてどのような飛び方をすればよいかの手掛かりが得られるかもしれません。

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988692/3a43cd69-c4fc-c343-faaf-b43edfdfbff1.png)
フライトログの例

## 作ったもの

作成した簡易的なデータ基盤は以下の通りです。ソースコードは[GitHub](https://github.com/mysuikabar/glider-data-infrastructure)で公開しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988692/9edaaeac-1616-f5e6-41fe-aa821bcc2074.png)


データソースはフライトログデータと気象データの2種類です。フライトログデータ（IGCファイル）はバケットへの投入をトリガーとして、Cloud Functionsを用いてパースした後、BigQueryにエクスポートします。気象データについても同じくCloud Functionsにより、日次で[気象庁HP](https://www.data.jma.go.jp/stats/etrn/index.php)からスクレイピングしてBigQueryにエクスポートします（スケジューリングにはCloud Schedulerを利用）。

BigQuery上のテーブルの作成にはdbtを利用します。GitHub Actionsでdbtの実行をスケジュールし、週次でフライトログデータと気象データを紐づけたテーブルの更新を行います。

## 実装

### Terraformによるインフラ構成

GCP上のインフラはTerraformを用いて構成します。Terraformにはmoduleという機能があり、システムを構成するコンポーネントをモジュールとして分割して定義することができます。そして、メインモジュール上でそれらを統合することで、全体のシステムを構成することができます。

今回は以下の3つのコンポーネントをモジュールとして定義しました。

- BigQuery
- バケット投入をトリガーとするCloud Functions（フライトログデータの処理に利用）
- Cloud Schedulerにより定期実行するCloud Functions（気象データの処理に利用）

以下はフライトログデータを処理するCloud Functionsのモジュールとそれを呼び出すメインモジュールの実装コードです（一部抜粋）。

```terraform
# infra/modules/storage_triggered_function/main.tf

resource "google_storage_bucket" "source_bucket" {
  name          = "${var.source_bucket_prefix}_${random_id.suffix.hex}"
  location      = var.location
  force_destroy = true
}

resource "google_storage_bucket" "function_bucket" {
  name          = "${var.function_bucket_prefix}_${random_id.suffix.hex}"
  location      = var.location
  force_destroy = true
}

resource "google_storage_bucket_object" "function_source" {
  name   = "function.zip"
  bucket = google_storage_bucket.function_bucket.name
  source = data.archive_file.function_archive.output_path
}

resource "google_cloudfunctions_function" "function" {
  name                  = var.function_name
  runtime               = "python312"
  available_memory_mb   = var.function_available_memory_mb
  source_archive_bucket = google_storage_bucket.function_bucket.name
  source_archive_object = google_storage_bucket_object.function_source.name
  entry_point           = "main"
  event_trigger {
    event_type = "google.storage.object.finalize"
    resource   = google_storage_bucket.source_bucket.id
  }
  environment_variables = {
    DATASET_ID = var.dataset_id
    TABLE_ID   = var.table_id
  }
}
```

```terraform
# infra/main.tf

module "igc_file_processor" {
  source                       = "./modules/storage_triggered_function"
  source_bucket_prefix         = "igc_files"
  function_bucket_prefix       = "igc_file_processor"
  function_name                = "igc_file_processor"
  function_available_memory_mb = 256
  dataset_id                   = "data_lake"
  table_id                     = "flight_log"
}
```

### dbtの定期実行

dbtはデータベース上のテーブルの変換処理（ELTのT）を多機能にサポートしてくれるツールです。dbtにはOSSとしてのdbt CoreとSaaSとしてのdbt Cloudの2種類がありますが、今回はdbt Coreを利用しました。dbt Coreの基本的な使い方は公式のチュートリアルが参考になりました。

https://docs.getdbt.com/guides/manual-install?step=1

dbtでは、作成したいテーブルをselect文で記述した`.sql`ファイルをdbtプロジェクトの`models/`に配置し、`dbt run`を実行することで、BigQuery上で変換処理が走りテーブルが作成されます。

今回はGitHub Actionsで`dbt run`を週次で実行するワークフローを定義しました。GCPの認証情報は、サービスアカウントのJSONファイルの中身をGitHubのシークレットとして登録し、ワークフロー実行時に読み込む形にしています。

```yaml
name: run dbt

on:
  schedule:
    - cron: '0 4 * * 0'
  workflow_dispatch:

jobs:
  run_dbt:

    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install dbt-bigquery
      - run: echo '${{ secrets.GCP_CREDENTIALS }}' > secrets/gcloud.json
      - run: dbt run
        working-directory: dbt/flight_log/
        env:
          GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
      - run: rm secrets/gcloud.json
```

### dbtドキュメントの自動公開

dbtを利用する嬉しさの一つにドキュメントの自動生成機能があります。`dbt docs generate`を実行すると、定義した`.sql`ファイルをもとに、各テーブルのスキーマやテーブル間の依存関係を表すグラフ（Lineage）などの情報を含んだドキュメントファイルが生成されます。そして、`dbt docs serve` を実行するとローカルでサーバーが起動し、dbtドキュメントにアクセスすることができます。

今回はGitHub Actionsを用いて、mainブランチの更新をトリガーとして、GitHub Pagesでdbtドキュメントを自動ホスティングするワークフローを作成しました。GitHub Pagesへのデプロイには、Marketplace上で公開されているアクション[actions-gh-pages](https://github.com/peaceiris/actions-gh-pages)を利用しました。

```yaml
name: publish dbt docs

on:
  push:
    branches:
      - main

jobs:
  publish_dbt_docs:
    
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install dbt-bigquery
      - run: echo '${{ secrets.GCP_CREDENTIALS }}' > secrets/gcloud.json 
      - run: dbt docs generate
        working-directory: dbt/flight_log/
        env:
          GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
      - uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: dbt/flight_log/target/
      - run: rm secrets/gcloud.json
```


最終的に構築した基盤のテーブル間の依存関係は以下のようなグラフで表示されます。フライトログデータと気象データをデータソースとして、それらを紐づけたテーブルを作成できました。

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2988692/99dc0276-7b20-1a18-a814-4826828d9ed5.png)


## おわりに

今回はdbtとBigQueryを中心とした簡易的なデータ基盤の作成を行いました。開発を通して、Terraformやdbtの基本的な使い方を身に付けることができた気がします。

データ基盤は作っても活用しないと意味がないので、今後はDashを用いたWebアプリケーション化などにも取り組んでみたいと思います。