Cloud Workflowsを使ってみて得たTips

この記事は、[Supershipグループ Advent Calendar 2024](https://qiita.com/advent-calendar/2024/supership)の4日目の記事になります。

## はじめに
Google CloudのWorkflowsを業務で使用する機会があったため、そこで得た知見を共有したいと思います。
Workflows をあまり触ったことがない人向けです。

## 事例
今回は、GCSの任意の`orig`フォルダに動画をアップロードしたときに動画変換を行うワークフローをWorkflowsを使って実現しました。

```mermaid
graph TD
    A(../orig/に動画を格納) --> B[動画変換を要求]
    B --> C{変換状態をチェック}
    C -->|完了| D[GCSにアップロード]
    C -->|未完了| E[5分待つ]
    E --> C
    D --> F[メタデータをFirestoreに保存]
```

## 柔軟なトリガーを使いたい時は Cloud Audit Logs 経由のイベントを指定する
さっそくWorkflowsというよりEventarcの話になりますが…。
GCSの任意のフォルダへアップロードしたときにトリガーを発火させたい場合は、Eventarcの[パスパターン](https://cloud.google.com/eventarc/docs/path-patterns?hl=ja)が有効でした。

例えば、今回の事例ではフォルダ `orig` を含むときに発火させたかったので、パスパターンは以下のようになりました。
```
/projects/_/buckets/バケット名/**/orig/*
```

このパスパターンですが、イベントタイプによって使用できない場合があります。
今回の事例でいうと、GCSをイベントプロバイダ、Workflowsをイベントの宛先とする場合、Eventarcトリガーで使用できるイベントタイプには以下の2種類があります。
- Cloud Audit Logsイベント
- 直接イベント

[![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204622/82c9bae6-4940-f1f6-b6dd-83e9bc942c1d.png)](https://cloud.google.com/eventarc/docs/event-providers-targets?hl=ja)
この2つについて[パスパターンの適用可否を確認](https://cloud.google.com/eventarc/docs/path-patterns?hl=ja#identify_if_you_can_apply_a_path_pattern)してみます。

Cloud Audit Logsイベントの場合は、`pathPatternSupported: true`とありますので、パスパターンを適用できることがわかります。
```
gcloud eventarc providers describe cloudaudit.googleapis.com --location=asia-northeast1
displayName: Cloud Audit Logs
eventTypes:
- description: An audit log is created that matches the trigger's filter criteria.
  filteringAttributes:
  - attribute: methodName
    description: The identifier of the service's operation.
    required: true
  - attribute: resourceName
    description: The complete path to a resource. Used to filter events for a specific
      resource.
    pathPatternSupported: true <-
  - attribute: serviceName
    description: The identifier of the Google Cloud service.
    required: true
  - attribute: type
    required: true
  type: google.cloud.audit.log.v1.written
name: projects/プロジェクトID/locations/asia-northeast1/providers/cloudaudit.googleapis.com
```
一方で直接イベントの場合は、`pathPatternSupported: true`が見当たらないので、パスパターンを適用できないことがわかります。
```text
$ gcloud eventarc providers describe storage.googleapis.com --location=asia-northeast1
displayName: Cloud Storage
eventTypes:
- description: The live version of an object has become a noncurrent version.
  filteringAttributes:
  - attribute: bucket
    description: The bucket name being watched.
    required: true
  - attribute: type
    required: true
  type: google.cloud.storage.object.v1.archived
- description: An object has been permanently deleted.
  filteringAttributes:
  - attribute: bucket
    description: The bucket name being watched.
    required: true
  - attribute: type
    required: true
  type: google.cloud.storage.object.v1.deleted
- description: A new object is successfully created in the bucket.
  filteringAttributes:
  - attribute: bucket
    description: The bucket name being watched.
    required: true
  - attribute: type
    required: true
  type: google.cloud.storage.object.v1.finalized
- description: The metadata of an existing object changes.
  filteringAttributes:
  - attribute: bucket
    description: The bucket name being watched.
    required: true
  - attribute: type
    required: true
  type: google.cloud.storage.object.v1.metadataUpdated
name: projects/プロジェクトID/locations/asia-northeast1/providers/storage.googleapis.com
```
以上より、GCSでイベントプロバイダとしてパスパターンによる柔軟なトリガーを使いたい場合は、Cloud Audit Logs経由のイベントを指定する必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204622/945b28b0-9559-a23f-e8c0-a5553733643e.png)

prefix指定のみできれば良い場合は、直接イベントで行う方法もあるようですね！
- [GCSへのファイルアップロードをトリガーにWorkflowsを実行する(prefix指定で)](https://qiita.com/legacsilve/items/66ef77346763db9cf230)

ちなみに2024年10月31日にPreview版になった[Eventarc Advanced](https://cloud.google.com/eventarc/advanced/docs/overview)の方が、より詳細な制御ができるそうですが、GCSはサポートされておらず、また東京リージョンでは使えませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204622/1a1ec82d-9a3f-4aa8-dd19-efcf6cc896f3.png)

## サブワークフローは変数のスコープが分断される
[公式ベストプラクティス](https://cloud.google.com/workflows/docs/best-practice?hl=ja#use_subworkflows_and_external_workflows)にもある通り、サブワークフローを使用するとワークフローを分割することができ、ワークフローの複雑化を回避することができます。
可視化されるフロー図もサブワークフロー単位となるので実行の流れも追いやすくなります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204622/92b16d4e-9cfe-996f-fd6e-21412a662d9a.png)
エラーハンドリングのような共通で使用するワークフローは、特にサブワークフローとして切り出すと良さそうです。

ただし変数のスコープが分かれるので、サブワークフローで使用する変数をparamsフィールドで受け取る必要があります。
> Subworkflows do not have access to any variables from the main block of a workflow or other subworkflows. A subworkflow receives values passed in a call, and assigns those values to new variables in the subworkflow scope. Note that all step names in a subworkflow must be unique, even if they are in a scope.
> https://cloud.google.com/workflows/docs/reference/syntax/variables#variable-scope

例えば、発生したエラーをFirestoreに格納したい場合、格納するdocument_pathを参照するためにエラーが発生するワークフローからdocument_pathを引き回す必要がありました。
```yaml
main:
  params: [event]
  steps:
    - init:
        assign:
          - document_path: ...
    - call_convert_file:
        call: convert_file
        args:
          ...
          document_path: ${document_path}
    - call_upload_converted_file:
        call: upload_converted_file
        args:
          ...
          document_path: ${document_path}
    ...

convert_file:
  params: [..., document_path]
  steps:
    - post_convert:
        try:
          ...
        except:
          as: error
          steps:
            - known_error:
                switch:
                  - condition: ${error.body != null and error.body.status != null}
                    steps:
                      - store_known_error:
                          call: store_error
                          args:
                            error_code: ${error.body.code}
                            error_message: ${error.body.message}
                            document_path: ${document_path}
                          next: end
            - unknown_error:
                call: store_error
                args:
                  error_code: "unknown"
                  error_message: ${error.message}
                  document_path: ${document_path}
                next: end
    ...

upload_converted_file:
  params: [..., document_path]
  steps:
    ...

store_error:
  params: [error_code, error_message, document_path]
  steps:
    - init_store_error:
        assign:
          - project_id: ${sys.get_env("GOOGLE_CLOUD_PROJECT_ID")}
    - store_error_to_firestore:
        call: googleapis.firestore.v1.projects.databases.documents.patch
        args:
          name: ${"projects/"+project_id+"/databases/(default)/documents/"+document_path}
          updateMask:
            fieldPaths:
              - "errorCode"
              - "errorMessage"
          body:
            fields:
              errorCode:
                stringValue: ${error_code}
              errorMessage:
                stringValue: ${error_message}
    - raise_error:
        raise:
          error_code: ${error_code}
          error_message: ${error_message}
```
サブワークフローはプログラミング言語の関数に似ていますが、グローバル変数のようには変数を使えないと思って分割を考える必要があります。

## 環境変数で環境差分を吸収する
各環境でアクセスするドメイン名やSecret Managerのシークレット名などが異なる場合は、[環境変数](https://cloud.google.com/workflows/docs/use-environment-variables?hl=ja)を使用してYAMLの共通化を図ることができます。
env.prod.yaml や env.stg.yaml として保存しておき、`--env-vars-file` オプションでそのパスを指定すれば読み込むことができます。
```
$ gcloud workflows deploy WORKFLOW_NAME --env-vars-file ./env.prod.yaml
```
コンソール画面上からも確認、編集することができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/204622/4dd0bc4e-b34f-464a-57d1-186532ea8b6b.png)

環境変数をWorkflowsのコード上から参照するときは、[sys.get_env()](https://cloud.google.com/workflows/docs/reference/stdlib/sys/get_env)を使用します。
```yaml
main:
  steps:
    - init:
        assign:
          - keyValue: ${sys.get_env("KEY1")}
    - returnResult:
        return: ${keyValue}
```
使用できる環境変数は最大で20個、4KiBまでです。

## 長時間実行には向いているが処理には向いていない
Workflowsは、実行可能な最長時間が**1年**と非常に長いため、タイムアウトを気にする必要がほとんどありません。
料金形態もステップごとの課金のため、長時間実行による課金はありません。
プロジェクトあたりの同時実行数も定期的に増加されており、現在では**10,000**となっています[^KEY1]。

今回の事例では動画変換状態のポーリングをしていて変換には5〜10分以上かかるため、Cloud Functionsではタイムアウト時間 9分[^KEY2] を超える恐れがありましたが、Workflowsではタイムアウトの心配をせずに済みました。

その一方で、Workflowsは、変数に512KBまでしかデータを持つことができません。

今回の事例では変換後の動画がS3に格納されており、それをGCSにアップロードする必要がありましたが、512KBまでしかデータを持つことができず、それ以前にHTTPレスポンスサイズも2MBまでなので、ダウンロード時点でエラーになってしまいました。
仕方ないので大人しく、アップロード処理のみCloud Functionsで実装しました。
他にも、割り当て(assign)できる変数の上限が50個であることも考慮する必要があります。

以下の記事でも言及されているように、基本的に処理はCloud Functionsに任せてワークフローの実装に集中するのが良さそうですね。

- [Google Cloud Workflowsに過度な期待をするな](https://qiita.com/zackey2/items/dcb96d3a3dfde4fd07ad)

## おわりに
Workflowsを使用してみて、Google Cloudサービスであれば認証情報を渡さずとも正しいロールが設定してあれば簡単にオーケストレーションできるのがやはり便利でした。
[公式サンプル](https://github.com/GoogleCloudPlatform/workflows-demos)が充実しているのもありがたいですね。

:::note
**最後に宣伝です。**

Supershipではプロダクト開発やサービス開発に関わる人を絶賛募集しております。
ご興味がある方は以下リンクよりご確認ください。
[Supership 採用サイト](https://supership.jp/recruit/)
是非ともよろしくお願いします。
:::

[^KEY1]: 日本語ドキュメントでは未更新のためか7,500になっているので注意: https://cloud.google.com/workflows/quotas?hl=ja#quotas

[^KEY2]: https://cloud.google.com/functions/docs/configuring/timeout?hl=ja