terraform planを動的に実行するシンプルなGitHub Actions

こんにちは、プラットフォームチームのテックリードの星井です。HRBrainのAdvent Calendarの8日目の記事です。

先日、Terraformコマンドを実行するGitHub Actionsのワークフローを作成しました。

作成するにあたって、いくつかインターネットの記事を参考にしましたが、monorepo用でシンプルなワークフローが意外となかったので、ブログにまとめることにしました。

メンテナンスしやすいワークフロー

GitHub Actionsは便利ですが、数が増えてくると、依存関係のアップデートなどが面倒です。なので、今回はメンテナンスコストを最低限にしたワークフローを作ることを目指しました。コマンドスクリプトを使って動的なワークフローにして、便利なtfcmtを使うことで、メンテナンス性を高めています。

name: terraform-plan
on:
  pull_request:
    paths:
      - 'environments/**'
env:
  TF_VAR_project_id: <PROJECT_NAME>
jobs:
  check_changed_dirs:
    runs-on: ubuntu-20.04
    outputs:
      changes: ${{ steps.pr_dir_changes.outputs.changes }}
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - id: pr_dir_changes
        # masterと差分のあるディレクトリのみを抽出する
        # https://stackoverflow.com/questions/50440420/git-diff-only-show-which-directories-changed
        run: echo "changes="[$(git diff origin/"${GITHUB_BASE_REF}" --dirstat=files,0 | awk '{print $2}' | sed -e 's:/*$::' | grep 'environments' | awk '{ print "\""$0"\""}' | paste -sd, -)]"" >> "$GITHUB_OUTPUT"
  tf-ci:
    runs-on: ubuntu-20.04
    permissions:
      contents: 'write'
      id-token: 'write'
      pull-requests: 'write'
    timeout-minutes: 20
    needs: check_changed_dirs
    strategy:
      matrix:
        dir: ${{ fromJSON(needs.check_changed_dirs.outputs.changes) }}
    defaults:
      run:
        shell: bash
        working-directory: ${{ matrix.dir }}
    steps:
      - uses: actions/checkout@v3
      - run: echo ${{ matrix.dir }}
      - uses: google-github-actions/auth@v1
        with:
          workload_identity_provider: <WORKFLOW_IDENTITY_PROVIDER_ID>
          service_account: <SERVICE_ACCOUNT>
      - uses: google-github-actions/setup-gcloud@v1
      # mercari/tfnotifyが頻繁に更新されていないため、mercari/tfnotifyのフォークのtfcmtを使う
      - run: |
          sudo curl -fL -o tfcmt.tar.gz https://github.com/suzuki-shunsuke/tfcmt/releases/download/$TFCMT_VERSION/tfcmt_linux_amd64.tar.gz
          sudo tar -C /usr/bin -xzf ./tfcmt.tar.gz
        env:
          TFCMT_VERSION: v4.0.0
      - uses: hashicorp/setup-terraform@v2.0.3
        with:
          terraform_version: 1.3.2
      - run: terraform fmt
        continue-on-error: true
      - run: terraform init
      # 長くなってしまうので、matrix.dirのenvironmentsは省いてtfcmtに渡す
      - run: tfcmt -var "target:$(echo ${{ matrix.dir }} | sed 's/environments\///g')" plan -patch -- terraform plan -no-color -lock=false
        env:
          TF_VAR_project_id: ${{ env.TF_VAR_project_id }}
          TF_VAR_org_id:  <ORGANIZATION_ID>
          TF_VAR_billing_account: <BILLING_ACCOUNT>
          PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
          PR_NUMBER: ${{ github.event.number }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

コマンドスクリプトを使って動的なワークフローにする

HRBrainのTerraformリポジトリは以下のような構成になっています。

├── environments
│   ├── dev
│   │   ├── app1
│   │   ├── app2
│   ├── prod
│   │   ├── app1
│   │   ├── app2
└── modules

各ディレクトリにTerraformの構成ファイルが存在しており、ディレクトリごとにterraform planを実行する必要があります。

元々リポジトリ直下のファイルに実行したいディレクトリを指定してterraform planを実行していましたが、そのファイルがコンフリクトしたり、手順がそもそも分かりづらく、Terraformリポジトリを触る開発者に混乱を招いていました。

そこで今回は、プルリクエストの差分から実行したいディレクトリを取得し、動的にワークフローを実行するようにしました。できるだけ依存関係を減らしたいので、実行したいディレクトリを取得するのにActionやプログラミング言語を使わず、コマンドスクリプトを使うよう工夫しました。

  check_changed_dirs:
    runs-on: ubuntu-20.04
    outputs:
      changes: ${{ steps.pr_dir_changes.outputs.changes }}
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - id: pr_dir_changes
        # masterと差分のあるディレクトリのみを抽出する
        # https://stackoverflow.com/questions/50440420/git-diff-only-show-which-directories-changed
        run: echo "changes="[$(git diff origin/"${GITHUB_BASE_REF}" --dirstat=files,0 | awk '{print $2}' | sed -e 's:/*$::' | grep 'environments' | awk '{ print "\""$0"\""}' | paste -sd, -)]"" >> "$GITHUB_OUTPUT"

ちなみにcheckout時にfetch-depth: 0を指定しないと動きません。ここで取得したディレクトリをfromJSONを使って、ジョブのマトリックスに設定します。

マトリックスを使用して、working-directoryに指定すると、ディレクトリごとにジョブを実行することができます。

  tf-ci:
    runs-on: ubuntu-20.04
    permissions:
      contents: 'write'
      id-token: 'write'
      pull-requests: 'write'
    timeout-minutes: 20
    needs: check_changed_dirs
    strategy:
      matrix:
        dir: ${{ fromJSON(needs.check_changed_dirs.outputs.changes) }}
    defaults:
      run:
        shell: bash
        working-directory: ${{ matrix.dir }}

このように並列でterraform planが実行できるので便利です。

マトリックスに基づいて複数ジョブが実行される

便利なtfcmtを使う

以前はmercari/tfnotifyを使っていて、特に問題がなかったのですが、今年の2月くらいからアップデートが止まっているようでした。調べたら、tfcmtというmercari/tfnotifyのフォークがあり、ちゃんとメンテナンスされてそうだったので、こちらを使いました。依存関係は増えますが、terraform planの内容をGitHub上にコメントしてくれるため、とても便利です。

      - run: tfcmt -var "target:$(echo ${{ matrix.dir }} | sed 's/environments\///g')" plan -patch -- terraform plan -no-color -lock=false
        env:
          PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
          PR_NUMBER: ${{ github.event.number }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

tfcmtはデフォルトの設定のままで、-var-patchオプションを使っています。-varオプションでターゲットを指定すると、コメントにどのディレクトリで実行したかを表示してくれます。-patchオプションは、追加コミット時にコメントを追加せず、元のコメントを更新してくれます。これにより、プルリクエスト上のコメントの可読性が上がります。

tfcmtによるコメント

コメント自体もシンプルなmercari/tfnotifyに比べて読みやすいです。

終わりに

GitHub Actionsを使って、プルリクエストの差分から動的にterraform planを実行する方法を説明しました。初めは、動的なワークフローを作ることが目的でしたが、tfcmtを使うことで、可読性も上げることができました。

今回のワークフローを作ったことにより、依存関係の自動アップデートツールなども導入できるようなります。機会があれば、その内容も記事にしてみたいと思います。

参考にした記事

zenn.dev

tech-blog.yayoi-kk.co.jp

zenn.dev