【GitHubActions】GitHub Actions で terraform をイイ感じに実行する【terraform】

# 概要
GitHub Actions で terraform を実行する、イイ感じに。
イイ感じとは、なるべくエンジニアの手を煩わせることなく、いろいろ自動化することである。

# 目的
自動化バンザイ。

また、今回は実業務で得たノウハウのアウトプットになるため、パーソナルな内容ではなく、チームで活用出来たら良いな、という内容になっている。

# 事前調査
+ **GitHub Actions**
GitHub Actionsというものを利用すると、Git上で色々なことが出来るらしい。
terraformを実行（planやapply）をすることも出来るので、ローカルに実行環境を持たなくて良くなる、素晴らしい。
　
公式のGitHubホストランナーとして下記が提供されており（実際にはAzure内でインスタンスが起動する）、terraform最新版（多分）他様々なツールがプレインストール済みだが、それ以外のツールを使用したい場合、追加インストールしてカスタマイズすることが出来る。
　
[About GitHub-hosted runners](https://docs.github.com/ja/actions/using-github-hosted-runners/about-github-hosted-runners)
　
+ **Terraform用ワークフロー**
上記のランナーに対してワークフロー（ジョブやスクリプトみたいなもの）を定義し、実際に実行される内容を設定する。
[ワークフローについて](https://docs.github.com/ja/actions/using-workflows/about-workflows)
　

+ **Terraform用Action**
hashicorpから「setup-terraform」というActionが提供されており、これを使用することで特定のバージョンのterraformを使ったり、terraformコマンドを簡単に実行したり出来るようになる。　
　
[setup-terraform](https://github.com/hashicorp/setup-terraform)
　
このActionで「terraformをどこで実行するか？」の指定が可能になっており、「Terraform Cloud」か「GitHubホストランナー」かを選択出来る。
ただ、どちらの場合にせよ「実行環境の起動」があるので、実行時間は長くなると思われる（とはいえ、以前Terraform Cloudを試用した場合もそこまで極端に遅くはなかった印象）
この実行時間やインスタンスのスペックが許容出来ない場合は、自前で用意したインスタンスに「ランナーアプリケーション」をインストールした「セルフホストランナー」を選択することになる。
　
[About self-hosted runners](https://docs.github.com/ja/actions/hosting-your-own-runners/about-self-hosted-runners)
　
実際の GitHub Actions の動き（デモ）は下記を参照（音声出るため注意）
[GitHub & HashiStackで始めるクラウドインフラ自動化入門（38:01 - GitHub Actions の紹介, GitHub で Terraform 運用するメリット）](https://www.youtube.com/watch?v=idkz-31qQ5Y&t=2281s)
　
今回は、導入の手軽さ・運用管理コストなどの面から、GitHubホストランナーを採用する。
　
+ **クラウドサービスの認証について**
クラウドサービス側にリソースを作成するにあたっての認証方式として考えられるのが主に下記２つ。
　①サービスアカウントキー認証
　②OIDC認証
お手軽なのは①だが、セキュリティ面を考慮すると②が推奨。


# 1. ワークフロー作成
GitHub Actions から各クラウドサービスに対して Terraform を実行するにあたって必要となる構築・設定は大別すると下記３つとなる。
　①GitHub　　　　　：リポジトリ作成
　①クラウドサービス：①で作成したリポジトリからOIDC認証をするための設定
　②GitHub　　　　　：GitHubActions側でOIDC認証を含んだワークフロー設定

今回、①②は済んでいるものとし、ワークフローにフォーカスした内容とする。


## 1.1. ワークフロー全文
GitHubリポジトリ直下に下記を作成。

<details><summary>.github\workflows\terraform.yml</summary><div>

```
name: 'Terraform'

on:
  push:
    branches:
      - master
      - 'feature/**'
  pull_request:
    branches:
      - master

permissions: write-all

env:
  terraform_version: '1.x.y' # 使用する terraform のバージョン
  project_number: 'hoge' # OIDC認証に使用する workload_identity_pool が存在する Project
  workload_identity_pool: 'hoge'  # OIDC認証に使用する workload_identity_pool
  workload_identity_provider: 'hoge' # OIDC認証に使用する workload_identity_provider
  service_account: 'hoge' # OIDC認証に使用するSA
  reviewer: 'hoge' # Pull Request に設定するレビュワー
  GITHUB_TOKEN: ${{ secrets.FOR_RUNNER_NPRD }} # GitHub Personal Access Token
  jira_org: 'hoge'


jobs:
  # Terraform 実行ディレクトリを取得
  set-matrix:
    name: 'Set Matrix'

    runs-on: ubuntu-latest

    env:
      TF_ROOT_DIR: .

    steps:
      # Checkout the repository to the GitHub Actions runner
      - name: 'Checkout'
        id: 'checkout'
        uses: actions/checkout@v3
        #with:
        #  fetch-depth: 0

      - name: Find tfstate dirs
        id: find-tfstate-dirs
        run: |
          dirs=$(find ${TF_ROOT_DIR} -type f -name '*.tf' -exec dirname {} \; | grep -v 'modules\|\.terraform' | uniq | jq -R -s -c 'split("\n")[:-1]')
          dirs=`echo ${dirs//\.\//}` # ディレクトリパスの「./」を削除
          echo $dirs # 確認用
          echo "::set-output name=dirs::${dirs}"

    outputs:
      dirs: ${{ steps.find-tfstate-dirs.outputs.dirs }}


  # Terraform 実行
  terraform:
    needs: set-matrix

    name: 'Terraform'

    permissions: write-all
    # permissions:
    #   id-token: write # OIDC 認証のために必要
    #   contents: read  # actions/checkout のために必要

    runs-on: ubuntu-latest

    # Terraform 実行ディレクトリをmatrix変数に格納
    strategy:
      fail-fast: false
      max-parallel: 5
      matrix:
        TF_DIR: ${{ fromJson(needs.set-matrix.outputs.dirs) }}

    env:
      TF_DIR: ${{ matrix.TF_DIR }}

    defaults:
      run:
        shell: bash
        working-directory: ${{ env.TF_DIR }}

    steps:
      # Checkout the repository to the GitHub Actions runner
      - name: 'Checkout'
        id: 'checkout'
        uses: actions/checkout@v3
        with:
          fetch-depth: 2

      # 更新履歴を比較し、差分チェック
      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v29.0.0
        with:
          files: |
            ${{ env.TF_DIR }}/*.tf

      # OIDC 認証
      - name: 'Authenticate to Google Cloud'
        id: 'auth'
        if: steps.changed-files.outputs.any_modified == 'true'
        uses: 'google-github-actions/auth@v0'
        with:
          token_format: 'access_token'
          workload_identity_provider: 'projects/${{ env.project_number }}/locations/global/workloadIdentityPools/${{ env.workload_identity_pool }}/providers/${{ env.workload_identity_provider }}'
          service_account: ${{ env.service_account }}

      # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token
      - name: Setup Terraform
        id: setup
        if: steps.changed-files.outputs.any_modified == 'true'
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: ${{ env.terraform_version }}
          terraform_wrapper: true

      # Checks that all Terraform configuration files adhere to a canonical format
      - name: Terraform Format (${{ env.TF_DIR }})
        id: fmt
        if: steps.changed-files.outputs.any_modified == 'true'
        run: terraform fmt
        continue-on-error: true

      # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.
      - name: Terraform Init (${{ env.TF_DIR }})
        id: init
        if: steps.changed-files.outputs.any_modified == 'true'
        run: terraform init

      # Checks that all Terraform configuration files adhere to a canonical syntax
      - name: Terraform Validate (${{ env.TF_DIR }})
        id: validate
        if: steps.changed-files.outputs.any_modified == 'true'
        run: terraform validate -no-color

      # リリース用PRが既に存在するかどうかをチェック、及び PR 内容設定
      - name: Check if pr exists
        id: check_pr
        if: ( github.ref != 'refs/heads/master' && github.event_name == 'push' && steps.changed-files.outputs.any_modified == 'true' )
        env:
          GH_TOKEN: ${{ env.GITHUB_TOKEN }}
        run: |
          base_branch=master
          pr_title=${{ github.ref }}
          pr_title=`echo ${pr_title//refs\/heads\//}`
          pr_ticket=`echo ${pr_title//feature\//}`
          pr_body=`echo "【チケット】
          https://docomo-common.atlassian.net/browse/${pr_ticket}
          
          【変更内容】
          ${{ github.event.head_commit.message }}"`

          # 改行エスケープ処理
          pr_body="${pr_body//'%'/'%25'}"
          pr_body="${pr_body//$'\n'/'%0A'}"
          pr_body="${pr_body//$'\r'/'%0D'}" 

          # 後続処理に受け渡すため Output
          echo "::set-output name=count::$(gh pr list -S ${pr_title}' in:title' -B $base_branch | wc -l)"
          echo "::set-output name=base_branch::$base_branch"
          echo "::set-output name=pr_title::$pr_title"
          echo "::set-output name=pr_body::$pr_body"

      # リリース用PRを作成
      - name: Create release pr
        if: ( github.ref != 'refs/heads/master' && github.event_name == 'push' && steps.changed-files.outputs.any_modified == 'true' && steps.check_pr.outputs.count == 0 )
        env:
          GH_TOKEN: ${{ env.GITHUB_TOKEN }}
        run: |
          gh pr create -B ${{ steps.check_pr.outputs.base_branch }} -t ${{ steps.check_pr.outputs.pr_title }} -b "${{ steps.check_pr.outputs.pr_body }}" --reviewer ${{ env.reviewer }}

      # Generates an execution plan for Terraform
      - name: Terraform Plan (${{ env.TF_DIR }})
        id: plan
        if: steps.changed-files.outputs.any_modified == 'true'
        run: terraform plan -no-color -input=false
        continue-on-error: true

      # Pull Request/コード差分有り/Planにて変更有り　の場合、コメントを追記する
      - name: Adding Pull Request Comment
        uses: actions/github-script@v6
        if: ( github.event_name == 'pull_request' && steps.changed-files.outputs.any_modified == 'true' && !contains(steps.plan.outputs.stdout, 'No changes.') )
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ env.GITHUB_TOKEN }}
          script: |
            const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
            #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
            #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

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

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

            </details>

            *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

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

      # Pull Request/コード差分有り/Planにて変更有り　の場合、コメントを追記する
      - name: Adding Pull Request Comment 2
        uses: actions/github-script@v6
        if: ( github.event_name == 'pull_request' && steps.changed-files.outputs.any_modified == 'true' && !contains(steps.plan.outputs.stdout, 'No changes.') )
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ env.GITHUB_TOKEN }}
          script: |
            const output = `\n#### Terraform Format and Style 🖌 \`${{ steps.fmt.outcome }}\`\n
            #### Terraform Initialization ⚙️ \`${{ steps.init.outcome }}\`\n
            #### Terraform Validation 🤖 \`${{ steps.validate.outcome }}\`\n
            #### Terraform Plan 📖 \`${{ steps.plan.outcome }}\`\n

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

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

            </details>

            *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

            await core.summary
              .addHeading('Terraform plan report')
              .addRaw(output)
              .write()

      # Plan に失敗していたら、Applyの実行はしない
      - name: Terraform Plan Status
        if: steps.plan.outcome == 'failure'
        run: exit 1

      # masterブランチ/push（merge）/Planにて変更有り　の場合、Applyを実行する
      - name: Terraform Apply (${{ env.TF_DIR }})
        id: apply
        if: ( github.ref == 'refs/heads/master' && github.event_name == 'push' && steps.changed-files.outputs.any_modified == 'true' && !contains(steps.plan.outputs.stdout, 'No changes.') )
        run: terraform apply -auto-approve -input=false
```
</div></details>

以下、ポイント解説。

## 1.2. 変数設定
```terraform.yml
env:
  terraform_version: '1.x.y' # 使用する terraform のバージョン
  project_number: 'hoge' # OIDC認証に使用する workload_identity_pool が存在する Project
  workload_identity_pool: 'hoge'  # OIDC認証に使用する workload_identity_pool
  workload_identity_provider: 'hoge' # OIDC認証に使用する workload_identity_provider
  service_account: 'hoge' # OIDC認証に使用するSA
  reviewer: 'hoge' # Pull Request に設定するレビュワー
  GITHUB_TOKEN: ${{ secrets.FOR_RUNNER_NPRD }} # GitHub Personal Access Token
```
詳細はコメントの通り。
「GITHUB_TOKEN」は、GitHubのSecretに登録したものを指定している。
プルリク作成や、プルリクへのコメント追記に必要。


## 1.3. Terraform 実行ディレクトリを取得
```terraform.yml
  set-matrix:
    name: 'Set Matrix'

    runs-on: ubuntu-latest

    env:
      TF_ROOT_DIR: .

    steps:
      # Checkout the repository to the GitHub Actions runner
      - name: 'Checkout'
        id: 'checkout'
        uses: actions/checkout@v3
        #with:
        #  fetch-depth: 0

      - name: Find tfstate dirs
        id: find-tfstate-dirs
        run: |
          dirs=$(find ${TF_ROOT_DIR} -type f -name '*.tf' -exec dirname {} \; | grep -v 'modules\|\.terraform' | uniq | jq -R -s -c 'split("\n")[:-1]')
          dirs=`echo ${dirs//\.\//}` # ディレクトリパスの「./」を削除
          echo $dirs # 確認用
          echo "::set-output name=dirs::${dirs}"

    outputs:
      dirs: ${{ steps.find-tfstate-dirs.outputs.dirs }}
```
terraformコードを作成するにあたって、ディレクトリ構成をどうするか？という悩みは常に付き纏うことかと思う。
公式のベストプラクティスが必ずしも実態に合うとは限らないし、社風や好みなども大きいだろう。
そこで、tfファイルが存在するディレクトリ一覧を取得し、それら全てを「terraform実行ディレクトリ」と想定する。
取得したディレクトリ一覧は output することで、後続のJobで利用することが出来る。
（Jobを分けている理由は後述）

## 1.4. Terraform 実行ディレクトリを matrix 定義
```terraform.yml
    strategy:
      fail-fast: false
      max-parallel: 5
      matrix:
        TF_DIR: ${{ fromJson(needs.set-matrix.outputs.dirs) }}

    env:
      TF_DIR: ${{ matrix.TF_DIR }}
```
前Jobで指定した Terraform 実行ディレクトリを、上記のような matrix 変数として定義すると、要素分だけ Job を並列実行することが出来る。
この matrix は Job レベルの変数でしか定義出来ないため、動的に指定したい場合、前段 Job で動的に定義した変数を output 経由で渡す必要がある。
（複数ディレクトリを静的に指定するだけで良ければ、前段 Job は不要）

[Using a matrix for your jobs](https://docs.github.com/ja/actions/using-jobs/using-a-matrix-for-your-jobs)


## 1.5. コード差分チェック
```terraform.yml
    steps:
      # Checkout the repository to the GitHub Actions runner
      - name: 'Checkout'
        id: 'checkout'
        uses: actions/checkout@v3
        with:
          fetch-depth: 2

      # 更新履歴を比較し、差分チェック
      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v29.0.0
        with:
          files: |
            ${{ env.TF_DIR }}/*.tf
```
実際に terraform を実行したいディレクトリは、コードに変更があったところだけとしたいため、差分チェックをする。
Drift check にもなるので全ディレクトリ一括 apply しても良いのだが、コードの規模に応じて実行時間も長くなってしまう。
Drift check をしたいのであれば、それはそれで別途ワークフローを定義するなり、別手段を設けるなりした方が良いかと思う。

## 1.6. OIDC 認証
```terraform.yml
      # OIDC 認証
      - name: 'Authenticate to Google Cloud'
        id: 'auth'
        if: steps.changed-files.outputs.any_modified == 'true'
        uses: 'google-github-actions/auth@v0'
        with:
          token_format: 'access_token'
          workload_identity_provider: 'projects/${{ env.project_number }}/locations/global/workloadIdentityPools/${{ env.workload_identity_pool }}/providers/${{ env.workload_identity_provider }}'
          service_account: ${{ env.service_account }}
```
GCP に対する OIDC 認証を行っている。
他クラウドサービスを利用する場合は、「uses」の指定を変更すれば良いと思う。
「with」指定する変数がクラウドサービスによって異なる可能性もあるが、今回は GCP でのみ確認をしている。

また、「if」にて前 step にて行った差分チェックの結果に応じて、step の実行有無を判定している。


## 1.7. Setup Terraform
```terraform.yml
      # Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token
      - name: Setup Terraform
        id: setup
        if: steps.changed-files.outputs.any_modified == 'true'
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: ${{ env.terraform_version }}
          terraform_wrapper: true
```
事前調査でも述べた、hashicorp 提供の Actionである「setup-terraform」を使用し、terraform のバージョン指定を行っている。
公式のGitHubホストランナーには terraform がプレインストールされてはいるものの、おそらく実行時の最新版となるため、実行するタイミングによって terraform バージョンが異なってしまう。
一般的にはコード作成にあたり terraform バージョンを指定あるいは固定して行うことが多いと思うので、実行環境のバージョンも明示的に指定・使用すべきかと思う。


## 1.8. terraform fmt & init & validate
```terraform.yml
      # Checks that all Terraform configuration files adhere to a canonical format
      - name: Terraform Format (${{ env.TF_DIR }})
        id: fmt
        if: steps.changed-files.outputs.any_modified == 'true'
        run: terraform fmt
        continue-on-error: true

      # Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.
      - name: Terraform Init (${{ env.TF_DIR }})
        id: init
        if: steps.changed-files.outputs.any_modified == 'true'
        run: terraform init

      # Checks that all Terraform configuration files adhere to a canonical syntax
      - name: Terraform Validate (${{ env.TF_DIR }})
        id: validate
        if: steps.changed-files.outputs.any_modified == 'true'
        run: terraform validate -no-color
```
terraform plan 前のコードチェックを行っている。
また、fmt については差分（フォーマット修正）があった場合、return code が 3 となり、GitHub Actions がエラー判定で止まってしまうことがあるため、「continue-on-error: true」を付け、エラー時も続行するようにしている。


## 1.9. プルリク作成
```terraform.yml
      # リリース用PRが既に存在するかどうかをチェック、及び PR 内容設定
      - name: Check if pr exists
        id: check_pr
        if: ( github.ref != 'refs/heads/master' && github.event_name == 'push' && steps.changed-files.outputs.any_modified == 'true' )
        env:
          GH_TOKEN: ${{ env.GITHUB_TOKEN }}
        run: |
          base_branch=master
          pr_title=${{ github.ref }}
          pr_title=`echo ${pr_title//refs\/heads\//}`
          pr_ticket=`echo ${pr_title//feature\//}`
          pr_body=`echo "【チケット】
          https://${jira_org}.atlassian.net/browse/${pr_ticket}
          
          【変更内容】
          ${{ github.event.head_commit.message }}"`

          # 改行エスケープ処理
          pr_body="${pr_body//'%'/'%25'}"
          pr_body="${pr_body//$'\n'/'%0A'}"
          pr_body="${pr_body//$'\r'/'%0D'}" 

          # 後続処理に受け渡すため Output
          echo "::set-output name=count::$(gh pr list -S ${pr_title}' in:title' -B $base_branch | wc -l)"
          echo "::set-output name=base_branch::$base_branch"
          echo "::set-output name=pr_title::$pr_title"
          echo "::set-output name=pr_body::$pr_body"

      # リリース用PRを作成
      - name: Create release pr
        if: ( github.ref != 'refs/heads/master' && github.event_name == 'push' && steps.changed-files.outputs.any_modified == 'true' && steps.check_pr.outputs.count == 0 )
        env:
          GH_TOKEN: ${{ env.GITHUB_TOKEN }}
        run: |
          gh pr create -B ${{ steps.check_pr.outputs.base_branch }} -t ${{ steps.check_pr.outputs.pr_title }} -b "${{ steps.check_pr.outputs.pr_body }}" --reviewer ${{ env.reviewer }}
```
feature ブランチへ push した際、自動で master ブランチへのプルリク作成を行う処理。
作成前に同ブランチのプルリクが既に存在しないかをチェックしてから作成を行う。

プルリクの本文作成なども行っており、push 時のコミットメッセージが「変更内容」として本文に記載されるようになっている。

ここが一番見える化に影響する部分になるので、いろいろ細かくしていきたい。
例えば、terraform graph の結果をプルリクに貼るとか（ただ、個人的に terraform graph 自体が見辛い…）


## 1.10. terraform plan
抜粋にしては長いけど、ここはセットなので…。
```terraform.yml
      # Generates an execution plan for Terraform
      - name: Terraform Plan (${{ env.TF_DIR }})
        id: plan
        if: steps.changed-files.outputs.any_modified == 'true'
        run: terraform plan -no-color -input=false
        continue-on-error: true

      # Pull Request/コード差分有り/Planにて変更有り　の場合、コメントを追記する
      - name: Adding Pull Request Comment
        uses: actions/github-script@v6
        if: ( github.event_name == 'pull_request' && steps.changed-files.outputs.any_modified == 'true' && !contains(steps.plan.outputs.stdout, 'No changes.') )
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ env.GITHUB_TOKEN }}
          script: |
            const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
            #### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
            #### Terraform Validation 🤖\`${{ steps.validate.outcome }}\`
            #### Terraform Plan 📖\`${{ steps.plan.outcome }}\`

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

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

            </details>

            *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

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

      # Pull Request/コード差分有り/Planにて変更有り　の場合、コメントを追記する
      - name: Adding Pull Request Comment 2
        uses: actions/github-script@v6
        if: ( github.event_name == 'pull_request' && steps.changed-files.outputs.any_modified == 'true' && !contains(steps.plan.outputs.stdout, 'No changes.') )
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ env.GITHUB_TOKEN }}
          script: |
            const output = `\n#### Terraform Format and Style 🖌 \`${{ steps.fmt.outcome }}\`\n
            #### Terraform Initialization ⚙️ \`${{ steps.init.outcome }}\`\n
            #### Terraform Validation 🤖 \`${{ steps.validate.outcome }}\`\n
            #### Terraform Plan 📖 \`${{ steps.plan.outcome }}\`\n

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

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

            </details>

            *Pusher: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;

            await core.summary
              .addHeading('Terraform plan report')
              .addRaw(output)
              .write()
```
terraform plan を実行し、その結果をプルリクにコメント投稿する。
「if」では実行条件を細かく指定し、plan 結果が「No changes.」だったらコメント投稿しない、などとしている。
というのも、１つのプルリク内で複数の terraform 実行フォルダを並列実行可能なので、例えばコードのリファクタリングやコード内のコメント分修正などのみを行った場合、実リソースに影響はないのに、プルリクコメントが大量に投稿されてしまったりする。
それは可読性が悪くなるので、あくまでコメント投稿は実リソースに変更がある場合のみ、としている。


## 1.11. terraform apply
```terraform.yml
      # Plan に失敗していたら、Applyの実行はしない
      - name: Terraform Plan Status
        if: steps.plan.outcome == 'failure'
        run: exit 1

      # masterブランチ/push（merge）/Planにて変更有り　の場合、Applyを実行する
      - name: Terraform Apply (${{ env.TF_DIR }})
        id: apply
        if: ( github.ref == 'refs/heads/master' && github.event_name == 'push' && steps.changed-files.outputs.any_modified == 'true' && !contains(steps.plan.outputs.stdout, 'No changes.') )
        run: terraform apply -auto-approve -input=false
```
最後に terraform apply を実行するが、前段の plan 結果でエラーとなっていた場合は実行しないよう、break 的な処理を入れている。


# 2. Tips & 課題的なもの
## 2.1. Secret について
GitHub 内の Secret （GCP の SecretManager や、AWS の Parameter Store 的なもの）に Personal Access Token（以下PAT） 、リポジトリ毎に設定する必要があるため、リポジトリの数が多かったり、PATの変更・更新などがある場合やや面倒である。
Organization レベルの Secret を登録すると、複数リポジトリから使用したりも出来るようだが、権限的に出来ず未確認。

将来的には hashicorp の Vault 利用なども検討したい。
hashicorp から、Vault 用の Action も提供されている。

[hashicorp/vault-action](https://github.com/hashicorp/vault-action)

## 2.2. プルリク作成者について
プルリク作成を共通ユーザーにて行う想定だが、この場合「プルリク作成者（コード修正者）」を特定出来ない。
レビュワーにチーム指定や、チーム内からランダム割当などを行っている場合、プルリク作成者にもレビュワーが割り当てられてしまうことがある。


## 2.3. ブランチ名について
Jira を併用するケースを想定し、プルリクにはブランチ名をJiraチケットURLとして自動挿入している。
つまり、「Jiraチケット番号＝ブランチ名」である前提となっているので、１つのチケットに対して複数のブランチが作成されることを想定していない。
ただ、ここはスクリプトでどうにでもなりそうな部分なので、実態に合わせて改善していければと思う。


## 2.4. GitHub の Personal Access Token について
デフォルトの Personal Access Token には workflow の実行権限がないため、スコープを修正する必要がある（workflowを追加）
また、プルリク作成時、レビュワーにグループを指定するために、['read:org', 'read:discussion']スコープも必要。
（レビュワーがグループではない場合や、レビュワー指定が不要な場合は、上記権限は不要）

下記は、スコープが不足している状態でのエラー。

```bash
Run gh pr create -B master -t feature/gitac-test -b "" --reviewer hoge/hoge
Warning: 1 uncommitted change
GraphQL: Your token has not been granted the required scopes to execute this query. The 'id' field requires one of the following scopes: ['read:org', 'read:discussion'], but your token has only been granted the: ['repo', 'workflow'] scopes. Please modify your token's scopes at: https://github.com/settings/tokens.,  Your token has not been granted the required scopes to execute this query. The 'slug' field requires one of the following scopes: ['read:org', 'read:discussion'], but your token has only been granted the: ['repo', 'workflow'] scopes. Please modify your token's scopes at: https://github.com/settings/tokens. 
Error: Process completed with exit code 1.
```

# 3. まとめ
今回初めて、プライベートでやった内容ではなく、業務内で経験したことを書いてみた。
なるべく特定されないような形では書いているけど、もしどこかにそれっぽい記述が残っていたら、コメント等でさりげなく教えていただけると幸いです。

内容的には、自動化・最適化・マネージドサービス活用といった個人的に大好きな分野なので、業務であっても楽しく出来た。
今後の業務にも活かしていければと思う。