terraformでGCPのCloud functionsをデプロイしてみた

# やりたいこと
- terraformでcloudfunctionsをデプロイする
- cloudfunctionsは課金の予算上限を超過した際のbudgetアラートをトリガーに、請求先アカウントの紐づけ解除による対象プロジェクトの課金を無効化する

![qiita_functions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3550845/fdbb89f6-85d7-e2e1-e9ed-0d925dfeaf9e.png)


# つまずき
- terraformの公式ドキュメントに記載のサンプルを参照しても実際のgoogleAPIからエラーを返されたりとトライアンドエラーの部分が多かった。githubのissueで検索かけると同じ事象が発生していて、デバッグの際に参考するとよさげ。
https://github.com/hashicorp/terraform-provider-google/issues
# いまだに上手くいっていないこと
- cloudfunctionsで関数をデプロイする際にterraformのオプションで任意のサービスアカウントを指定できるはずだが、デフォルトのcompute engine用のサービスアカウント以外ではデプロイに失敗する。(おそらく、[このissue](https://github.com/hashicorp/terraform-provider-google/issues/5889)と同現象だと思われる）
私の勘違いである場合はぜひコメントいただけますと幸いです！

# ソースコード
- cloud functionsにデプロイする用のpythonコードを下記URLからコピぺする。
https://cloud.google.com/billing/docs/how-to/notify?hl=ja
（他言語もあるのでお好きな言語をご利用ください。）
ディレクトリ構成は下記のようになっていて、srcディレクトリの下に必要な関数を格納しています。
（terraformのディレクトリ構造プラクティスについていずれ学ばなくては。。）
```
.
├── main.tf
├── src
│   ├── main.py
│   └── requirements.txt
```
-  terraformソースコード
main.tfに記載の内容は下記６つのリソースを定義しています。
① 管理用プロジェクトでAPIの有効化
② サービスアカウントへ権限追加
③ pythonコード類をzipする
④ Google cloud storage にzipファイルを保存するバケット作成
⑤ Pub/Sub topicsを作成
⑥ 予算アラートの作成
⑦ 予算アラートがパブリッシュされることをトリガーに課金を無効化するcloud functionsを作成

① variableに格納された有効化したいAPIリストを参照して有効化する。（別の処理もlaC化しているので不要なAPIもあるかもしれません。）
有効化に1,2分かかることがあるため、他のリソースが並行して作成されるとAPIが使用できない旨のエラーが返される。
回避策としては、数分待つ処理（"hashicorp/time"プロバイダーのtime_sleepリソース）を定義し他のGCPリソースより前に作成されるようにdepends_onで依存関係を定義しておくことでＡＰＩが有効になるまでの数分を待機する。
```tf 
variable "gcp_service_list" {
  description ="The list of apis necessary for the project"
  type = list(string)
  default = [
    "cloudresourcemanager.googleapis.com",
    "serviceusage.googleapis.com",
    "iam.googleapis.com",
    "cloudbilling.googleapis.com",
    "billingbudgets.googleapis.com",
    "pubsub.googleapis.com",
    "cloudfunctions.googleapis.com",
    "artifactregistry.googleapis.com",
    "cloudbuild.googleapis.com",
    "compute.googleapis.com",
    "run.googleapis.com",
    "eventarc.googleapis.com" 
  ]
}
resource "google_project_service" "gcp_services" {
  for_each = toset(var.gcp_service_list) #ループしてリソースを複数作成する
  project = [管理用のプロジェクトを指定].id　#cloudfunctionsを呼び出すプロジェクトIDを指定する
  service = each.key 
}
```
②請求先アカウントの紐づけ解除するのに必要なロール(billing.projectManager）を無効化対象のプロジェクトでfunctionsのサービスアカウントに付与する

```tf

data "google_service_account" "functions_sa" {
  account_id = "${[管理用のプロジェクトを指定].number}-compute@developer.gserviceaccount.com"
  depends_on = [google_cloudfunctions2_function.disablebilling]
}


resource "google_project_iam_binding" "project_iam_binding_billing_maneger" {
  project = [課金を無効にする対象のプロジェクトIDを指定]
  role    = "roles/billing.projectManager"
  members = [
    "serviceAccount:${data.google_service_account.functions_sa.email}",
  ]
  depends_on = ["data.google_service_account.functions_sa"]
}
```

③ソースコードをzip化する。
```tf
# ソースプロバイダーはhashicorp/archive、バージョンは2.4.0です
data "archive_file" "default" {
  type        = "zip"
  source_dir  = "./src"
  output_path = "./XXXXX/XXXXXXXXXX.zip" #<-任意の出力ファイルパスを記載
}
```
④ Google cloud storage にzipファイルを保存するバケット作成
```tf
resource "google_storage_bucket" "default" {
  project                     = "${data.google_project.[管理用プロジェクト].number}" 
  name                        = "${random_id.default.hex}-functions-source" # Every bucket name must be globally unique
  location                    = "ASIA-NORTHEAST1"
  uniform_bucket_level_access = true
}

resource "google_storage_bucket_object" "object" {
  name   = "unlink_bllingaccount_triggered_budget_alert.zip"
  bucket = google_storage_bucket.default.name
  source = data.archive_file.default.output_path # Add path to the zipped function source code
}

```
⑤pub/sub topicsも無効化対象のプロジェクトごとに作成する。

```tf
resource "google_pubsub_topic" "topic" {
  project = "${replace([管理用のプロジェクトを指定].id,"projects/","")}" 
  name = "overbudget-${google_project.[無効化対象のプロジェクト].number}"
}
```
⑤ デプロイする複数のプロジェクトごとにアラートを作成する。
```tf
resource "google_billing_budget" "budget" {
  count       = "${length(var.projectlist)}" # プロジェクトの数だけループする
  billing_account = data.google_billing_account.account.id #請求先アカウントのIDをセット
  display_name = "Billing Budget-${element(var.projectlist, count.index),0}" #プロジェクトの識別子をアラートの表示名にしている 
  amount {
    specified_amount { #予算を記載
      currency_code = "JPY"
      units = "20000"
    }
  }
  budget_filter {
    projects = ["projects/${element(google_project.project, count.index).number}"] #.numberでなく、.idだとうまくいかない。
  }

  threshold_rules {
      threshold_percent =  0.9
  }
  
  all_updates_rule {
    pubsub_topic= "${element(google_pubsub_topic.topic, count.index).id}" 
    #ここにアラート発生時にメッセージをパブリッシュするトピックを記載する。今回はプロジェクトごとにpub/sub topicsを実装している。
  }
  
}
```

⑥ 無効化するプロジェクトごとに、管理用プロジェクトでcloud functionを定義する。

```tf

resource "google_cloudfunctions2_function" "disablebilling" {
  count       = "${length(var.projectlist)}"
  project     = "${replace([管理用のプロジェクトIDを指定].id,"projects/","")}" 
  # 単純に.id属性を指定すると、失敗する。APIにリクエストするprojectidが/projects/projects/XXXXXという正しくないIDとなり、not foundが返されてしまう。
  name        = "XXXXXXXXX"
  location    = "asia-northeast1"
  description = "when project cost is over the budget, the triggerd function unlinks the billing account."

  build_config {
    runtime     = "python312" #ランタイムの指定
    entry_point = "stop_billing" # main.pyで実装されたレスポンスを受け取る関数を指定
  }

  service_config {
    max_instance_count = 1
    available_memory   = "256M"
    timeout_seconds    = 60
    environment_variables = {
      GCP_PROJECT = "${element(google_project.project, count.index).number}" #環境変数に請求先アカウントを無効化するプロジェクトを指定する
    }
    ingress_settings = "ALLOW_INTERNAL_ONLY"
    #service_account_email = デフォルトのサービスアカウント以外を指定すると失敗する
  }
  event_trigger { #トリガーとなるpubsub topicを指定
    trigger_region = "asia-northeast1"
    event_type = "google.cloud.pubsub.topic.v1.messagePublished"
    pubsub_topic = "${element(google_pubsub_topic.topic, count.index).id}"
    retry_policy = "RETRY_POLICY_RETRY"
  }
}
```