こんにちは!
今回は実際の業務で社内ツールをGCPのWorkflowsを使って全自動化したときの話をアウトプットしていきたいと思います。
本記事では、実装の話は一切せず設計方針をどう考えたのか、というプロセスに焦点を絞ってお話していきます。
背景
そもそもの背景として、既存システムはどのようなもので、どのような問題点があったのかを簡単に説明しておきます。
もともと社内ツールは主にAIを使ってデータ分析を行ったり、データをきれいにクレンジング(整形)したりするためのツールがバラバラに存在していました。今までは、これらのツールを組み合わせて1つの大きな工程を手動で回している状況でした。しかし、やはり手動で回すと手間も時間も多くかかるため、この作業を自動化したいというニーズが生まれました。そこで、今回システムチームへ社内ツールの全自動化を行う話が来たという経緯です。
既存システムの問題点
また、既存の各ツールはGCP上で動作しているもので、以下を基本構成として動作していました。
- 1つのツールは、複数のバッチから成り立っている
- 複数のバッチはPub/Subを経由することで、接続されている
- 各バッチはスケジューラーにて定期的に実行され、Pub/Subキューが入っているときのみ処理を実行する仕組みとなっている
図示すると、以下のようなイメージです。

しかし、この構成には以下のような問題点がありました。
- バッチが定期的に空実行されるため、リソースが無駄使いされている
- Pub/Subに設定されているAck Deadlineを超えて処理が実行されると、永遠にAckされない事故が起こりえる
- ツールを使っているビジネスサイドから見ると、実行ステータスがわからない
特に実行ステータスがわからない、という点は深刻な困りごとの1つで、ビジネスサイドは実行を開始してなかなかアウトプットが出てこなくてもエラーで落ちているのか、単に処理に時間がかかっているだけなのかなどの進捗がわからないのです。その結果、システムチームに問い合わせが多数来ており、その都度エラーがないかなどの確認作業が入るという状況でした。
メインの目的は自動化でしたが、サブの目的としてこうした既存の問題点も同時に解決するようなシステムが要求されていました。
解決策
自分は本案件の設計と一部の実装を担当しました。
まず、GCPサービスの調査から始めました。そこでWorkflowsというサービスがあることを知り、これが今回の解決策にうってつけだということがわかりました。
Workflowsは以下のような機能を備えています。
- Cloud Run JobsやAPIの呼び出しを自動化できる
- Console画面から実行ステータスや進捗が一目で確認できる
Workflowsを使うことで、バッチ実行の自動化はもちろん、空実行とPub/SubのAck Deadlineの問題も解決されます。さらに、実行ステータスや進捗はConsole画面の閲覧権限を付与するだけで済みます。まさに理想的なサービスでした。
というわけで、Workflowsの採用は早々に決まりました。これで大まかな方針は決まりましたが、まだ設計の詳細を決める必要があります。
- バッチで使われるデータはDB管理にするか、GCSのファイル管理にするか
- ファイル管理にする場合、GCSバケット構成はどうするか
- DB管理にする場合、テーブル構成はどうするか
これらは最低限決める必要があります。データ管理方法についてはファイル管理にしました。理由としては、一部実装の都合上(長くなるため割愛します)と実装コストの低さ、そして中間生成物をビジネスサイドでも容易に見られるという点で評価が高かったからです。
ここからは具体的なデータの流れとバケット構成の詳細などの全体的な構成を検討していきます。
新システム全体の構成案
Workflowsを使った新システムの基本動作原理は以下のような構想です。
- 1つのWorkflowは複数のバッチから成り立つ
- 各バッチの結果はそれぞれのバッチ専用のディレクトリに結果ファイルをアウトプットする
- 次のバッチは1つ前のバッチ実行結果を使って処理を実行する
図示すると、以下のようなイメージです。

これはあくまでもイメージで、実際には何十というバッチが動いて最終的な成果物をアウトプットします。
また、GCSバケットのディレクトリ構成は以下のようにしました。※あくまでイメージで細部は異なります
gs://my-automation-bucket/
├── input/
│ └── [workflow-name]/ # ワークフローごとの入力データ(固定、または定期配置)
│
├── workflows/ # 中間生成物・デバッグ用(14日間で自動削除)
│ └── [execution-id]/ # ワークフロー実行ごとの一意なID
│ ├── step-1-fetch-data/ # ステップごとのディレクトリ
│ │ └── result.json
│ ├── step-2-transform/
│ │ └── processed.csv
│ └── step-3-validation/
│ └── report.pdf
│
└── output/ # 最終成果物(永続保存)
├── latest/ # 常に最新の結果が配置される(後続システム参照用)
│ └── final_result.zip
└── old/ # 過去の全履歴
workflowsディレクトリは中間生成物を残しておくためのディレクトリになっています。 このディレクトリがなぜ必要だったのか、なぜこの構成なのかは後述します。
output配下をlatestとoldに分けているのは、実行数が多くなってきた場合にファイルが多すぎて今回実行した結果がどのファイルかがわかりづらい、という問題にあらかじめ対処するためです。最新のファイルをlastestに置いておき、過去の履歴をoldに移動してしまえば、最新の実行結果はlastestディレクトリを見に行くだけでよいのでより簡単になります。
非機能要件の検討
次に、非機能要件をどう満たすかを考える必要があります。ここでは、運用がしやすく将来的に壊れにくい設計にするためにはどうするかを重視して考えます。たとえば、以下のような項目。
- 1回のワークフロー実行時間は長時間になることが予想されるため、エラーで失敗した場合にリトライしやすくするためにはどうすればいいか
- エラーで失敗した場合、原因特定のための追跡をしやすくするためにはどうするか
- ファイル管理にした場合、実行のたびにファイル数が膨れ上がる懸念があるが、これをどう対処するか
1つ目のリトライに関しては、エラー時点から再開できるのが理想的です。しかし、Workflowsには標準で再開する機能は搭載されていません。そのため、少し工夫が必要になります。
2つ目に関しては、各バッチの実行結果(中間生成物)をファイルとして残しておけば、バッチログとともに実データも見られるので追跡がしやすくなりそうです。
3つ目に関しては、インプットディレクトリとアウトプットディレクトリはビジネスサイドで定期的に削除してもらうことにし、中間生成物のファイルは一定期間で自動的に削除すればよさそうです。
というわけで、リトライ以外の項目については方針が決まったのでエラー時点からのリトライを具体的にどう実現するかを考えていきます。
エラー時点からのリトライを設計する
エラー時点からのリトライを実現するためには、1つ重要な条件があります。それは、「再開時点のバッチは、直前のバッチ実行結果を知っている必要がある」という点です。現在実行しているバッチは、直前のバッチ実行結果を使うためです。
そこで、workflowsディレクトリがこれを解決してくれます。少しだけ戻ってディレクトリ構成を見てみましょう。
├── workflows/ # 中間生成物・デバッグ用(14日間で自動削除)
│ └── [execution-id]/ # ワークフロー実行ごとの一意なID
│ ├── step-1-fetch-data/ # ステップごとのディレクトリ
│ │ └── result.json
│ ├── step-2-transform/
│ │ └── processed.csv
│ └── step-3-validation/
│ └── report.pdf
こうなっていました。ここで重要なのは「実行IDでディレクトリを切っている」という点です。こうすることで、実行IDさえわかれば、中間生成物のパスをすべて特定することができるようになります。
これでリトライを実現する条件をクリアすることができます。そのために、上記のようなバケット構成にしていました。
あとは、以下のWorkflowロジックにすればリトライを実現できそうです。
- リトライするときは手動実行とし、Workflow実行引数にリトライしたいWorkflowの実行IDとリトライ開始時点のステップ名を指定できるようにしておく
- 実行IDから該当のWorkflowの中間生成物のパスをすべてWorkflow変数にセットしておく
- リトライ開始時点が指定された場合は指定された開始ステップまでスキップする処理を冒頭に入れておく
図示すると、以下のようなイメージになります。

設計の答え合わせ
実際に設計どおりに実装が進んだか、実運用で問題などなかったかを現在視点で振り返りたいと思います。
結論から言うと、おおむね当初の設計方針通りに実装は進み、今のところ実運用でも深刻な問題はなく運用できているかと思います。特に、問題なくバッチが動いているかなどの確認の問い合わせが以前よりも減ったのと、システムチーム側で悩みの種になっていたPub/Subの問題を考えなくてよくなった点が大きいと思います。
リトライ機能も特にリリース当初は活躍できていました。リリース当初は想定外のバグが発生していたり、設定ミスでうまく動かない事例がありましたが、エラー時点から再開できるようにしていたことで効率的にリトライができていました。リトライ機能をつけていたのは結果的に正解だった気がします。
また、DB管理かファイル管理かの選択の分岐もファイル管理でよかったと思っています。ファイル管理にしていたことで、ビジネスサイドでも容易に中間工程をデバッグできるようになり、実装コストもそこまでかけずに実装できました。ファイル管理では特に実装上の問題は出ませんでしたが、DB管理にしていたら何かしらの不都合が出ていた可能性もあります。
今振り返ると、そもそも現時点でどのような問題点があり、どのようなシステムを作りたいのかなどの要件が明確になっていたという点は非常に重要だったと思います。ビジネスサイドの担当者は少し抽象度の高いふわっとしたイメージで語っていることも往々にしてあるので、それを自分なりにブレークダウンしていき明確な要件として定義することが実は最重要だった気がします。要件を明確にしていたおかげで、必要なソリューションを探し出すことができたのかなと思います。
非機能要件については、担当者から特に要求はなかったので自分で考える必要がありました。パフォーマンス、保守性、スケーラビリティなどいろいろ考慮すべきことはあると思いますが、特に仕様変更に強く壊れにくいという軸で考えておけば大きく外すことはないのかなと思いました。
まとめ
今回の設計における教訓をまとめます。
- 複数のバッチやAPI実行の自動化(オーケストレーション)にはGoogle CloudのWorkflowsが強力な選択肢になりえる
- システム設計はパフォーマンス、保守性、スケーラビリティなどいろいろ考慮すべきことはあるが、保守性を軸に考えるとよさそう
- ビジネスサイドの担当者としっかりすり合わせて、機能要件と非機能要件を明確にしておくのが最重要
コメントを投稿