PRベースでTerraformを適用！Atlantis × Helm × GKEでCI/CD環境を構築するPart1

## 記事の内容
本記事では、**AtlantisのHelm チャートを使った Terraform のCI/CD 環境構築**について説明する。

## この記事の作成に至った動機付けと背景
現在、私の携わっている案件では、GCP 環境で Terraform を手動で適用しており、作業負担や適用ミスのリスクが課題となっている。
さらに、以下のような問題もある。
- 商用環境では、適用当日にしか権限が付与されないため、事前に ``terraform plan`` の結果を確認できない
- ``terraform plan`` や ``terraform apply`` の結果を手動でコピー＆ペーストし、証跡管理を行っている（手間がかかる&証跡がPR内で完結しない）
- Terraform の適用が個人のローカル環境で実行されるため、チーム全体の可視性が低い

そこで、Terraform の適用を自動化するツールを検討した結果、Atlantis を利用することで、PR ベースのワークフローによる安全な適用が可能になると考えた。

現行の手動運用の流れ（問題点）
　1. GitHub に Push
　2. GitHub からクローン
　3. ローカル環境で ``terraform plan`` を実行（手動ミスのリスク）
　4. 期待した結果が得られたら PR 作成
　5. ローカル環境で ``terraform apply`` を実行（ログ管理が手間）
　6. PR を main ブランチにマージ

Atlantis を導入することで、手動適用の負担やミスのリスクを軽減し、安全な Terraform の適用が可能になると考えた。

## Atlantisとは
**Atlantis は、GitHub や GitLab のプルリクエストをトリガーに Terraform の plan や apply を実行できるOSSツール。**
特徴：
- PR 作成時に ``terraform plan`` を自動実行し、結果を PR 上にコメントとして出力
- 承認後に PR 上で ``atlantis apply`` のコメントを記入することで ``terraform apply`` を実行
- 手動での ``terraform apply`` 実行を防ぎ、Terraform の適用の一貫性を確保
- Terraform の状態管理は Google Cloud Storage（GCS） などに保存可能

本記事では、Atlantis のHelm チャートを使用する。Helmチャートとは、再利用が可能なKubernetesリソースの一式を、所定の仕様でパッケージングしたファイルである。Helm チャートを使うことで、Kubernetes クラスタ上に 簡単に Atlantis をデプロイ・管理 でき、アップグレードや設定変更も柔軟に行える。
デフォルトの設定値は[Artifact Hub](https://artifacthub.io/packages/helm/atlantis/atlantis)に記載されているが、より詳細な設定やカスタマイズの方法については、[GitHub の values.yaml](https://github.com/runatlantis/helm-charts/blob/main/charts/atlantis/values.yaml) を実際に確認しながら構築を進めていく。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4000068/46382bd4-42c3-55ff-2fcd-72fdaf820ea7.png)
### Artifact Hub のデフォルト設定値
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4000068/8d72c82f-f443-9236-dc83-728e131d59dd.png)
### GitHub の ``values.yaml``
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4000068/b5519434-b6d3-ca9e-aeb6-d293bb7d756d.png)


## アーキテクチャ
全体像は以下の通り。
![スクリーンショット 2025-02-07 044945.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4000068/be9faa4e-77e1-a1bb-22f2-adf8007be0b4.png)


１. GitHub からGKEクラスター 上に構築した Atlantis に Webhook でリクエスト
２. Atlantis が Terraform を実行する
３. Terraform がGCP上のリソースを apply。状態ファイルはGoogle Cloud Storageバケットに保存。
 Atlantis と Terraform は Helm チャートを利用しており、GKEクラスター上に構築している。

## 構築手順
⚠️ **注意：本記事の手順は検証目的の構成を想定している。本番環境での運用には、セキュリティ対策や可用性を考慮し、適切な設定変更が必要になる。**
前提として、
- GKEクラスターは、Workload Identity を有効化して構築済みであること
- GCSへ状態ファイルを格納する権限を持つGSA（roles/storage.objectAdmin または最小権限 IAMロールが付与 ）が作成済みであること
- コマンドは、GCP の`CloudShell`を利用すること

参考：[Atlantis導入ガイド](https://www.runatlantis.io/docs/deployment.html#kubernetes-helm-chart)

**1．下記コマンドを実行し、RunAtlantis Helm チャートリポジトリを登録する。**
```sh:
helm repo add runatlantis https://runatlantis.github.io/helm-charts
```
このコマンドによって、Helm に ``runatlantis`` というリポジトリを追加し、``runatlantis/atlantis`` という Helm チャートを利用できるようになる。


**2.Helm チャートを設定するディレクトリに移動後、下記コマンドを実行し、設定ファイル`values.yaml`を作成する。**
```sh:
helm inspect values runatlantis/atlantis > values.yaml
```
これによって、Helmチャートのデフォルトの設定値を``values.yaml``として出力される。このファイルを編集していくことで、カスタマイズされたAtlantis の環境を構築できる。

**3.Helm チャートの設定ファイル``values.yaml``を編集し、LoadBalancerを作成する。**
Artifact Hub に記載されているデフォルトの設定値を確認すると、``service.type`` が``NodePort`` であることが確認できた。外部IPを作成し、そこにGitHub から Webhook リクエストを送信し、Terraform の適用を実行するため、``service.type`` を``LoadBalancer`` に変更する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4000068/f077ba1d-765a-2427-984d-652196e37a21.png)

```sh:values.yaml
service:
  annotations: {}
  externalTrafficPolicy: null
  loadBalancerIP: null
  loadBalancerSourceRanges: []
  nodePort: null
  port: 80
  portName: atlantis
  targetPort: 4141
  type: LoadBalancer
```
**4.下記コマンドを実行し、Helm で Atlantis の設定を適用する。**
```sh:
helm upgrade --install -f values.yaml atlantis runatlantis/atlantis
```
コマンドの内容としては、
```sh:
helm upgrade --install -f <設定ファイルのパス> <リリース名> <Chart.yamlを格納するフォルダ>
```
``helm upgrade --install`` は、指定したリリースが存在しない場合は新規作成し、すでに存在する場合は設定を更新する。つまり、初回は ``helm install`` と同じ動作になり、2回目以降は ``helm upgrade`` の動作になる。
そして、このコマンドによって``runatlantis/atlantis`` というHelmチャートから``values.yaml``の設定を適用した``atlantis`` というリリースが作成される。
イメージとしては...
- Helmチャート（``runatlantis/atlantis``）はClass
- Helmリリース（``atlantis``）はClassから生成されたインスタンス
- ``values.yaml``はインスタンス作成時のコンストラクタ引数や設定ファイル

**runatlantis/atlantisというClass をもとに``values.yaml``の設定を適用してatlantis というインスタンスが作成または更新される**、というイメージですね。

**5.下記コマンドを実行し、LoadBalancer の外部IP ( EXTERNAL-IP )を取得する。**
```sh:
kubectl get svc
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4000068/126d8fc0-74f9-1f12-163f-351b5cfedb78.png)

**6.GitHub Webhook の作成**
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4000068/bd682e16-005c-85e5-7381-3ef4b04be8c1.png)

入力内容は以下の通り。今回は簡単な検証のため、 SSL 通信は利用しない。
 ```sh:GitHub Webhookの設定内容
 Payload URL：http://<LBの外部IP>/events
 Content type：application/json
 Secret：<Webhook 署名検証に使用する Secret>
 SSL verification：Disable (not recommended)
 Which events would you like to trigger this webhook?：
 Let me select individual events.
　　・Pull requests：PR 作成時にPlan 実行
　　・Issue comment：atlantis apply などのコメントをトリガーに適用
```

**7.Helm チャートの設定ファイル`values.yaml`にて、対象のGitHubリポジトリの認証情報とWebhook Secret を編集して追加する。また、対象の GitHubリポジトリを`orgAllowlist`に指定する。今回は、`github.com/<org名>/*`と指定することで、 GitHub の指定した org 内の全てのリポジトリに対して、Terraform の適用を許可する設定にする。**

```sh:values.yaml
github:
  hostname: github.com
  secret: <Webhook 署名検証に使用する Secret（GitHub Webhook 設定と同じ値）>
  token: <GitHub認証トークン>
  user: <GitHubユーザー名>
```
```sh:values.yaml
orgAllowlist: github.com/<org名>/*
```
**8 Helm チャートの設定ファイル ``values.yaml`` で、GSA のバインド設定を行う。``annotations`` に roles/storage.objectAdmin を持つ GSA を指定する。**


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4000068/dacb31a2-1257-8829-5fc4-229866f61670.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4000068/c938b2b5-ff26-1c42-112a-fef5cfe70ddb.png)
Artifact Hub と GitHub の ``values.yaml`` を確認し、適切な設定を行う。
Pod が GCP のサービスを利用できるようにするため、KSA と GSA をバインディングする設定に変更する。

```sh:values.yaml
serviceAccount:
  create: true
  mount: true
  name: atlantis-ksa
  annotations:
    iam.gke.io/gcp-service-account: "<roles/storage.objectAdminを付与したGSA名>"
```
**9.下記コマンドを実行し、Helm で Atlantis の設定を適用する。**
```sh:
helm upgrade --install -f values.yaml atlantis runatlantis/atlantis
```

**10.下記コマンドを実行し、 roles/storage.objectAdmin を付与した GSA を自動作成された KSA にバインディングする。**
```sh:
gcloud iam service-accounts add-iam-policy-binding \
    <roles/storage.objectAdminを付与したGSA> \
    --role roles/iam.workloadIdentityUser \
    --member "serviceAccount:<PROJECT>.svc.id.goog[atlantis/atlantis-ksa]"
```
**⚠️ 注意**
本手順では、Terraform の状態ファイルを保存するために GCS へのアクセス権限 (`roles/storage.objectAdmin`) を持つ GSA を KSA にバインディングする。

しかし、Terraform で GCP のリソースをデプロイするには、対象リソース（例: Compute Engine, Cloud SQL, GKEなど）への適切な IAMロールを持つ GSA が別途必要である。

**Terraform 適用時に権限エラーが発生する場合は、デプロイ対象のリソースに適した権限を持つ GSA を作成し、同様に KSA にバインディングしてください。**

**11.下記コマンドを実行し、ポッドを再起動し、設定を確実に反映する。**
```sh:
kubectl delete po -l app=atlantis
```
## Atlantis の動作を確認しながら PR を作成する
PR 作成時の Pod の挙動を確認するため、下記コマンドを実行し、 PR を作成してみる。
```sh:
kubectl logs -f <atlantisのPod名>
```
PR を作成してみると、以下のようにTerraform の ``Plan`` 結果が PR のコメントとして出力された。変更リソースが色付きで表示され、見やすくなっている。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4000068/dc372953-d2ce-0865-5387-5676bd6b10e7.png)
また、CloudShell上のPodのログでも、``terraform plan``を実行していることが確認できた。
```sh:
{"level":"info","ts":"2025-02-04T13:31:03.524Z","caller":"vcs/github_client.go:913","msg":"Updating GitHub Check status for 'atlantis/plan' to 'pending'","json":{"repo":"<リポジトリ名>","pull":"8"}
```
このログは、Atlantis が ``plan`` の結果を GitHub に送信し、PR 上の Checks に反映しようとしていることを示している。

その後、PR のコメントに`atlantis apply -d .`と入力すると、 ``apply`` が正常に実行されたことを確認できた。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4000068/9191ac75-4b8c-7d17-56fe-737f90da44fa.png)
## 構築を終えての感想
他の方のブログでは、多くが GCE インスタンス上に Atlantis を構築していた。
しかし、Helm チャートを活用することで、GKE 上に容易にデプロイできることが分かった。
また、GKE を採用することで、以下のような利点も得られる。
- **可用性・冗長性の向上**
    - マネージド Kubernetes による自動復旧・スケーリング
- **運用負担の軽減**
    - インスタンス管理不要
    - Helm チャートを使った容易なデプロイ

TerraformのCI/CD ツールにはTerraform Cloud やGitHub Actions も選択肢にあるが、実際の案件で活用する場合、それらと比較してAtlantis を導入することで、以下のような大きなメリットがあると感じた。
- **Terraform Cloud よりもコストを抑えられる**（GKEクラスターが既に稼働している環境であれば追加コストが最小限）
- **GitHub Actions よりも環境構築が容易**（Helm チャートを適用するだけで Atlantis をセットアップ可能）

実際の案件では、Terraform の適用を Atlantis で管理し、その後 ArgoCD でマニフェストを適用することで、Terraform と Kubernetes の運用を分離し、効率化を図る。
また、認証情報（GitHubトークンやWebhookシークレット）を Secrets で安全に管理し、Webhook のSSL通信 を適用することで、セキュリティを強化していく。
さらに、Atlantis の自動デプロイをさらに最適化し、Terraform と Kubernetes の統合運用をより安定させていく。

---

⚠️ **本番環境での考慮点**
本記事の手順は **検証環境向け** に作成しており、本番環境での運用には以下のような追加の設定が推奨される。

- **GitHubトークンや Webhookシークレットの Secret 化**
  - `values.yaml` に認証情報を直接記載せず、Kubernetes Secret を利用する
- **SSL 対応**
  - Webhook の SSL 設定を有効化し、HTTPS を使用することでセキュリティを強化する
- **IAM ロールの最小権限化**
  - Workload Identity の IAM ロールを最小限に絞り、不要な権限を付与しない

## 本記事の内容をさらに発展させ、ArgoCD を活用して Helm チャートを自動適用する方法を以下の記事で解説しています。ぜひご覧ください！
[PRベースでTerraformを適用！Atlantis × Helm × GKEでCI/CD環境を構築するPart2　HelmチャートをArgoCDで適用しよう！](https://qiita.com/ILOVERAMEN/items/66caf41ee52f43e0c462)