Cloud Run functions × TypeScript 開発をしよう！【セキュリティ：Secret Manager をTerraformで構成する】

# はじめに
この記事では以下のことについて説明しています。
- Terraformで Secret Manager を構成する方法
---

「セキュリティ」なんて大体的なカテゴリにしていますが、Seceret ManagerをCloud Run functionsで使う方法を紹介しているだけです。
# Secret Managerとは
以下公式引用です。

> Secret Manager は、API キー、ユーザー名、パスワード、証明書などの機密データを保存して管理できるシークレットおよび認証情報管理サービスです。

https://cloud.google.com/secret-manager/regional-secrets/overview-rs?hl=ja

要するに他所から見られると確実にまずい認証情報を置いておけるサービスです。一度コンソールの方で触ってみてください。

まずはこのSecret Managerに置く秘密情報を、Terraformで作成しましょう。

# 前準備：Secret Manager APIを有効にする
GCPの検索欄に「Secret Manager」を入力してサービスページにいきます。そして有効化しましょう。
![スクリーンショット 2024-12-16 2.20.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/374872/ffb1458c-980d-c25d-0813-04365fe8ff90.png)

# Terraformで秘密情報をSecert Managerに置く
今回Secret Managerに配置する秘密情報 `my-secret.json` です。`main.tf` と同じ階層に置いておきます。
```js: my-secret.json
{
    "hoge": "hogehoge"
}
```


## シークレットのリソース定義
レプリケーションポリシーを「自動」にした `my-secret` という名前のシークレットのリソースを作成します。
```tf: シークレットのリソース定義
resource "google_secret_manager_secret" "my-secret" {
  secret_id = "my-secret"

  replication {
    automatic = true
  }
}
```

## シークレットのバージョンのリソース定義
シークレットはバージョンという概念で値の管理をしています。まずシークレットの最新バージョンを示すリソース `my-secret-version` を表現します。値は先ほど作成した `my-secret.json` にします。

また、新しくバージョンが作られた際に過去のバージョンを全て破棄するようにします。

```tf: シークレットのバージョンのリソース定義
resource "google_secret_manager_secret_version" "my-secret-version" {
  secret      = google_secret_manager_secret.my-secret.id
  secret_data = file("${path.module}/my-secret.json") # string形式なのでこれ

  deletion_policy = "DELETE" # 新規バージョン作成時、過去のバージョンを全て破棄
}
```

### 注意1
`deletion_policy` には設定できる項目が `DELETE`, `DISABLE`, `ABANDON` の3つありますが、名前の雰囲気で設定すると「あれ？思ったように動かないぞ？」となります。

- `DELETE` : 直前のバージョンの破棄
- `DISABLE` : 直前のバージョンの無効化
- `ABANDON` : 直前のバージョンを保持

です。`ABANDON` が破棄じゃないの罠すぎる。

https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/secret_manager_secret_version#argument-reference

### 注意2
また、**`deletion_policy` の更新とシークレットの追加を同時に行った場合、更新した`deletion_policy` が反映されるのは次にシークレットの追加を行った時です。** 注意してください。

### 事例
■ 状況
ver.1のシークレット: 有効, versionの `deletion_policy`: `ABANDON`

■ アクション
`deletion_policy` を `DELETE`にして ver.2 のシークレットを追加し `terraform apply`

■ 結果1
ver.1のシークレット: **有効** → 無効にならない
ver.2のシークレット: 有効

■ 行動2
ver.3 のシークレットを追加し `terraform apply`

■ 結果2
ver.1のシークレット: 有効
ver.2のシークレット: **破棄**
ver.3のシークレット: 有効 → これ以降作られるverに新しい`deletion_policy`が適応される

### 解決策
`deletion_policy` の更新 とシークレットの追加を別々の `terraform apply` で行うことです。

## 最新のシークレットバージョンの `data` ソースを定義する
新規バージョンの作成は上記の `"google_secret_manager_secret_version" . "my-secret-version"` でできますが、参照は `data`ソースを使います。リソースで作成済みでないと404エラーになってしまうので、`depends_on` に対象のバージョンのリソースを記載します。

```tf: 最新のシークレットのバージョンに関する data ソース
data "google_secret_manager_secret_version" "my-secret-version-latest" {
  secret  = google_secret_manager_secret.my-secret.id
  version = "latest"

  depends_on = [
    google_secret_manager_secret_version.my-secret-version
  ]
}
```

## シークレットにアクセスするIAMを定義
そしてそのシークレットにアクセスするIAMを定義します。シークレットにアクセスするのはCloud Run functionsのランタイムですが、そのランタイムのデフォルトサービスアカウントは `<PROJECT_NUMBER>-compute@developer.gserviceaccount.com` の形式になっているので、IAMの `member` に指定してあげます。

```tf: Secret ManagerにアクセスできるIAMの設定
resource "google_secret_manager_secret_iam_member" "secretmanager-my-secret" {
  secret_id = google_secret_manager_secret.my-secret.id
  role      = "roles/secretmanager.admin"
  member    = "serviceAccount:${data.google_project.project.number}-compute@developer.gserviceaccount.com"

  condition {
   title       = "my-secret iam"
   description = "my-secret へのアクセス許可を行うIAM"
   expression  = "resource.name.startsWith(\"${google_secret_manager_secret.my-secret.name}\")"
  }
}
```

### 参考

https://cloud.google.com/functions/docs/concepts/iam?hl=ja#runtime_service_accounts

そして `terraform plan` を入力して、内容に問題がなければ `terraform apply` をしましょう。

```bash
$ terraform apply
# ...省略

  # data.google_secret_manager_secret_version.my-secret-version-latest will be read during apply
  # (config refers to values not yet known)
 <= data "google_secret_manager_secret_version" "my-secret-version-latest" { ... }

  # google_cloud_run_service_iam_member.member will be created
  + resource "google_cloud_run_service_iam_member" "member" { ... }

  # google_cloudfunctions2_function.default will be created
  + resource "google_cloudfunctions2_function" "default" { ... } 

  # google_secret_manager_secret.my-secret will be created
  + resource "google_secret_manager_secret" "my-secret" { ... }

  # google_secret_manager_secret_iam_member.secretmanager-my-secret will be created
  + resource "google_secret_manager_secret_iam_member" "secretmanager-my-secret" { ... }
  
  # google_secret_manager_secret_version.my-secret-version will be created
  + resource "google_secret_manager_secret_version" "my-secret-version" { ... }
  
  # google_storage_bucket.default will be created
  + resource "google_storage_bucket" "default" { ... }
  
  # google_storage_bucket_object.object will be created
  + resource "google_storage_bucket_object" "object" { ... }
  
  # random_id.default will be created
  + resource "random_id" "default" { ... }
Plan: 8 to add, 0 to change, 0 to destroy.

Changes to Outputs:
  + function_uri = (known after apply)

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes
  
# ...省略

Apply complete! Resources: 8 added, 0 changed, 0 destroyed.

Outputs:

function_uri = "https://xxx.a.run.app"
```

# おわりに
Secret Managerを Terraform で構成する方法を紹介しました。次回はこうして作成したシークレットのバージョンにCloud Run functions からアクセスする方法を紹介します。

# トラブルシューティング
## Error: Error creating function: googleapi: Error 403: Could not create Cloud Run service 関数名. Cannot access API run.googleapis.com in project プロジェクト名...

### エラー文
```bash
╷
│ Error: Error creating function: googleapi: Error 403: Could not create Cloud Run service sample-crf. Cannot access API run.googleapis.com in project プロジェクト名
│ 
│   with google_cloudfunctions2_function.default,
│   on main.tf line 42, in resource "google_cloudfunctions2_function" "default":
│   42: resource "google_cloudfunctions2_function" "default" {
│ 
╵
```

### 解決策
GCPのAPIが一部有効になっていないため発生している可能性があります。以下のコマンドを実行してみましょう。

```bash
$ gcloud services enable run.googleapis.com --project プロジェクト名

Operation "operations/acf.p2-735381230523-757f2cf0-10ce-43af-9812-3e93e1993208" finished successfully.
```