GitHub ActionsでGCPにTerraformでインフラCI/CDする

# 本稿について

2019年11月、[GitHub](https://github.com/)上で利用できる無料のワークフローツールの[GitHub Actions](https://github.com/features/actions)が正式にリリースされました。[^1]
これを使って、CI/CDなどの処理を自動化することができます。

[^1]: https://github.blog/jp/2019-11-14-universe-day-one/

本稿では、GitHub Actionsで[Terraform](https://www.terraform.io/)を実行し、[Google Cloud Platform](https://cloud.google.com/)の構成管理を行う方法を紹介します。
また、GitOpsによるインフラCI/CDの作業フローも紹介します。

昨日、[Bitbucket PipelinesでGCPに対してTerraformでインフラCI/CDする - Qiita](https://qiita.com/progrhyme/items/ed96d42dd811063a24c2)という記事を書きましたが、そのGitHub Actions版となります。
共通する内容が多いので、以降ではその記事を「Bitbucke Pipelines版」として参照させて頂きます。

## 更新履歴

- 20200504 `.github/workflows/terraform-apply.yml` で、 `terraform apply` 失敗時にDeployment失敗通知が飛ばなかった問題を修正
- 20200503 初稿投稿

# 動作確認環境

- terraform v0.12.24
- [terraform-provider-google](https://github.com/terraform-providers/terraform-provider-google/) v3.19.0

# 準備するもの

- GitHubアカウント
- GitHub上のリポジトリ ... TerraformのコードとGitHub Actionsのワークフロー設定ファイルを入れる
- GCPプロジェクト
  - Service Account
     - GitHub Actions内で実行するTerraformで利用する
     - Terraformで行う操作に必要なCloud IAMの権限をつける
     - 今回の構成では、少なくともGCSへの読み書き権限が必要
  - GCSバケット ... TerraformのBackendとして利用し、tfstateを保存する
  - 必要なAPIを有効化する
     - 例）GCEインスタンスの操作には、Compute Engine APIの有効化が必要

※Bitbucket Pipelines版から、Bitbucket→GitHubになっただけ

## 推奨開発環境

[Bitbucket Pipelines版](https://qiita.com/progrhyme/items/ed96d42dd811063a24c2#推奨開発環境)と同様です。

# セットアップ手順

1. Terraform用のService AccoutのService Account KeyをJSON形式で生成し、保存する
2. 1. をGitHubリポジトリのSecretsに設定する
  - キー名は任意だが、ここでは `GOOGLE_CREDENTIALS` とする
  - 値のBase64化は不要
3. リポジトリの `.github/workflows/` に、下のような2つのYAMLファイルを用意する

## PR時にterraform fmt/validate/planするワークフロー

HashiCorpが提供している[hashicorp/terraform-github-actions](https://github.com/hashicorp/terraform-github-actions)アクションを使います。

```YAML:.github/workflows/terraform-plan.yml
name: terraform plan

# プルリクエストのOpen、更新、Reopen時にトリガー
on: pull_request

env:
  tf_version: '0.12.24'
  tf_work_dir: '.'
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}

jobs:
  plan:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: terraform fmt
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.tf_version }}
          tf_actions_subcommand: fmt
          tf_actions_working_dir: ${{ env.tf_work_dir }}

      - name: terraform init
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.tf_version }}
          tf_actions_subcommand: init
          tf_actions_working_dir: ${{ env.tf_work_dir }}

      - name: terraform validate
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.tf_version }}
          tf_actions_subcommand: validate
          tf_actions_working_dir: ${{ env.tf_work_dir }}

      - name: terraform plan
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.tf_version }}
          tf_actions_subcommand: plan
          tf_actions_working_dir: ${{ env.tf_work_dir }}
```

このワークフロー設定により、プルリクエストの作成、マージ元ブランチの更新[^2]、プルリクエストのReopenをトリガーとして、 `terraform fmt` → `terrafom init` → `terrafom validate` → `terrafom plan` を順番に実行します。

[^2]: 正確には `synchronize` というevent typeなのですが、どういうイベントなのかちゃんと調べていません（汗）参考: https://help.github.com/en/actions/reference/events-that-trigger-workflows#pull-request-event-pull_request

## PRマージ時にterraform plan/applyするワークフロー

上と同様に、[hashicorp/terraform-github-actions](https://github.com/hashicorp/terraform-github-actions)アクションを使います。

また、[bobheadxi/deployments](https://github.com/bobheadxi/deployments)アクションを用いて、 `terraform apply` を実行する時に合わせて[Deployment](https://developer.github.com/v3/repos/deployments/)を作成します。

```YAML:.github/workflows/terraform-apply.yml
name: terraform apply

on:
  pull_request:
    branches:
      - master
    types: [closed]
env:
  tf_version: '0.12.24'
  tf_work_dir: '.'
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }}

jobs:
  apply:
    runs-on: ubuntu-latest
    if: ${{ github.event.pull_request.merged == true }}

    steps:
      - uses: actions/checkout@v2

      - name: terraform init
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.tf_version }}
          tf_actions_subcommand: init
          tf_actions_working_dir: ${{ env.tf_work_dir }}

      - name: terraform plan
        id: plan
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.tf_version }}
          tf_actions_subcommand: plan
          tf_actions_working_dir: ${{ env.tf_work_dir }}

      # Deploymentを開始する
      - name: Start Deployment
        # terraform planで差分があるときのみ実行
        if: ${{ steps.plan.outputs.tf_actions_plan_has_changes == 'true' }}
        uses: bobheadxi/deployments@master
        id: deployment
        with:
          step: start
          token: ${{ secrets.GITHUB_TOKEN }}
          env: production

      - name: terraform apply
        # terraform planで差分があるときのみ実行
        if: ${{ steps.plan.outputs.tf_actions_plan_has_changes == 'true' }}
        uses: hashicorp/terraform-github-actions@master
        with:
          tf_actions_version: ${{ env.tf_version }}
          tf_actions_subcommand: apply
          tf_actions_working_dir: ${{ env.tf_work_dir }}

      # Deploymentを終了する
      - name: Finish Deployment
        uses: bobheadxi/deployments@master
        # terraform planで差分があるときのみ実行
        if: ${{ always() && steps.plan.outputs.tf_actions_plan_has_changes == 'true' }}
        with:
          step: finish
          token: ${{ secrets.GITHUB_TOKEN }}
          status: ${{ job.status }}
          deployment_id: ${{ steps.deployment.outputs.deployment_id }}
```

このワークフロー設定により、masterブランチへのプルリクエストのマージをトリガーとして、 `terrafom init` → `terraform plan` → `terraform apply` を順番に実行できます。
また、 `terraform plan` を実行して差分がない場合、 `terraform apply` の実行、及びDeploymentの作成は行いません。

Deploymentの作成タスクはインフラCI/CDの作業フロー上で本質的な部分ではないので、省いても構いません。とはいうものの、[GitHubのSlackインテグレーション](https://slack.github.com/)で通知ができるのと、デプロイ履歴が見れるので便利だと思います。

以上のワークフロー設定により、インフラコードをGitで管理し、プルリクエストで変更をレビューしてCI/CDで適用する、といった流れのGitOpsが実現できます。
実際にどのような挙動になるかは、作業フローと合わせて後述の「デモ」の節で見ていきます。

### 5/4追記: デプロイ失敗通知が飛ばなかった問題を修正

上の `.github/workflows/terraform-apply.yml` の `Finish Deployment` のタスクを次のように修正しました。

```diff:.github/workflows/terraform-apply.yml
       :
       # Deploymentを終了する
       - name: Finish Deployment
         uses: bobheadxi/deployments@master
         # terraform planで差分があるときのみ実行
-        if: ${{ steps.plan.outputs.tf_actions_plan_has_changes == 'true' }}
+        if: ${{ always() && steps.plan.outputs.tf_actions_plan_has_changes == 'true' }}
         with:
           step: finish
           token: ${{ secrets.GITHUB_TOKEN }}
           status: ${{ job.status }}
           deployment_id: ${{ steps.deployment.outputs.deployment_id }}
```

ワークフローでタスクが失敗すると、デフォルトでは後続のタスクはスキップされます。
この仕様により、修正前の設定では `terraform apply` が失敗したときに `Finish Deployment` タスクがスキップされていました。

`if` の条件に `always()` を加えることで、当該ステップを常に評価することができます。

ドキュメント「[Context and expression syntax for GitHub Actions - GitHub Help#always](https://help.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always)」に次のように記載されています:

> A job or step will not run when a critical failure prevents the task from running. For example, if getting sources failed.

## 補足①GCPへの認証設定について

[Bitbucket Pipelines版](https://qiita.com/progrhyme/items/ed96d42dd811063a24c2#gcpへの認証設定について53編集)を参照して下さい。

## 補足②ブランチ更新でワークフローを実行する場合

例えば、上記のYAMLで `on:` のブロックを以下のように書き換えれば、所望のブランチでワークフローを実行できます。

```YAML
# masterブランチへの更新をトリガーとする場合
on:
  push:
    branches:
      - master

---
# masterブランチ以外の更新をトリガーとする場合
on:
  push:
    branches-ignore:
      - master
```

このような設定でも、元のYAMLによるプルリクエストをトリガーとする場合と同じようなインフラCI/CDの作業フローを実現できます。
ただ、[hashicorp/terraform-github-actions](https://github.com/hashicorp/terraform-github-actions)アクションを使う場合、プルリクエストに対して `terraform fmt|validate|plan|apply` の結果をコメントで通知してくれるので、プルリクエスト駆動のフローを組んだ方が便利だと思います。

## 参考文書等のリンク

- https://www.terraform.io/docs/github-actions/
- https://help.github.com/en/actions
  - [Workflow syntax for GitHub Actions - GitHub Help](https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions)
  - [Using variables and secrets in a workflow - GitHub Help](https://help.github.com/en/actions/configuring-and-managing-workflows/using-variables-and-secrets-in-a-workflow)
- [GitHub Actions覚え書き - Qiita#プルリクエストで実行する](https://qiita.com/progrhyme/items/56c24b3731deffcd4481#%E3%83%97%E3%83%AB%E3%83%AA%E3%82%AF%E3%82%A8%E3%82%B9%E3%83%88%E3%81%A7%E5%AE%9F%E8%A1%8C%E3%81%99%E3%82%8B)

# デモ: GitOpsによるインフラCI/CD

上で示したプルリクエスト駆動のワークフローを設定した状態で、インフラCI/CDの作業フローがどのようになるかを見ていきます。

## 0. 下準備

先に示した事前準備とセットアップ手順に加えて、以下を行っています:

- Terraformでテスト用のService Accountを作成するため、GCPプロジェクトでCloud Resource Manager APIを有効化しています
- [GitHubのSlackインテグレーション](https://slack.github.com/)をリポジトリに設定し、Slack通知を有効にしています
  - featureとして、 `issues`, `pulls`, `deployments`, `statuses`, `public`, `releases` をsubscribeしました（デフォルトから `commits` のみunsubscribe）

## 1. topicブランチを作成し、コード変更をプッシュ

1. `topic/add-test-sa` というブランチを作成します
1. .tfファイルに `google_service_account` リソースを定義します
  - 実はこのコードには敢えて誤りを含め、 `terraform fmt` でも差分が出るようにしています
1. GitHubにブランチをプッシュします。

```sh
% git checkout -b topic/add-test-sa
% $EDITOR main.tf
% git diff
diff --git a/main.tf b/main.tf
index b6bb61a..d5c0613 100644
--- a/main.tf
+++ b/main.tf
@@ -31,3 +31,7 @@ resource "google_project_iam_member" "sa_terraform" {
   role    = "roles/owner"
   member  = "serviceAccount:${google_service_account.terraform.email}"
 }
+
+resource "google_service_account" "test" {
+  account_id    =    "test"
+}
% git add .
% git commit -m "Add test service account"
% git push origin topic/add-test-sa
```

## 2. プルリクエストの作成

GitHub画面上でプルリクエストを作成します。

![Screenshot from 2020-05-03 20-21-37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/207d0321-d7d5-bbe5-852a-28b60d95a33b.png)

プルリクエストの作成により、 `.github/workflows/terraform-plan.yml` で設定したワークフローがトリガーされます。

## 3. ビルド結果（失敗）の確認①

プルリクエスト画面上で、 `terraform fmt` に失敗したことが確認できました。

![Screenshot from 2020-05-03 19-19-50_crop.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/65a16d5f-e3e5-6106-db11-1de8d1f22d66.png)

`./main.tf` 行を展開すると、fmt適用時の差分が確認できます。

![Screenshot from 2020-05-03 19-20-31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/c3a27ee4-9682-781e-c594-429e89bf8fb1.png)

プルリクエスト画面の「×」アイコンや「Details」のリンクをクリックすると、ワークフローの詳細な実行ログを確認することができます。

![Screenshot from 2020-05-03 19-22-41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/06b0c5c2-101e-22a5-1b99-39b63b4f2e11.png)

`terraform fmt` に失敗しているので、その後の `terraform {init,validate,plan}` のタスクがスキップされていることがわかります。

## 4. コードを修正して再プッシュ①

ローカルで `terraform fmt` を適用して、ブランチを再プッシュします。

```sh
% terraform fmt
main.tf
% git diff
diff --git a/main.tf b/main.tf
index 7ee15f7..d5c0613 100644
--- a/main.tf
+++ b/main.tf
@@ -33,5 +33,5 @@ resource "google_project_iam_member" "sa_terraform" {
 }
 
 resource "google_service_account" "test" {
-  account_id    =    "test"
+  account_id = "test"
 }
% git add .
% git commit -m "terraform fmt"
% git push origin topic/add-test-sa
```

プルリクエストのマージ元ブランチの更新により、 `.github/workflows/terraform-plan.yml` で設定したワークフローがトリガーされます。

## 5. ビルド結果（失敗）の確認②

次は `terraform validate` が失敗してしまいました。

![Screenshot from 2020-05-03 19-26-13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/4cc52888-93af-35c4-a5d2-7fe7c7887337.png)

Bitbucket Pipelines版のデモと同様ですが、`google_service_account.test.account_id` のバリデーションでエラーになっています。

今回、 `terraform fmt` では差分がなかったので、コメントは付いていません。

先ほどはキャプチャを撮り損ねましたが、Slackにも通知が来ています。

![Screenshot from 2020-05-03 19-26-48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/6053fef8-d2ec-2350-8b73-2d9ffb9edf02.png)

## 6. コードを修正して再プッシュ②

`google_service_account.test.account_id` をバリデーションが通るように修正して、ブランチを再プッシュします。

```sh
% $EDITOR main.tf
% git diff
diff --git a/main.tf b/main.tf
index d5c0613..fd9a3ec 100644
--- a/main.tf
+++ b/main.tf
@@ -33,5 +33,5 @@ resource "google_project_iam_member" "sa_terraform" {
 }
 
 resource "google_service_account" "test" {
-  account_id = "test"
+  account_id = "test01"
 }
% git add .
% git commit -m "Fix test service account id"
% git push origin topic/add-test-sa
```

## 7. ビルド結果（成功）の確認

ようやく `terraform plan` まで成功しました。
プルリクエストに届いたコメントで差分が確認できます。

![Screenshot from 2020-05-03 19-31-35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/453be4ce-6b54-18a1-a845-661f47707e0e.png)

`terraform fmt|validate` ではエラーは出ていないので、今回それらのコメントは付いていません。

## 8. プルリクエストのマージ

上で差分を確認し、問題ないのでマージします。

masterブランチへのマージによって、 `.github/workflows/terraform-apply.yml` で設定したワークフローがトリガーされます。

## 9. デプロイ結果の確認

デプロイの開始時と完了時にSlackに通知が来ます。（下は完了後）

![Screenshot from 2020-05-03 19-36-08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/d3888866-67e0-cce9-9ab9-2c539adffe14.png)

また、プルリクエストにも `terraform {plan,apply}` の結果がコメントで付加されます。

![Screenshot from 2020-05-03 19-35-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/03dce15e-fad0-1fe6-62bf-f3f54e7f4e79.png)

最後に、Deployments Dashboardを見ておきましょう。[^3]
リポジトリのメニューの「:rocket:environment」のリンクから見ることができます。

[^3]: [Viewing deployment activity for your repository - GitHub Help](https://help.github.com/en/github/administering-a-repository/viewing-deployment-activity-for-your-repository)によれば、2020-05-03現在、このダッシュボードはβ版だそうです。

![Screenshot from 2020-05-03 19-38-25_mark.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/d28e15ba-d820-625d-8978-b76bc3c8a42b.png)

クリックすると、次のような画面が確認できます。

![Screenshot from 2020-05-03 19-38-45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168843/573cafc8-0080-fc42-9365-fd8879745dd2.png)

以上で、デプロイが成功したことが確認できました :tada: 

# まとめと所感

GitHub ActionsでTerraformを実行し、GCP環境のインフラCI/CDを行う方法と、GitOpsによる作業フローのイメージを紹介しました。

今回は、Bitbucket Pipelines版から設定を移植したので、基本的な部分はすぐにできたのですが、CI/CDフローを作り込む過程で、意外にBitbucket Pipelinesにある機能がGitHub Actionsにないことに気付きました。[^4]

[^4]: 本稿の設定を組む上で気づいたのは、YAMLアンカー、ビルドの手動トリガー、ビルド結果のSlack通知（サードパーティー製のアクションを使えばできる）。参考: [GitHub Actions覚え書き - Qiita#既知の制限事項](https://qiita.com/progrhyme/items/56c24b3731deffcd4481#%E6%97%A2%E7%9F%A5%E3%81%AE%E5%88%B6%E9%99%90%E4%BA%8B%E9%A0%85)

とはいえ、Bitbucket Pipelines側で（自分が）実現できていない機能もあるので、一長一短はあると思います。[^5]

[^5]: プルリクエストにコメントを付けるのが特に難しそうな気がしています。

# 脚注