Terraformを使ってWorkflowsとCloud Functionsをデプロイする

データエンジニアをしています。
運用しているデータパイプラインの一部にGoogle CloudのWorkflowsとCloud Functionsを用いておりますが、デプロイの俊敏性・正確性に課題があります。（コピペ・手動デプロイをやめたいです）
Terraformを使用してこれらを解消できないか、検証をしたことを共有します。

# 想定読者

- Terraform初心者
- Terraformを業務に導入してみたいと考えている人
- 既存のデプロイフローや手法に課題があると考えている人

# 試したことをかんたんに

- WorkflowsとCloud Functionsをterraformでデプロイした
- Cloud FunctionsはローカルのソースコードをZIP化してGCSにアップロードし、それをもとにCloudFunctionsをデプロイした
- 外部ファイル（ローカルのソースコードのファイル）を読み込む形でWorkflowsのソースコードを指定した

# 試した環境・構成
- Cloud Shell

```ディレクトリ構成.txt
work
  └ archive
      └ cloudfuncitons
           └ test_function1
                └ main.zip
  └ cloudfunctions
       └ test_function1
            └ main.py 
  └ workflows
       └ test_workflows
             └ test_workflow.yml
  └ main.tf
```

`archive`
Cloud FunctionsをデプロイするためにZIP化したソースコードを格納するディレクトリ。
実運用ではCloud FunctionsだけでなくLambdaも使用予定なので、サービス名別・関数名別にディレクトリを作成している。

`cloudfunctions`
デプロイするCloud Functionsのソースコードを格納するディレクトリ。

`workflows`
デプロイするWorkflowsのソースコードを格納するディレクトリ。

`main.tf`
Cloud FunctionsやWorkflowsとそれに関連するリソースの構成情報を記載したtfファイル。

# Cloud Functionsのソースコード

Hello Worldを出力するシンプルなコードです。

```main.py
def hello_world(request):
    print('Hello World')
    return 'Hello from Cloud Functions!'
```

# Workflowsのソースコード

Cloud Functionsを呼び出すシンプルなコードです。

```test_workflows.yml
- invoke_test_function1:
      call: http.get
      args:
          url: "Cloud FuncitonsのURL"
          auth:
            type: OIDC
      result: response
- logResponse:
      call: sys.log
      args:
        text: ${response.body}
        severity: "INFO"
```

# Terraformのソースコード

`プロバイダの設定`

```main.tf
provider "google" {
  project = "プロジェクト名"
  region  = "asia-northeast1"
}
```

`Cloud Functionsの構成情報`

```main.tf
# CloudFunctionsのソースコードを格納するGoogle Cloud Storageバケット
resource "google_storage_bucket" "cloud_functions_bucket" {
  name     = "バケット名"
  location = "asia-northeast1"
  # アクセスレベルを均一化
  uniform_bucket_level_access = true
  # 公開アクセスを「非公開」に
  public_access_prevention = "enforced"
}

# CloudFunctionsのソースコードをGCSのオブジェクトとしてアップロードする
resource "google_storage_bucket_object" "test_function1" {
  name   = data.archive_file.function_archive.output_path
  bucket = google_storage_bucket.cloud_functions_bucket.name
  source = data.archive_file.function_archive.output_path
}

# CloudFunctionsのローカルのソースコードをZIP化して、ローカルのarchiveディレクトリに格納する（このZIPファイルをGCSにアップロードする）
data "archive_file" "function_archive" {
  type        = "zip"
  source_dir  = "cloudfunctions/test_function1"
  output_path = "archive/cloudfunctions/test_function1/main.zip"
}

# CloudFunctionsの構成情報
resource "google_cloudfunctions_function" "test_function1" {
  name        = "test_function1"
  description = "An example Cloud Function"
  runtime     = "python310"

  # GCSにアップロードしたソースコードをもとにデプロイする
  source_archive_bucket        = google_storage_bucket_object.test_function1.bucket
  source_archive_object        = google_storage_bucket_object.test_function1.source
  available_memory_mb          = 128
  entry_point                  = "hello_world"
  trigger_http                 = true
  https_trigger_security_level = "SECURE_ALWAYS"
  timeout                      = 60
}
```

`Workflowsの構成情報`

```main.tf
# Workflowsの構成情報
resource "google_workflows_workflow" "test_workflow" {
  name        = "test_workflow"
  description = "An example workflow"
  # ローカルのymlファイルをソースコードとしてデプロイする
  # ${path.module}はTerraformファイルがあるディレクトリを表します
  source_contents = file("${path.module}/workflows/test_workflows/test_workflow.yml")
  # TODO: 既存サービスアカウントもTerraformで扱えるようにimportしたい
  service_account = "既存のサービスアカウント"
}
```

# ハマったポイント

結果として`terraform apply`でのデプロイは成功したのですが、それまでにいくつかハマったポイントがあったので、記録しておきます。

## Workflowsのソースコードを外部から読み込む

TerraformやGoogle Cloudのドキュメントを見ていると、Workflowsのソースコードはtfファイルに直書きしている例が多かったです。
しかし、これだと保守性も悪くなりそうですし、テキスト容量の制限（32KB）もあったので、外部ファイルを読み込むことができないか模索しました。

結果、Terraformの`file`関数と`path.module`変数を用い、下記のように外部ファイルを読み込むことができました。
```
file("${path.module}/workflows/test_workflows/test_workflow.yml")
```

参考1：[google_workflows_workflow](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/workflows_workflow)
参考2：[Terraform を使用してワークフローを作成する](https://cloud.google.com/workflows/docs/samples/workflows-workflow-deploy?hl=ja)


## WorkflowsからCloud Functionsを呼び出すとき

これはTerraform関連の話ではないのですが、WorkflowsからCloud Functionsを呼び出すとき、403エラーが出でしまいました。
Workflowsには十分な権限を付与していたのですが、エラーとなってしまいました。

ドキュメントをみると`auth`セクションを加えることで認証情報を付与してCloud Functionsを呼び出すことができるようでした。

>Cloud Functions や Cloud Run にリクエストを送信するときは、OIDC を使用して認証します。

>OIDC を使用して HTTP リクエストを行うには、URL を指定した後に、ワークフローの定義の args セクションに auth セクションを追加します。この例では、Cloud Functions を呼び出すために、リクエストが送信されます。

参考：[ワークフローに認証情報を追加する](https://cloud.google.com/workflows/docs/calling-run-functions?hl=ja#add_auth_info)


# 今後の展望
- 既存のリソースをTerraformで管理できるようにする
    - すでに運用しているデータパイプラインをTerraformで扱えるようにする
- Step FunctionsとLambdaもTerraformで管理できるようにする
    - すでに運用しているデータパイプラインはStep FuncitonsとLambdaも使用しているため
- Githubとクラウド環境のソースコードおよび構成情報を一致させるために、CI/CDパイプラインとTerraformを連携させる
    - Github ActionsとTerraformを組み合わせてデプロイの俊敏性・正確性を上げる