GitHub ActionsでTerraformの実行を自動化する

# どんな感じに自動化したいか

- Pull Request作成時にterraform planを実行し、その結果をPRにコメントしたい。
- Pull Requestをマージしたときにterraform applyを実行したい。
- tfstateをいい感じに分けたい。
- 変更した部分だけterraformを実行したい。

# 実際のコードはこちら

- https://github.com/rinchsan/terraform-github-actions
- ECRのリポジトリを1つ作るだけのシンプルな例です。
- 公式が出している、GitHub ActionsでTerraformを自動化するチュートリアルは [こちら](https://learn.hashicorp.com/tutorials/terraform/github-actions) にあります。

# 解説

## `plan.yml`

- planはPR作成時や変更時にGitHub Actionsを実行しましょう。

```yaml
name: Plan
on:
  pull_request:
    branches:
      - master
```

---

- `strategy.matrix` に何か配列を指定すると、それぞれを使って並列にジョブを実行することができます。
- 1つの `tfstate` ファイルに対応するディレクトリごとに `strategy.matrix.dir` に追記していくイメージです。
    - Dev環境、Stg環境などの環境ごとに `resources/dev/` 、 `resources/stg/` などに分割して `strategy.matrix.dir` に追加していくのがよくありそうです。
    - `resources/dev/` 配下でさらに `resources/dev/api/` とか `resources/dev/worker/` みたいな感じでドリルダウンしていくのも良さそうです。
    - `resources/dev/ecs/` とか `resources/dev/rds/` みたいにリソースの種類ごとに分けるのもアリかもです。

```yaml
jobs:
  plan:
    name: Plan
    runs-on: ubuntu-latest
    strategy:
      matrix:
        dir: [
          resources/shd/ecs,
        ]
```

---

- とりあえずCheckoutしてきて、 [technote-space/get-diff-action](https://github.com/technote-space/get-diff-action) を使ってmasterとの差分を取得しましょう。
    - 自分でgit diffするように実装してもいいと思うんですが、面倒ですよね。
- ここで取得した差分が存在するかどうかによって、 `matrix.dir` ごとに `terraform plan` を実行するかどうかを後続のstepで判断します。
    - `id` に何か文字列を指定しておくと、後続のstepで実行結果などを参照することができます。
- 先ほど指定していた `strategy.matrix.dir` は `${{ matrix.dir }}` みたいな感じで参照することができます。

```yaml
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Check diff
        id: diff
        uses: technote-space/get-diff-action@v4.0.2
        with:
          PATTERNS: |
            modules/**/*.tf
            ${{ matrix.dir }}/**/*.tf
```

---

- 今回はAWSを想定しているのでAWSの認証を行いましょう。
- ここでは普通にIAM Userのキーペアを使っていますが、会社のセキュリティ事情によってAssumeRoleとかでSessionTokenを取得しないといけなかったりする場合は、頑張ってください。

```yaml
      - name: Configure aws credentials
        if: steps.diff.outputs.diff
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1
```

---

- Hashicorp公式が提供している [hashicorp/setup-terraform](https://github.com/hashicorp/setup-terraform) を使って `terraform` コマンドをインストールしましょう。
- `if` で `steps.diff.outputs.diff` を指定することで、 `Check diff` stepで差分が得られたときのみ実行するようにします。

```yaml
      - name: Setup terraform
        if: steps.diff.outputs.diff
        uses: hashicorp/setup-terraform@v1.2.1
        with:
          terraform_version: 0.13.2
```

---

- `terraform fmt` でフォーマットをチェックしておきます。
- フォーマットはPRをマージするまでに直ればいいので、ここでは `continue-on-error: true` を指定して後続のstepは実行します。

```yaml
      - name: Check format
        id: fmt
        if: steps.diff.outputs.diff
        run: terraform fmt -check -recursive
        working-directory: ${{ matrix.dir }}
        continue-on-error: true
```

---

- `${{ matrix.dir }}` で `terraform init` を実行しましょう。
- 今回のサンプルではTerraformのBackend設定やProvider設定などを書いた `init.tf` をディレクトリごとに用意する想定ですが、このファイルを共通化したい場合はリポジトリルートに `init/init.tf` などを用意して、 `${{ matrix.dir }}` にコピーしてから `terraform init` を実行したりするとよいでしょう。
    - `${{ matrix.dir }}` を元に `tfstate` の `key` を指定することもお忘れなく。

```yaml
      - name: Initialize
        id: init
        if: steps.diff.outputs.diff
        run: terraform init
        working-directory: ${{ matrix.dir }}
```

---

- `terraform get` で依存モジュールのダウンロードなどを行い、 `terraform validate` で設定を検証しましょう。

```yaml
      - name: Download modules
        if: steps.diff.outputs.diff
        run: terraform get
        working-directory: ${{ matrix.dir }}

      - name: Validate
        if: steps.diff.outputs.diff
        run: terraform validate
        working-directory: ${{ matrix.dir }}
```

---

- `terraform plan` を実行しましょう。
- `terraform plan` で `-no-color` オプションを指定しているのは、後続のstepでPRにコメントを残す際に表示がおかしくなるのを防ぐためです。

```yaml
      - name: Plan
        if: steps.diff.outputs.diff
        id: plan
        run: terraform plan -no-color
        working-directory: ${{ matrix.dir }}
        continue-on-error: true
```

---

- [actions/github-script](https://github.com/actions/github-script) を使って `terraform fmt` や `terraform plan` の結果をPRにコメントしてもらいましょう。
- いちいちGitHub Actionsの実行結果ページへ行く必要がないので楽ですね。
- このスクリプトは上述の公式チュートリアルにあるものを拝借しています。

```yaml
      - name: Comment
        if: steps.diff.outputs.diff
        uses: actions/github-script@v3.0.0
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `## \`${{ matrix.dir }}\`
            #### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
            #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

            <details><summary>Show Plan</summary>

            \`\`\`${process.env.PLAN}\`\`\`

            </details>`;

            github.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })
```

## `apply.yml`

- `terraform apply` はmasterブランチへのPush時に実行しましょう。
- `strategy` まわりはplanと同様です。

```yaml
name: Apply

on:
  push:
    branches:
      - master

jobs:
  apply:
    name: Apply
    runs-on: ubuntu-latest
    strategy:
      matrix:
        dir: [
          resources/shd/ecs,
        ]
```

---

- applyでは最新のPRの差分を見て実行するかどうかを判断したいので、 `ref` と `fetch-depth` を指定します。
- `actions/checkout@v2` はデフォルトでは最新のコミット、つまり今回の場合はPRのマージコミットしかfetchしてくれないため、 `HEAD^` と `HEAD` の差分を取得するために `fetch-depth: 2` を指定します。

```yaml
    steps:
      - name: Checkout
        uses: actions/checkout@v2
        with:
          ref: ${{ github.event.pull_request.head.sha }}
          fetch-depth: 2
```

---

- `terraform` コマンドのインストールとAWSの認証はplanと同様に行います。

```yaml
      - name: Setup terraform
        uses: hashicorp/setup-terraform@v1.2.1
        with:
          terraform_version: 0.13.2

      - name: Configure aws credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1
```

---

- 最後に `HEAD^` との `git diff` を取って、 `resources/` もしくは `modules/` に変更があった場合のみ、 `terraform apply` を実行します。
- 小さいですがあまりスクリプトは書きたくないなと思うので、いい感じに差分を取得できるアクションを書いてみたいですね。
- この結果はGitHub Actionsの実行結果ページに行って確認しましょう。
    - もし失敗していたら新たに修正ブランチをmasterから切ってそのPRをマージする形で対応しましょう。

```yaml
      - name: Apply
        run: |
          DIFF=$(git diff --name-only HEAD^ modules ${{ matrix.dir }})
          if [[ ${DIFF} = *resources* || ${DIFF} = *modules* ]]; then
            cd ${{ matrix.dir }}
            terraform init
            terraform get
            terraform apply -auto-approve
          fi
        shell: bash
```

# 感想

Orb未対応のCircleCIとかで頑張ってスクリプトを書いて実現していたのをある程度キレイに書けていい感じです。