Github ActionsでTerraformの実践的なCI/CDを構築する

# はじめに
TerraformのCI/CDを構築し、普段アプリケーション開発で行っているフローを踏襲することで、Terraformのコードの品質を担保することができます。
この記事では、チームで開発することを想定して構築したTerraformの実践的なCI/CDについて書こうと思います。

構築する際は以下のことを考慮しました。
- コードの変更があったディレクトリでのみplanを実行する
- tfsec, tflintなどでコードのチェックを行う
- プルリクエストでplan結果やチェックで発生したエラーを確認できるようにする
- プラグインをキャッシュして高速化する

Githubに今回構築するCI/CDを含む簡単なサンプルプロジェクトをあげているので、こちらも参考にしてください。

https://github.com/tak23753/sample-terraform

Terraformのディレクトリ構成については前回投稿したこちらの記事を参照してください。

https://qiita.com/tak0203753/items/0d9b8bb2d5cc9b6f84b4

# 前提
- 環境
開発環境(dev)と本番環境(prod)の二つを構築
- 運用フロー
ブランチ戦略はgit-flowに近い形にしています。
mainブランチからdevelopブランチを作成し、developブランチからfeatureブランチを作成し、開発はfeatureブランチで行います。
開発時のフローは以下のようなものを想定しています。
①featureブランチで実装
②featureブランチからdevelopブランチにプルリクを作成
③開発環境に対してplanを実行
④問題がなければプルリクをマージ
⑤開発環境に対してapplyを実行
⑥developブランチからmainブランチにプルリクを作成
⑦本番環境に対してplanを実行
⑧問題がなければプルリクをマージ
⑨本番環境に対してapplyを実行




# 使用するツール

| ツール名                                         | 概要                                                                                 |
|-------------------------------------------------|--------------------------------------------------------------------------------------|
| [Terragrunt](https://terragrunt.gruntwork.io/)  | Terragruntは、Terraformの設定を簡素化し、共有可能なコードモジュールを再利用するためのツールです。Terraformのベストプラクティスをサポートします。|
| [tfsec](https://tfsec.dev/)                     | tfsecは、Terraformのセキュリティ設定をスキャンして、潜在的なセキュリティリスクを見つけるための静的解析ツールです。                      |
| [tflint](https://github.com/terraform-linters/tflint)    | tflintは、Terraformコードの潜在的な問題を検出するためのコードチェッカーです。シンタックスとベストプラクティスに対してチェックを行います。 |
| [reviewdog](https://github.com/reviewdog/reviewdog)      | reviewdogは、コードレビュー時に静的解析ツールの出力をフィードバックするツールです。多くの言語とツールに対応しています。             |
| [tfcmt](https://github.com/suzuki-shunsuke/tfcmt)    | tfcmt は terraform plan, apply の結果を GitHub の Pull Request にコメントとして通知する CLI ツールです。                                                         |
| [aqua](https://aquaproj.github.io/)                      | aquaは、CLI ツールを YAML でバージョン管理できるツールです。 |


# planワークフロー

<details><summary>planワークフローの全体はこちらから確認してください</summary><div>

```yaml:plan.yml
name: Terraform plan
on:
  pull_request:
    branches:
      - main
      - develop
    types:
      - opened
      - synchronize

env:
  AWS_REGION: ${{ vars.AWS_REGION }}
  SYSTEM: ${{ vars.SYSTEM }}
  DEV_AWS_ACCOUNT_ID: ${{ secrets.DEV_AWS_ACCOUNT_ID }}
  PROD_AWS_ACCOUNT_ID: ${{ secrets.PROD_AWS_ACCOUNT_ID }}

permissions:
  contents: read
  id-token: write
  pull-requests: write
  actions: read

jobs:
  notify-started:
    uses: ./.github/workflows/_notify_started.yml
    secrets: inherit

  setup:
    runs-on: ubuntu-latest
    outputs:
      modules_changed_dirs: ${{ steps.modules_changes.outputs.changes }}
      envs_changed_dirs: ${{ steps.filter_changed_envs_dirs.outputs.envs_changed_dirs}}
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Get changed modules dirs
        uses: dorny/paths-filter@v2
        id: modules_changes
        with:
          filters: .github/modules-path-filter.yml

      - name: Get changed envs dirs
        uses: dorny/paths-filter@v2
        id: envs_changes
        with:
          filters: .github/envs-path-filter.yml

      - name: Filter changed envs dirs
        id: filter_changed_envs_dirs
        run: |
          dirs=${{ toJSON(steps.envs_changes.outputs.changes) }}
          if [ ${{ github.base_ref }} == 'main' ]; then
            env_type='prod'
          elif [ ${{ github.base_ref }} == 'develop' ]; then
            env_type='dev'
          else
            echo "Unsupported base_ref: ${{ github.base_ref }}" >&2
            exit 1
          fi

          env_changed_dirs=$( echo "${dirs}" | jq '.[]' | grep $env_type | jq -sc )
          echo "envs_changed_dirs=${env_changed_dirs}" >> $GITHUB_OUTPUT

  modules-ci:
    needs: setup
    if: needs.setup.outputs.modules_changed_dirs != '[]'
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        changed_dir: ${{ fromJson(needs.setup.outputs.modules_changed_dirs) }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup aqua
        uses: aquaproj/aqua-installer@v2.1.2
        with:
          aqua_version: v2.9.0
          aqua_opts: ""

      - name: Set env vars for dev
        if: github.base_ref == 'develop'
        run: |
          echo "ENVIRONMENT=dev" >> $GITHUB_ENV
          echo "AWS_ACCOUNT_ID=$DEV_AWS_ACCOUNT_ID" >> $GITHUB_ENV

      - name: Set env vars for prod
        if: github.base_ref == 'main'
        run: |
          echo "ENVIRONMENT=prod" >> $GITHUB_ENV
          echo "AWS_ACCOUNT_ID=$PROD_AWS_ACCOUNT_ID" >> $GITHUB_ENV

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1-node16
        with:
          role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-role
          role-session-name: ${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-session
          aws-region: ${{ env.AWS_REGION }}

      - name: TFlint
        working-directory: modules/${{ matrix.changed_dir }}
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          tflint --config $GITHUB_WORKSPACE/.tflint.hcl --init
          tflint --config $GITHUB_WORKSPACE/.tflint.hcl --format=checkstyle | \
          reviewdog -f=checkstyle \
                    -name="tflint" \
                    -reporter=github-pr-review \
                    -filter-mode=nofilter \
                    -fail-on-error \

      - name: Check Terraform fmt
        working-directory: modules/${{ matrix.changed_dir }}
        run: terraform fmt -check

  envs-ci:
    needs: setup
    if: needs.setup.outputs.envs_changed_dirs != '[]'
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        changed_dir: ${{ fromJson(needs.setup.outputs.envs_changed_dirs) }}
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup aqua
        uses: aquaproj/aqua-installer@v2.1.2
        with:
          aqua_version: v2.9.0
          aqua_opts: ""

      - name: Set env vars for dev
        if: github.base_ref == 'develop'
        run: |
          echo "ENVIRONMENT=dev" >> $GITHUB_ENV
          echo "AWS_ACCOUNT_ID=$DEV_AWS_ACCOUNT_ID" >> $GITHUB_ENV

      - name: Set env vars for prod
        if: github.base_ref == 'main'
        run: |
          echo "ENVIRONMENT=prod" >> $GITHUB_ENV
          echo "AWS_ACCOUNT_ID=$PROD_AWS_ACCOUNT_ID" >> $GITHUB_ENV

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1-node16
        with:
          role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-role
          role-session-name: ${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-session
          aws-region: ${{ env.AWS_REGION }}

      - name: Config Terraform plugin cache
        run: |
          echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' >~/.terraformrc
          mkdir --parents ~/.terraform.d/plugin-cache

      - name: Cache Terraform Plugins
        uses: actions/cache@v3
        with:
          path: ~/.terraform.d/plugin-cache
          key: ${{ runner.os }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}
          restore-keys: |
            ${{ runner.os }}-terraform-

      - name: Terragrunt init
        working-directory: envs/${{ matrix.changed_dir }}
        run: |
          terragrunt init --terragrunt-non-interactive

      - name: TFsec
        working-directory: envs/${{ matrix.changed_dir }}
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          tfsec --format=checkstyle | \
          reviewdog -f=checkstyle \
                    -name="tfsec" \
                    -reporter=github-pr-review \
                    -filter-mode=nofilter \
                    -fail-on-error \

      - name: TFlint
        working-directory: envs/${{ matrix.changed_dir }}
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          tflint --config $GITHUB_WORKSPACE/.tflint.hcl --init
          tflint --config $GITHUB_WORKSPACE/.tflint.hcl --format=checkstyle | \
          reviewdog -f=checkstyle \
                    -name="tflint" \
                    -reporter=github-pr-review \
                    -filter-mode=nofilter \
                    -fail-on-error \

      - name: Check terragrunt fmt
        working-directory: envs/${{ matrix.changed_dir }}
        run: terragrunt fmt -check

      - name: Terragrunt validate
        working-directory: envs/${{ matrix.changed_dir }}
        run: terragrunt validate

      - name: Terragrunt plan
        working-directory: envs/${{ matrix.changed_dir }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          terragrunt plan --terragrunt-tfpath $GITHUB_WORKSPACE/.github/scripts/tfwrapper.sh

  notify-finished:
    if: always()
    needs: [modules-ci, envs-ci]
    uses: ./.github/workflows/_notify_finished.yml
    secrets: inherit


```
</div></details>

上から順に重要そうなポイントを説明していきます。

### トリガー条件

```yaml
on:
  pull_request:
    branches:
      - main
      - develop
    types:
      - opened
      - synchronize
```

ワークフローのトリガーはmain, developブランチに対してプルリクが開かれた時と、プルリクが更新された時に設定しています。

### setupジョブでコードの変更があったディレクトリの一覧を取得

```yaml
setup:
    runs-on: ubuntu-latest
    outputs:
      modules_changed_dirs: ${{ steps.modules_changes.outputs.changes }}
      envs_changed_dirs: ${{ steps.filter_changed_envs_dirs.outputs.envs_changed_dirs}}
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Get changed modules dirs
        uses: dorny/paths-filter@v2
        id: modules_changes
        with:
          filters: .github/modules-path-filter.yml

      - name: Get changed envs dirs
        uses: dorny/paths-filter@v2
        id: envs_changes
        with:
          filters: .github/envs-path-filter.yml

      - name: Filter changed envs dirs
        id: filter_changed_envs_dirs
        run: |
          dirs=${{ toJSON(steps.envs_changes.outputs.changes) }}
          if [ ${{ github.base_ref }} == 'main' ]; then
            env_type='prod'
          elif [ ${{ github.base_ref }} == 'develop' ]; then
            env_type='dev'
          else
            echo "Unsupported base_ref: ${{ github.base_ref }}" >&2
            exit 1
          fi

          env_changed_dirs=$( echo "${dirs}" | jq '.[]' | grep $env_type | jq -sc )
          echo "envs_changed_dirs=${env_changed_dirs}" >> $GITHUB_OUTPUT
```

setupジョブの中で[dorny/paths-filter](https://github.com/dorny/paths-filter)というアクションを使用して、変更が加えられたディレクトリを取得します。
あらかじめ対象のファイルと、そのファイルに対するラベルを定義しておくことで、対象のファイルに差分が発生したラベルの一覧を取得することができます。

また、envs配下のディレクトリに関しては、自身のディレクトリだけでなく、依存するモジュールも対象に加えるようにしています。これによってモジュールだけが変更された場合でも適切にplanが実行されるようになります。

<details><summary>envs配下の設定ファイル</summary><div>

```yaml:.github/envs-path-filter.yml
dev/app:
  - 'envs/dev/app/**'
  - 'modules/ecr/**'
  - 'modules/ecs/**'
  - 'modules/cognito_user_pool/**'
  - 'modules/s3/**'
  - 'modules/ses/**'
  - 'modules/lambda/**'

prod/app:
  - 'envs/prod/app/**'
  - 'modules/ecr/**'
  - 'modules/ecs/**'
  - 'modules/cognito_user_pool/**'
  - 'modules/s3/**'
  - 'modules/ses/**'
  - 'modules/lambda/**'

dev/cicd:
  - 'envs/dev/cicd/**'

prod/cicd:
  - 'envs/prod/cicd/**'

dev/log:
  - 'envs/dev/log/**'
  - 'modules/s3/**'
  - 'modules/athena/**'
  - 'modules/lambda/**'

prod/log:
  - 'envs/prod/log/**'
  - 'modules/s3/**'
  - 'modules/athena/**'
  - 'modules/lambda/**'

dev/network:
  - 'envs/dev/network/**'
  - 'modules/vpc/**'

prod/network:
  - 'envs/prod/network/**'
  - 'modules/vpc/**'

dev/operation:
  - 'envs/dev/operation/**'
  - 'modules/cloudtrail/**'
  - 'modules/config/**'

prod/operation:
  - 'envs/prod/operation/**'
  - 'modules/cloudtrail/**'
  - 'modules/config/**'

dev/routing:
  - 'envs/dev/routing/**'
  - 'modules/acm/**'
  - 'modules/cloudfront/**'
  - 'modules/elb/**'
  - 'modules/waf/**'

prod/routing:
  - 'envs/prod/routing/**'
  - 'modules/acm/**'
  - 'modules/cloudfront/**'
  - 'modules/elb/**'
  - 'modules/waf/**'

dev/security:
  - 'envs/dev/security/**'
  - 'modules/guardduty/**'
  - 'modules/securityhub/**'

prod/security:
  - 'envs/prod/security/**'
  - 'modules/lambda/**'
  - 'modules/guardduty/**'
  - 'modules/securityhub/**'

dev/storage:
  - 'envs/dev/storage/**'
  - 'modules/rds_aurora/**'

prod/storage:
  - 'envs/prod/storage/**'
  - 'modules/rds_aurora/**'

```
</div></details>


<details><summary>modulesの設定ファイル</summary><div>

```yaml:.github/modules-path-filter.yml
acm:
  - 'modules/acm/**'

athena:
  - 'modules/athena/**'

cloudfront:
  - 'modules/cloudfront/**'

cloudtrail:
  - 'modules/cloudtrail/**'

cognito_user_pool:
  - 'modules/cognito_user_pool/**'

config:
  - 'modules/config/**'

ecr:
  - 'modules/ecr/**'

ecs:
  - 'modules/ecs/**'

elb:
  - 'modules/elb/**'

guardduty:
  - 'modules/guardduty/**'

lambda:
  - 'modules/lambda/**'

rds_aurora:
  - 'modules/rds_aurora/**'

s3:
  - 'modules/s3/**'

securityhub:
  - 'modules/securityhub/**'

ses:
  - 'modules/ses/**'

vpc:
  - 'modules/vpc/**'

waf:
  - 'modules/waf/**'

```
</div></details>

setupジョブでは差分が発生したディレクトリの一覧をアウトプットとして設定しており、のちのジョブでmatrixのインプットとして使用します。


### modulesのCI
modules-ciジョブでモジュールに対するCIを実行します。
- matricsを設定
```yaml
  modules-ci:
    needs: setup
    if: needs.setup.outputs.modules_changed_dirs != '[]'
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        changed_dir: ${{ fromJson(needs.setup.outputs.modules_changed_dirs) }}
```
setupジョブのアウトプットで設定した、差分が発生したモジュールディレクトリの一覧からmatrixを設定します。
これによって差分が発生したモジュール全てで並列に処理を実行することができます。
また、fail-fast: falseを指定することで、あるジョブが失敗しても他のジョブはキャンセルされずに最後まで実行されるようになります。

![スクリーンショット 0005-07-19 4.16.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091701/d99acb1f-d3fb-2213-f072-4cb169ebd8ac.png)


- aquaを使用してCLIツールをセットアップ
```yaml
      - name: Setup aqua
        uses: aquaproj/aqua-installer@v2.1.2
        with:
          aqua_version: v2.9.0
          aqua_opts: ""
```
aquaのアクションを使用して、設定ファイルに記述しているツールをインストールします。
設定ファイルは以下の通りです。

```yaml
---
# aqua - Declarative CLI Version Manager
# https://aquaproj.github.io/
# checksum:
#   # https://aquaproj.github.io/docs/reference/checksum/
#   enabled: true
#   require_checksum: true
#   supported_envs:
#   - all
registries:
- type: standard
  ref: v4.23.0 # renovate: depName=aquaproj/aqua-registry
packages:
- name: aquasecurity/tfsec@v1.28.1
- name: hashicorp/terraform@v1.5.2
- name: gruntwork-io/terragrunt@v0.48.0
- name: terraform-linters/tflint@v0.47.0
- name: reviewdog/reviewdog@v0.14.2
- name: suzuki-shunsuke/tfcmt@v4.4.2
- name: direnv/direnv@v2.32.3
```

- tflintの実行

```yaml
      - name: TFlint
        working-directory: modules/${{ matrix.changed_dir }}
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          tflint --config $GITHUB_WORKSPACE/.tflint.hcl --init
          tflint --config $GITHUB_WORKSPACE/.tflint.hcl --format=checkstyle | \
          reviewdog -f=checkstyle \
                    -name="tflint" \
                    -reporter=github-pr-review \
                    -filter-mode=nofilter \
                    -fail-on-error
```
tflintを実行して、問題が発生した場合はreviewdogによってレビューコメントをつけるようにしています。
![スクリーンショット 0005-07-19 4.19.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091701/ac873c19-f807-d7ba-2a8b-7256ecd46407.png)


### envs配下のCIとplan

envs-ciジョブでenvs配下のCIを行ったのち、planを実行します。
modulesのCIと共通の処理の説明は省きます。

- プラグインのキャッシュの設定

```yaml
      - name: Config Terraform plugin cache
        run: |
          echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' >~/.terraformrc
          mkdir --parents ~/.terraform.d/plugin-cache

      - name: Cache Terraform Plugins
        uses: actions/cache@v3
        with:
          path: ~/.terraform.d/plugin-cache
          key: ${{ runner.os }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}
          restore-keys: |
            ${{ runner.os }}-terraform-
```
プラグインのキャッシュの設定を行うことで、initを実行するたびに毎回プラグインをインストールする必要がなくなるので、実行時間の短縮になります。
今回のようにモジュールを細かく分割していると、より恩恵を受けやすくなります。

キャッシュについて詳しくは下記の公式ドキュメントを参考にしてください。

https://developer.hashicorp.com/terraform/cli/config/config-file#provider-plugin-cache

- tfsecとtflintの実行

```yaml
      - name: TFsec
        working-directory: envs/${{ matrix.changed_dir }}
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          tfsec --format=checkstyle | \
          reviewdog -f=checkstyle \
                    -name="tfsec" \
                    -reporter=github-pr-review \
                    -filter-mode=nofilter \
                    -fail-on-error

      - name: TFlint
        working-directory: envs/${{ matrix.changed_dir }}
        env:
          REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          tflint --config $GITHUB_WORKSPACE/.tflint.hcl --init
          tflint --config $GITHUB_WORKSPACE/.tflint.hcl --format=checkstyle | \
          reviewdog -f=checkstyle \
                    -name="tflint" \
                    -reporter=github-pr-review \
                    -filter-mode=nofilter \
                    -fail-on-error
```

tflintと同じ要領でtfsecも実行します。
tfsecは使用しているモジュールの中身もチェックしてくれますが、tflintは現在のモジュールしかチェックしないため、モジュールのCIでもtflintを実行する必要がありました。

- planを実行し、結果をプルリクにコメント

```yaml
      - name: Terragrunt plan
        working-directory: envs/${{ matrix.changed_dir }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          terragrunt plan --terragrunt-tfpath $GITHUB_WORKSPACE/.github/scripts/tfwrapper.sh
```

```sh:.github/scripts/tfwrapper.sh
#!/bin/bash

set -euo pipefail

# コマンドの種類を取得(例: apply, plan, fmt...)
type=$(echo "$@" |  awk '{print $1}')

# 実行しているディレクトリ名を取得
current_dir=$(pwd | sed 's/.*\///g')

if [ "$type" == "plan" ]; then
    # planのときは-patchオプションを付ける
    # 実行ディレクトリ名をターゲットとして指定
    tfcmt -var "target:${current_dir}" plan -patch -- terraform "$@"
elif [ "$type" == "apply" ]; then
    tfcmt -var "target:${current_dir}" apply -- terraform "$@"
else
    terraform "$@"
fi
```

terragrunt planを実行し、tfcmtを使用して結果をプルリクエストにコメントします。
![スクリーンショット 0005-07-19 5.23.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1091701/d7e011e4-6367-5bc4-f67c-253be8041c6f.png)

terragruntとtfcmtを組み合わせて使う方法は過去の投稿で説明しているので、詳細はそちらを参照ください。

https://qiita.com/tak0203753/items/5d12ea942ca12177238f

# applyワークフロー
<details><summary>applyワークフローの全体はこちらから確認してください</summary><div>

```yaml:apply.tf
name: Terraform apply

on:
  pull_request:
    branches:
      - develop
      - main
    types:
      - closed

env:
  AWS_REGION: ${{ vars.AWS_REGION }}
  SYSTEM: ${{ vars.SYSTEM }}
  DEV_AWS_ACCOUNT_ID: ${{ secrets.DEV_AWS_ACCOUNT_ID }}
  PROD_AWS_ACCOUNT_ID: ${{ secrets.PROD_AWS_ACCOUNT_ID }}

permissions:
  contents: read
  id-token: write
  pull-requests: write
  actions: read

jobs:
  notify-started:
    if: github.event.pull_request.merged == true
    uses: ./.github/workflows/_notify_started.yml
    secrets: inherit

  apply:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    steps:
      - name: Set env vars for each env
        run: |
          if [ ${{ github.ref_name }} == 'main' ]; then
            echo "ENVIRONMENT=prod" >> $GITHUB_ENV
            echo "AWS_ACCOUNT_ID=${{ env.PROD_AWS_ACCOUNT_ID }}" >> $GITHUB_ENV

          elif [ ${{ github.ref_name }} == 'develop' ]; then
            echo "ENVIRONMENT=dev" >> $GITHUB_ENV
            echo "AWS_ACCOUNT_ID=${{ env.DEV_AWS_ACCOUNT_ID }}" >> $GITHUB_ENV
          fi

      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup aqua
        uses: aquaproj/aqua-installer@v2.1.2
        with:
          aqua_version: v2.9.0
          aqua_opts: ""

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1-node16
        with:
          role-to-assume: arn:aws:iam::${{ env.AWS_ACCOUNT_ID }}:role/${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-role
          role-session-name: ${{ env.SYSTEM }}-${{ env.ENVIRONMENT }}-github-actions-terraform-session
          aws-region: ${{ env.AWS_REGION }}

      - name: Config Terraform plugin cache
        run: |
          echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' >~/.terraformrc
          mkdir --parents ~/.terraform.d/plugin-cache

      - name: Cache Terraform Plugins
        uses: actions/cache@v3
        with:
          path: |
            ~/.terraform.d/plugin-cache
          key: ${{ runner.os }}-terraform-${{ hashFiles('**/.terraform.lock.hcl') }}
          restore-keys: |
            ${{ runner.os }}-terraform-

      - name: Terragrunt run-all init
        working-directory: envs/${{ env.ENVIRONMENT }}
        run: |
          terragrunt run-all init --terragrunt-non-interactive

      - name: Terragrunt run-all apply
        working-directory: envs/${{ env.ENVIRONMENT }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          terragrunt run-all apply --terragrunt-non-interactive --terragrunt-tfpath $GITHUB_WORKSPACE/.github/scripts/tfwrapper.sh

  notify-finished:
    needs: apply
    if: always()
    uses: ./.github/workflows/_notify_finished.yml
    secrets: inherit

```
</div></details>

### トリガー条件

```yaml
on:
  pull_request:
    branches:
      - develop
      - main
    types:
      - closed
```

applyのワークフローのトリガーはmain, developブランチに対するプルリクが閉じられた時に設定しています。
「if: github.event.pull_request.merged == true」と組み合わせることで、マージされた時のみ処理を実行するように設定できます。

### applyの実行

```yaml
      - name: Terragrunt run-all apply
        working-directory: envs/${{ env.ENVIRONMENT }}
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          terragrunt run-all apply --terragrunt-non-interactive --terragrunt-tfpath $GITHUB_WORKSPACE/.github/scripts/tfwrapper.sh
```
applyはrun-allコマンドで一括で実行します。
--terragrunt-non-interactiveをつけることでインタラクティブなY/Nの確認がなくなります。

# まとめ
ここまでTerraformのCI/CDを紹介してきましたが、実は自分自身まだこのCI/CDをちゃんと運用したことはありません。
今後、実際に使っていきながらアップデートしていきたいと思っています。

# 参考

https://engineer.retty.me/entry/2022/11/18/130000?utm_source=pocket_reader

https://zenn.dev/naofumimurata/articles/tf-monorepo-github-actions