理系学生日記

おまえはいつまで学生気分なのか

GitHub ActionsでTerraformのinit/fmt/plan

これまでTerraformに関するCIとしてはGitLab CI/CDを使っていました。 最近はプライベートでGitHubを利用しTerraform周りのコードを管理しており、GitHub上でのCIを色々試行錯誤しています。

ようやく以下のように、terraform planの結果等をPull Requestへ自動コメントできるようになりました。

完全なworkflowはこちらです。

ルートモジュール毎の実行

Terraformでのベストプラクティスの1つとしてstateを分けるということはよく言われています。 結果として、ルートモジュールの数が多くなり、個々のモジュールにterraform fmtterraform validateを実行すると時間がかかりすぎます。

この問題に対してはjobs.<job_id>.strategy.matrixを利用することで、ルートモジュールそれぞれで並列実行するようにしました。

    strategy:
      matrix:
        dir:
          - aws/backend
          - aws/budgets
          - aws/cloudtrail
          - aws/memories
          - aws/github_actions
    steps:

このテクニックは以下の記事を参考にしています。

差分があったルートモジュールに対してだけCIジョブを実行する

ルートモジュールがたくさんあっても、CIジョブを実行すべきなのは「差分があったルートモジュール」だけなはずです。 差分の有無についてはgit diffを使えば良いのですが、せっかくGitHub Actionsを使うのでコマンドを羅列するのは避けたい。

get-diff-actionというActionsがあるので、これを使って差分を検出することにしました。

前述の通り、jobs.<job_id>.strategy.matrixで並列実行していることを前提に、.tfあるいは.tfvarsの差分を検出するようにしています。

      # matrix.dir で指定されたサブディレクトリの中にある terraform ファイルの差分を確認する
      - name: Diff Terraform Scripts
        id: diff
        uses: technote-space/get-diff-action@v5
        with:
          PATTERNS: |
            ${{ matrix.dir }}/**/*.tf
            ${{ matrix.dir }}/**/*.tfvars

差分があった場合はsteps.diff.outputs.diffで検知できます。差分があった場合にのみ後続ジョブを実行するため、後続ジョブではifを使用してジョブの実行可否を制御します。

      - name: Configure aws credentials
        if: steps.diff.outputs.diff

AWS環境用の権限を得る

configure-aws-credentialsを使うことで、AWSのアクセスキーをハードコードすることなく権限を入手できます。

      - name: Configure aws credentials
        if: steps.diff.outputs.diff
        uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: ${{ secrets.TERRAFORM_ROLE_TO_ASSUME }}
          role-duration-seconds: 900
          aws-region: ${{ env.AWS_REGION }}

いくつか事前準備は必要ですが、それはこちらで記載しました。

Terraformのセットアップ

Terraformのセットアップについてはhashicorp公式のGitHub Actionsが存在するため、 こちらを利用すれば良いでしょう。

      - name: Setup terraform
        if: steps.diff.outputs.diff
        uses: hashicorp/setup-terraform@v1
        with:
          terraform_version: 1.1.2
          terraform_wrapper: true

ポイントはterraform_wrapperを有効化することでしょうか。これを有効化することで、terraformを実行するときにSTDOUTSTDERR等を後続ジョブで利用できます。

fmtvalidateplanの結果をコメント投稿する

terraform fmt等の実行については単にコマンドを実行するのみです。 一方で、その結果をコメント投稿するためにterraform-pr-commenterを使いました。terraaform-pr-commenterは、fmtvalidateplanの実行結果を良い感じに整形してコメント投稿してるActionです。

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

      - name: Comment format results
        uses: robburger/terraform-pr-commenter@v1
        if: steps.fmt.outputs.diff
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          commenter_type: fmt
          commenter_input: ${{ format('{0}{1}', steps.fmt.outputs.stdout, steps.fmt.outputs.stderr) }}
          commenter_exitcode: ${{ steps.fmt.outputs.exitcode }}

注意すべきは、このコメント投稿にはコメント投稿可能な権限を持ったGITHUB_TOKENが必要であることです。 GITHUB_TOKENの持つ権限はpermissionsで設定できるので、ワークフローの設定ファイルで設定しましょう。

Pull Requestへのコメントはpull-requestsへのwriteがあれば良いので、以下のようになるでしょう。

jobs:
  plan:
    name: Plan
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      pull-requests: write

AWSの権限をOIDC経由で入手しようとするとid-token: writeの明示的な設定が必要となり、結果としてpull-requestsの権限が落ちるという落とし穴があります。ぼくもここにハマりました。 以下の記事でpull-requests: writeの設定漏れに気づいた次第です。

あとは同様に、validateplan等のジョブを設定すれば良いでしょう。

エラーが発生した場合にはワークフローをFailさせる

以下のように設定しました。正直、 if: {{ failure() }}でよかったような気もします。

      - name: Exit with appropriate status
        if: steps.fmt.outcome == 'failure' || steps.init.outcome == 'failure' || steps.validate.outcome == 'failure' || steps.plan.outcome == 'failure'
        run: exit 1