2026-01-21

GitHub Actions で terraform plan を自動実行して結果を PR にコメントとして表示する

GitHub Actions を使って PR 作成時に terraform plan を自動実行し、その結果を PR コメントとして表示するワークフローを作成しました。追加コミット時のコメント更新やトグル表示による見やすさの工夫についても紹介します。

概要

Terraform でインフラを管理している場合、コードの変更による PR を作成する際には、コードの差分だけでなく terraform plan の結果もあわせてレビューしたい場面が多くあります。しかし、手元で plan を実行してその結果を PR に貼り付けるのは面倒です。そこで、GitHub Actions を使って plan の自動実行と結果の表示を自動化しました。

前提

今回のワークフローは以下の環境を前提としています。

Terraform で AWS のリソースを管理しており、tfstate ファイルは Amazon S3 に保存しています。GitHub Actions から AWS へのアクセスには、アクセスキーを使わず OIDC による認証を採用しています。OIDC を使うことで、長期的なクレデンシャルを GitHub Secrets に保存する必要がなくなり、セキュリティが向上します。

OIDC 認証を利用するためには、事前に AWS 側で IAM Identity Provider と IAM Role を作成しておく必要があります。IAM Role には terraform plan の実行に必要な権限(対象リソースへの読み取り権限、S3 バックエンドへのアクセス権限など)を付与します。

ワークフローの特徴

今回作成したワークフローには以下のような特徴があります。

1. 追加コミット時は既存のコメントを更新

PR 作成後に追加のコミットが発生した場合、新たにコメントを追加するのではなく、既存の plan 結果コメントを更新する形にしています。これにより、PR のコメント欄が plan 結果で溢れることを防ぎ、常に最新の plan 結果だけが表示されるようになります。

2. トグル表示でスペースを節約

plan 結果は HTML の <details> タグを使ってトグル表示にしています。これにより、PR のコメント欄を圧迫せず、必要なときだけ展開して確認できるようになっています。

3. シンタックスハイライトの適用

plan 結果のコードブロックには hcl の言語指定をすることで、GitHub 上でシンタックスハイライトが適用されるようにしています。これにより、リソースの追加・変更・削除が視覚的に分かりやすくなります。

ワークフローの全体像

作成したワークフローの全体は以下の通りです。

name: Terraform Plan (infra)

on:
  pull_request:
    paths:
      - "infra/**"
      - ".github/workflows/infra-plan.yml"
  workflow_dispatch:

jobs:
  plan:
    runs-on: ubuntu-latest

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

    env:
      TF_IN_AUTOMATION: "true"

    steps:
      - name: Checkout
        uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
        with:
          ref: ${{ github.head_ref }}
          fetch-depth: 0

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3

      - name: Write tfvars
        working-directory: infra/environments/prd
        run: |
          cat > terraform.auto.tfvars <<'EOF'
          hosted_zone_id = "${{ secrets.TF_VARS_HOSTED_ZONE_ID }}"
          github_actions_oidc_provider_arn = "${{ secrets.TF_VARS_GHA_OIDC_PROVIDER_ARN }}"
          EOF

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5
        with:
          role-to-assume: ${{ vars.TF_PLAN_ROLE_ARN }}
          aws-region: ap-northeast-1
          role-session-name: tf-plan-${{ github.run_id }}

      - name: Terraform init
        working-directory: infra/environments/prd
        run: terraform init -input=false

      - name: Terraform plan
        id: plan
        continue-on-error: true
        working-directory: infra/environments/prd
        run: |
          set -o pipefail
          terraform plan -input=false -no-color -var-file=terraform.auto.tfvars -out=tfplan 2>&1 | tee plan.log
          echo "exitcode=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"

      - name: Terraform show
        if: always()
        working-directory: infra/environments/prd
        run: |
          if [ -f tfplan ]; then
            terraform show -no-color tfplan > plan.txt
          fi

      - name: Prepare plan comment
        if: always()
        id: comment
        working-directory: infra/environments/prd
        run: |
          {
            echo "## Terraform Plan (prd)"
            echo
            echo "- Commit: [\`${{ github.event.pull_request.head.sha }}\`](https://github.com/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha }})"
            echo
            if [ "${{ steps.plan.outputs.exitcode }}" != "0" ]; then
              echo "**Plan failed (exit code ${{ steps.plan.outputs.exitcode }})**"
            else
              echo "**Plan succeeded**"
            fi
            echo
            echo "<details><summary>Plan output</summary>"
            echo
            echo '```hcl'
            if [ -f plan.txt ]; then
              cat plan.txt
            else
              cat plan.log
            fi
            echo '```'
            echo
            echo "</details>"
          } > plan-comment.md
          echo "body_path=infra/environments/prd/plan-comment.md" >> "$GITHUB_OUTPUT"

      - name: Find existing plan comment
        if: always()
        id: find-comment
        uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4
        with:
          issue-number: ${{ github.event.pull_request.number }}
          comment-author: "github-actions[bot]"
          body-includes: "Terraform Plan (prd)"

      - name: Comment on PR
        if: always()
        uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
          issue-number: ${{ github.event.pull_request.number }}
          comment-id: ${{ steps.find-comment.outputs.comment-id }}
          body-path: infra/environments/prd/plan-comment.md

      - name: Fail if plan errored
        if: steps.plan.outputs.exitcode != '0'
        run: exit 1

ワークフローの解説

各ステップについて詳しく解説します。

トリガーの設定

on:
  pull_request:
    paths:
      - "infra/**"
      - ".github/workflows/infra-plan.yml"
  workflow_dispatch:

infra/ ディレクトリ配下のファイルまたはワークフローファイル自体が変更された場合にのみ実行されるようにしています。これにより、Terraform に関係のない変更で不要な plan が実行されることを防いでいます。また、workflow_dispatch を追加することで、手動での実行も可能にしています。

必要な権限の設定

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

このワークフローでは以下の 3 つの権限が必要です。

AWS 認証の設定

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5
  with:
    role-to-assume: ${{ vars.TF_PLAN_ROLE_ARN }}
    aws-region: ap-northeast-1
    role-session-name: tf-plan-${{ github.run_id }}

AWS への認証には aws-actions/configure-aws-credentials アクションを使用しています。role-to-assume に plan 実行用の IAM Role の ARN を指定することで、OIDC を使った認証が行われます。

IAM Role の ARN は GitHub リポジトリの Variables(vars.TF_PLAN_ROLE_ARN)に設定しています。Secrets ではなく Variables を使っているのは、Role ARN 自体は機密情報ではないためです。一方、tfvars に含まれる値(今回でいうと TF_VARS_HOSTED_ZONE_ID など)は Secrets に設定しています。

role-session-name には tf-plan-${{ github.run_id }} を指定しており、CloudTrail などで確認する際にどのワークフロー実行からのアクセスかを識別しやすくしています。

Terraform plan の実行

- name: Terraform plan
  id: plan
  continue-on-error: true
  working-directory: infra/environments/prd
  run: |
    set -o pipefail
    terraform plan -input=false -no-color -var-file=terraform.auto.tfvars -out=tfplan 2>&1 | tee plan.log
    echo "exitcode=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"

continue-on-error: true を設定することで、plan が失敗してもワークフロー全体が中断されないようにしています。これにより、plan が失敗した場合でもコメントを投稿できます。また、set -o pipefail${PIPESTATUS[0]} を使って tee コマンドを経由しても正確な終了コードを取得しています。

plan 結果の整形

- name: Terraform show
  if: always()
  working-directory: infra/environments/prd
  run: |
    if [ -f tfplan ]; then
      terraform show -no-color tfplan > plan.txt
    fi

terraform plan の出力には進捗表示などのノイズが含まれるため、terraform show を使って plan ファイルから整形された出力を取得しています。これにより、PR コメントに表示される plan 結果がより読みやすくなります。

コメントの作成

- name: Prepare plan comment
  if: always()
  id: comment
  working-directory: infra/environments/prd
  run: |
    {
      echo "## Terraform Plan (prd)"
      echo
      echo "- Commit: [\`${{ github.event.pull_request.head.sha }}\`](https://github.com/${{ github.repository }}/commit/${{ github.event.pull_request.head.sha }})"
      echo
      if [ "${{ steps.plan.outputs.exitcode }}" != "0" ]; then
        echo "**Plan failed (exit code ${{ steps.plan.outputs.exitcode }})**"
      else
        echo "**Plan succeeded**"
      fi
      echo
      echo "<details><summary>Plan output</summary>"
      echo
      echo '```hcl'
      if [ -f plan.txt ]; then
        cat plan.txt
      else
        cat plan.log
      fi
      echo '```'
      echo
      echo "</details>"
    } > plan-comment.md

コメントには以下の情報を含めています。

terraform show の出力がある場合はそちらを使い、ない場合(plan が途中で失敗した場合など)は terraform plan の出力を使うようにしています。

既存コメントの検索と更新

- name: Find existing plan comment
  if: always()
  id: find-comment
  uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4
  with:
    issue-number: ${{ github.event.pull_request.number }}
    comment-author: "github-actions[bot]"
    body-includes: "Terraform Plan (prd)"

- name: Comment on PR
  if: always()
  uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5
  with:
    token: ${{ secrets.GITHUB_TOKEN }}
    issue-number: ${{ github.event.pull_request.number }}
    comment-id: ${{ steps.find-comment.outputs.comment-id }}
    body-path: infra/environments/prd/plan-comment.md

peter-evans/find-comment アクションを使って、既存の plan 結果コメントを検索しています。検索条件として、コメントの投稿者が github-actions[bot] であること、およびコメント本文に Terraform Plan (prd) が含まれていることを指定しています。

既存のコメントが見つかった場合は comment-id にその ID が設定され、peter-evans/create-or-update-comment アクションがそのコメントを更新します。見つからなかった場合は新規にコメントを作成します。

ワークフローの最終結果

- name: Fail if plan errored
  if: steps.plan.outputs.exitcode != '0'
  run: exit 1

plan が失敗した場合でもコメントを投稿するために continue-on-error: true を設定していますが、最終的にはワークフロー自体を失敗させたいため、最後に終了コードをチェックして必要に応じて失敗させています。

実際の表示例

ワークフロー実行後、PR には以下のようなコメントが投稿されます。

plan 結果のコメント表示例

plan 結果がトグル表示されているため、コメント欄がコンパクトに保たれます。

まとめ

GitHub Actions を使って terraform plan を自動実行し、結果を PR コメントとして表示するワークフローを作成しました。

工夫した点として、追加コミット時には既存のコメントを更新する形にすることで PR のコメント欄を整理し、トグル表示でスペースを節約しつつ、hcl のシンタックスハイライトで見やすさを向上させています。

このようなワークフローを導入することで、Terraform の変更を含む PR のレビューがより効率的になります。コードの差分だけでなく、実際のインフラへの影響を plan 結果として確認できるため、レビュー時の見落としを防ぐことができます。