📑

GCP Workflowsのリファクタリング:先行プロジェクトのベストプラクティスを後続プロジェクトに適用する

に公開

はじめに

複数のETLパイプラインプロジェクトを運用していると、先行プロジェクトで確立されたベストプラクティスを後続プロジェクトに適用したくなることがあります。

この記事では、同じ構成の2つのプロジェクト(プロジェクトA、プロジェクトB)で、プロジェクトAで確立されたワークフロー設計をプロジェクトBに適用し、デプロイエラーを修正した記録を残します。

背景:2つのプロジェクトの乖離

プロジェクトAの状況

  • Cloud Workflows + Cloud Functions + Dataformで構成されたETLパイプライン
  • L1(変換層)とL2(レポート層)を分離して実行する設計
  • 認証設定やエラーハンドリングが適切に実装済み

プロジェクトBの課題

  • プロジェクトAをベースに構築したが、古いバージョンをコピーしたまま更新されていない
  • デプロイ時に構文エラーが発生
  • Dataform実行が不完全(L1のみで、L2が実行されない)
  • 不要なバックアップファイルや古いドキュメントが混在

修正内容

1. Dataform実行ワークフローの認証修正

問題点

子ワークフロー(Dataform実行用)に、Dataformジョブを実行するためのサービスアカウントが指定されていませんでした。

# 修正前: サービスアカウント指定なし
invocationConfig:
    includedTags:
        - ${tag}
    transitiveDependenciesIncluded: true
    # serviceAccountの指定がない!

解決策

プロジェクトAの設定を参考に、serviceAccountを追加しました。

# 修正後: サービスアカウントを明示
invocationConfig:
    includedTags:
        - ${tag}
    transitiveDependenciesIncluded: true
    serviceAccount: "dataform-runner@project-b.iam.gserviceaccount.com"

これにより、Dataformが適切な権限でBigQueryジョブを実行できるようになりました。

2. 不要なファイルのクリーンアップ

プロジェクト内に蓄積していた不要ファイルを削除しました。

# 削除したファイル一覧
workflow-dataform-runner.yaml       # プロジェクトAからコピーされた古いファイル
main.py.bak                         # バックアップファイル
workflow-dataform-runner.yaml.bak   # バックアップファイル
workflow.yaml.bak                   # バックアップファイル
deployment_notes_2025-10-17.md      # 古いドキュメント
refactoring_summary_2025-10-20.md   # 古いドキュメント

クリーンアップのポイント

  • .bakファイルはGitで管理されているため不要
  • 他プロジェクトの設定ファイルが混在していると混乱の元
  • ドキュメントは最新版のみ残す

3. 親ワークフローのデプロイエラー修正

発生したエラー

親ワークフローのデプロイ時に、以下の構文エラーが発生していました。

parse error: call target must be a symbolic reference...

エラー箇所:

# 問題のあったステップ
- extract_folder_and_tag:
    assign:
        - folder: ${text.split(event.data.name, "/")[1]}
        - tag: ${"l1_" + folder}

原因分析

プロジェクトAの正常動作しているworkflow.yamlと比較したところ、以下の違いが判明しました。

プロジェクトA(正常動作):

- init:
    assign:
        - event: ${sys.get_env("GOOGLE_CLOUD_WORKFLOW_EXECUTION_ARGUMENT")}
        - parts: ${text.split(event.data.name, "/")}
        - folder: ${parts[1]}
        - tag: ${"l1_" + folder}

プロジェクトB(エラー発生):

- init:
    assign:
        - event: ${sys.get_env("GOOGLE_CLOUD_WORKFLOW_EXECUTION_ARGUMENT")}
        # parts変数がない

- extract_folder_and_tag:  # 別ステップで処理しようとしてエラー
    assign:
        - folder: ${text.split(event.data.name, "/")[1]}
        - tag: ${"l1_" + folder}

解決策

プロジェクトAの設計に合わせ、分割処理をinitステップに集約しました。

# 修正後
- init:
    assign:
        - event: ${sys.get_env("GOOGLE_CLOUD_WORKFLOW_EXECUTION_ARGUMENT")}
        - parts: ${text.split(event.data.name, "/")}
        - folder: ${parts[1]}
        - l1_tag: ${"l1_" + folder}
        - l2_tag: "l2_reports"  # L2用のタグも追加

なぜエラーが起きたのか

Workflows YAMLの変数スコープの問題でした:

  • text.split()の結果を直接インデックスアクセス [1] するのは構文的に問題がある可能性
  • まず変数に代入してから、その変数にアクセスする方が安全
  • initステップで必要な変数を一括定義する方が可読性も高い

4. Dataform実行ロジックの改善(L1/L2分離)

問題点

プロジェクトBのワークフローは、Dataformを1回しか呼び出しておらず、L1処理(生データの変換)しか実行されていませんでした。

# 修正前: L1のみ実行
- call_dataform_l1:
    call: execute_dataform_workflow
    args:
        tag: ${l1_tag}

これでは、L2処理(レポート層の集計)が実行されず、不完全なパイプラインになっていました。

解決策

プロジェクトAのL1/L2分離実行方式をプロジェクトBにも適用しました。

# 修正後: L1とL2を順次実行
- call_dataform_l1:
    call: execute_dataform_workflow
    args:
        tag: ${l1_tag}  # 動的タグ(例: "l1_campaigns")
    result: l1_result

- call_dataform_l2:
    call: execute_dataform_workflow
    args:
        tag: ${l2_tag}  # 固定タグ("l2_reports")
    result: l2_result

L1/L2分離のメリット

L1層(変換層):

  • ソースデータの種類ごとに動的にタグを切り替え
  • 例: l1_campaigns, l1_products, l1_stores
  • データソース固有の変換ロジックを適用

L2層(レポート層):

  • 固定タグで全体のレポートを生成
  • L1で変換されたデータを集約・結合
  • ビジネスロジックに基づいた最終的なテーブルを作成

デプロイ手順

修正後のデプロイ手順:

# 1. 親ワークフローのデプロイ
gcloud workflows deploy main-workflow \
    --source=workflow.yaml \
    --location=asia-northeast1

# 2. 子ワークフロー(Dataform実行用)のデプロイ
gcloud workflows deploy dataform-runner \
    --source=workflow-dataform-runner.yaml \
    --location=asia-northeast1

# 3. デプロイ確認
gcloud workflows list --location=asia-northeast1

リファクタリング後の全体フロー

GCSアップロード
    ↓
親Workflow起動
    ├─ init: 変数初期化(folder, l1_tag, l2_tag)
    ├─ Cloud Function呼び出し(L0ロード)
    ├─ Dataform L1実行(動的タグ)
    │   └─ データソース固有の変換
    └─ Dataform L2実行(固定タグ)
        └─ レポート層の集計

ベストプラクティスのまとめ

1. 先行プロジェクトの設計を基準にする

複数プロジェクトを運用する場合、最も成熟したプロジェクトの設計を「ゴールデンスタンダード」として採用しましょう。

2. 変数の初期化は1箇所で

Workflowsの変数は、initステップで一括定義することで:

  • 構文エラーを減らせる
  • 変数スコープが明確になる
  • 可読性が向上する

3. L1/L2の分離実行

データパイプラインは段階的に処理することで:

  • デバッグが容易
  • 部分的な再実行が可能
  • 各層の責務が明確

4. サービスアカウントは必ず明示

Dataformなど、他のサービスを実行する場合:

  • serviceAccountを必ず指定
  • 権限エラーを未然に防ぐ
  • セキュリティ要件を明確化

5. 定期的なクリーンアップ

不要ファイルは定期的に削除:

  • .bakファイルはGitがあれば不要
  • 古いドキュメントは混乱の元
  • 他プロジェクトのファイルは分離

まとめ

複数の類似プロジェクトを運用する際は、先行プロジェクトで確立されたベストプラクティスを積極的に横展開することが重要です。

今回のリファクタリングにより:

  • ✅ デプロイエラーの解消
  • ✅ L1/L2分離による完全なパイプライン実現
  • ✅ プロジェクト間の一貫性向上
  • ✅ メンテナンス性の向上

特にWorkflowsのような宣言的な設定ファイルは、一度確立されたパターンをテンプレート化して再利用することで、品質と生産性を大きく向上させることができます。

ぜひ参考にしてみてください!

Discussion