CloudRunとGKEのハイブリッド環境でのIaCを考える

# はじめに
この記事は[QualiArts Advent Calendar 2024](https://qiita.com/advent-calendar/2024/qualiarts)の18日目の記事になります。

私が過去関わったプロジェクトでは

- アプリケーションコードの管理はモノレポ。1つのリポジトリで複数のアプリケーションを管理し、そのアプリケーションが一定共通のロジックやデータスキームを使用しており実行環境で(厳密なリアルタイム性は必要ないが)互換性が必要となる。
- モノレポで管理されている複数のアプリケーションが同一のバージョンとして同時にDockerイメージがビルドされ、それぞれCloudRun,GKE上のワークロードにデプロイされる。
- 同一バージョンの各アプリケーションのDockerイメージだがIaCとしての管理が「CloudRunはTerraform Manifestsで管理」、「k8sワークロードの管理はHelm Chartで管理」といった形に分散されてしまっている。

といった背景から1回のデプロイ作業で2つのコードの調整とデプロイ作業が必要になるというケースがありました。

![スクリーンショット 2024-12-18 午後1.18.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932189/6cfc0f25-21d1-7851-fb46-3905558590f3.png)

本記事ではそういったIaCとしてのコードの分散が発生してしまうケースについてのアプローチについてお話しします。

# Helm Chartに寄せるパターン

1つ目の方法としてはTerraform Manifestsで管理しているCloudRunの定義をHelm Chart管理に寄せる方法が挙げられます。
ここではk8sリソースとしてGCPリソースを管理できるようにするための **ConfigConnector** というKubernetesアドオンを使用します。

https://cloud.google.com/config-connector/docs/overview?hl=ja

![スクリーンショット 2024-12-18 午後1.21.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932189/660c5cc3-a06d-711d-e2e1-37c25fa000d6.png)

CloudRun Serviceの定義をConfigConnectorを用い、k8sリソースとして書くと以下のようになります。
(引用: [Sample YAML(s) > Run Service Basic](https://cloud.google.com/config-connector/docs/reference/resource-docs/run/runservice#run_service_basic))

```
apiVersion: run.cnrm.cloud.google.com/v1beta1
kind: RunService
metadata:
  name: runservice-sample-basic
spec:
  ingress: "INGRESS_TRAFFIC_ALL"
  launchStage: "GA"
  location: "us-central1"
  projectRef:
    # Replace ${PROJECT_ID?} with your project ID.
    external: projects/${PROJECT_ID?}
  template:
    containers:
      - env:
          - name: "FOO"
            value: "bar"
        image: "gcr.io/cloudrun/hello"
    scaling:
      maxInstanceCount: 2
  traffic:
    - percent: 100
      type: "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
```

## メリット

CloudRun自体がKnativeサービスとしてk8s上に構築されているということもあり、定義自体も通常のPodやDeploymentといったk8sリソースのマニフェストとかなり類似したものになり、k8sマニフェストファイルを書き慣れていれば違和感なく利用することが可能です。
またConfigConnectorでは本記事で触れているCloudRunの他、多くのGCPリソースに対応をしているため、例えば

- アプリケーションで利用するCloudSQLやSpannerといったデータストアリソースの定義や構築もk8sリソース上で行う
- CloudRun Serviceが紐づくLBの設定をk8sリソース上で行う

といった、アプリケーションに関連する周辺リソースを一元管理することも可能です。

## デメリット

### 差分チェック
最も大きなデメリットとして感じたのは「terraform plan」のような定義適用前の差分チェックが行えないという点です。
マニフェストファイル自体のバリデーションはありつつも、マニフェストファイルの構成自体に問題がなければ実際の適用結果は `kubectl apply` や `helm install` をした後に `RunService` リソースのStatusを見て確認する形となるため、その点は非常に不便に感じました。

### 削除ポリシー
また `cnrm.cloud.google.com/deletion-policy: abondon` Annotationを付けることで回避することができますが、ConfigConnectorで定義したk8sリソースを削除することで同時にGCPリソースも削除されてしまうため、データストア系のリソースなどConfigConnectorを利用して扱うGCPリソースの削除ポリシーはかなり気を付ける必要がありました。

# Terraform Manifestsに寄せるパターン

2つ目の方法としてはHelm Chartを **Terraform helm provider** によってhelm installすることでHelm Chart自体のコードをTerraform Manifestsの定義に内包させる方法が挙げられます。

https://registry.terraform.io/providers/hashicorp/helm/latest/docs

![スクリーンショット 2024-12-18 午後1.30.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3932189/698d1a40-1b7b-af8b-119d-bffa7a5dfb4d.png)

ディレクトリ構成としては以下のようにしました。

```
.
├── helm
│   ├── Chart.yaml
│   ├── templates
│   │   └── deployment.yaml
│   └── values.yaml
├── main.tf
└── variables.tf
```

`main.tf` は以下のように記述します。

```
provider "google" {
  project = "<gcp project name>"
}

data "google_client_config" "default" {}

data "google_container_cluster" "default" {
  name = "<gke cluster name>"
}

provider "helm" {
  experiments {
    manifest = true
  }
  kubernetes {
    host                   = local.data.google_container_cluster.default.endpoint
    cluster_ca_certificate = base64decode(data.google_container_cluster.default.master_auth[0].cluster_ca_certificate)
    token                  = data.google_client_config.default.access_token
  }
}

resource "helm_release" "helm_sample" {
  name      = "helm-sample"
  chart     = "./helm"
  namespace = "default"
  timeout   = 300

  set {
    name  = "value1"
    value = "value1"
  }

  set {
    name  = "value2"
    value = "value2"
  }
}
```

## メリット

「Terraformで」とは言いつつも、Helm Chart自体は分散して管理していた状態と同じものを利用することができます。あくまで「Helmの実行クライアントとしてTerraformを使用する」といった感覚が近いと感じました。
またHelm Chartの動的なvaluesに対して、Terraform Manifests上で定義した値やTerraformによって生成されたGCPリソースの各種値をそのまま `Terraformのvariables` と `Helmのvalues` を通して受け渡しができるため定義を連携させながら一元管理できるという点ではメリットを感じました。

## デメリット

### local chartの内容変更検知

Terraform helm providerでは

- リポジトリを指定してリモートのHelm Chartをインストール
- ローカルのHelm Chartをインストール

の両方を行うことができますが、特に設定をしない限り「Terraform Manifests上の内容」のみを変更検知の対象とするため、ローカルで定義した独自のHelm Chartの内容の変更をしても `terraform plan/apply` では検知はされません。
そこで利用するのがhelm providerの定義上で設定する `manifests` という値です。

```
provider "helm" {
  experiments {
    manifest = true
  }
  ...
}
```

この `manifests` を true にすることでTerraform Manifestsに内包させたローカルHelm Chartの内容までをTerraformの管理下に置くことができます。
ただし、2024/12/18現在、この `manifests` はexperiments機能となっており利用するに当たっては破壊的変更や廃止などのリスクもあるためこの記事では `デメリット` として記載をします。

# まとめ

今回はIaCを実現するためのTerraform ManifestsとHelm Chartという異なる情報ソースを統合するための方法として「ConfigConnector」と「terraform helm provider」を紹介させて頂きました。

もしも同様の問題を抱えている方がいれば何か参考になれば幸いです。

以上、18日目の記事でした。引き続き今後の[QualiArts Advent Calendar 2024](https://qiita.com/advent-calendar/2024/qualiarts)の投稿を宜しくお願いします。