🙆

(2/2)CI/CD GitHub Actionsでインフラを自動デプロイ!

に公開

今までの記事

目次

前回の続きから

フェーズ3:feature push用にplanも追加したいのでその編集を開始

既存のファイル修正箇所

yml
+ TFLINT_PLUGIN_DIR: /home/runner/.tflint.d/plugins
+ AWS_REGION: ap-northeast-1
+ HUB_ACCOUNT_ID: "318574063927"
+ STATE_BACKEND: "dev"
+ WORKDIR_DEV: "infra/static-website/environments/dev"

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

# name: Check for existing PR
-           if (prs.length > 0) {
-             console.log(`Found existing PR #${prs[0].number}`);
-             return prs[0].number;
-           }
-           return '';
-         result-encoding: string
+           core.setOutput('number', prs.length ? prs[0].number.toString() : '');

その他修正点

  • 環境変数を出来るだけ使うようにした
  • terraform fmf,validate,tflinkをdev環境にした
core.setOutput('number', prs.length ? prs[0].number.toString() : '');
  • core.setOutput():actions/giyhub-scriptが提供する@actions/coreライブラリのメソッド。
    • GitHub Actionsのワークフロー内で値を出力する。
      • 後ほどsteps.<id>.outputs.numberのような形で参照出来る。
  • length:配列の要素数を表すプロパティ。
    • 今回はps内に格納されている配列の要素数をカウント。
  • ?:三項演算子でif文の省略系(<条件>?<真>:<偽>)
    • prs.lengthは値が0とか値がない場合はfalseとなる。
      • なのでこの場合はprs.lengthが条件の役割を満たし、真と偽の処理に分かれる。
  • prs[0].number.toString()prs[0]が配列の最初の要素を取得し、.numberそのオブジェクトのnumberプロパティを取得し、,toStringその数値を文字列に変換
    • この一連が真の場合の処理。
  • '':空文字を表す。
    • 今回の場合は偽の時の処理がこれになる。

planのための追加分の記載

      # 👇 fmt/validate/tflint の outcome を後続ジョブへ渡す
      - name: Collect step outcomes
        id: collect
        run: |
          echo "fmt=${{ steps.fmt.outcome }}"       >> "$GITHUB_OUTPUT"
          echo "validate=${{ steps.validate.outcome }}" >> "$GITHUB_OUTPUT"
          echo "tflint=${{ steps.tflint.outcome }}" >> "$GITHUB_OUTPUT"

      # 👇 3つ全部 success のときだけ plan を許可するフラグを出力
      - name: Decide plan gate
        id: gate
        run: |
          ok=true
          [ "${{ steps.fmt.outcome }}" = "success" ] || ok=false
          [ "${{ steps.validate.outcome }}" = "success" ] || ok=false
          [ "${{ steps.tflint.outcome }}" = "success" ] || ok=false
          echo "lint_ok=$ok" >> "$GITHUB_OUTPUT"
  • outcomes:そのステップの実行結果を表す文字列。
    • steps.<step_id>.outcomeの形で参照出来て、取れる値はsuccess/failure/cancelled/skipped
  • echo "fmt=${{ steps.fmt.outcome }}" >> "$GITHUB_OUTPUT":outcomeの出力をfmtをキーとして、$GITHUB_OUTPUTへ追記している。
  • ok=true:okの値はtrueとして初期設定をする
  • [...]:testコマンド。
  • ||:左側が失敗となったら右側を実行するシェルのOR演算子。
    • 今回の場合は左の条件式が成立しなかたったら、右のok=falseを実行する。
  • echo "lint_ok=$ok" >> "$GITHUB_OUTPUT":lint_okをキーとし、okを変数として$GITHUB_OUTPUTへ追記している。
  # ---- dev だけ Terraform Plan(Hub→dev ロールチェーン)。lint成功時のみ ----
  plan-dev:
    needs: [lint-summary]
    if: ${{ needs.lint-summary.outputs.lint_ok == 'true' }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: read
      issues: write
      id-token: write

    steps:
      - uses: actions/checkout@v4
        with:
          persist-credentials: false

      # ✅ Terraform を 1.9.2 でセットアップ
      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.2

      # ✅ jq をインストール(バージョン表示は任意)
      - name: Install jq
        run: |
          sudo apt-get update
          sudo apt-get install -y jq
          echo "Using jq: $(jq --version)"

      # (任意・安定化)キャッシュディレクトリを先に作成
      - name: Create plugin cache dir
        run: mkdir -p "${{ env.TF_PLUGIN_CACHE_DIR }}"

      - name: Cache Terraform plugins
        uses: actions/cache@v4
        with:
          path: ${{ env.TF_PLUGIN_CACHE_DIR }}
          key: ${{ runner.os }}-tfplugins-${{ hashFiles('**/.terraform.lock.hcl') }}
          restore-keys: ${{ runner.os }}-tfplugins-

      # 1) まずハブアカウントに OIDC で Assume
      - name: Assume Hub (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ env.HUB_ACCOUNT_ID }}:role/GitHubActionsHubRole
          role-session-name: gha-hub-${{ github.run_id }}-${{ github.actor }}
          aws-region: ${{ env.AWS_REGION }}

      # init(既定)
      - name: Terraform Init (backend in DEV)
        working-directory: ${{ env.WORKDIR_DEV }}
        run: terraform init -input=false -lockfile=readonly -upgrade=false
  • needs: [lint-summary]needsは、このジョブを動かす前に完了していて欲しいジョブを宣言する場所。
    • 依存関係(実行順序)を作り、並列実行がデフォルトのジョブを順次実行に変えたり、前段ジョブの出力を参照出来るようにする。
  • if: ${{ needs.lint-summary.outputs.lint_ok == 'true' }}:GitHub ActionsのExpression構文で、文字列としてではなく式として評価→値に置き換えられる。
    • ifなので参照先がtrueならこのジョブの実行をする。
  • needs.lint-summary.outputs.lint_ok:依存ジョブのアウトプットのlint_okキーのものを参照する。
  • id-token: write:Assumeロールするのに必要な権限。
  • actions/checkout@v4:リポジトリのコードをランナー環境にチェックアウト(取得)するための公式アクション。
  • persist-credentials: false:actions/checkoutの入力オプションで、チェックアウト時に設定される認証情報(GITHUB_TOKENなど)をローカルのGit設定に残さない設定。
      - name: Install jq
        run: |
          sudo apt-get update
          sudo apt-get install -y jq
          echo "Using jq: $(jq --version)"
  • jq:JSONをコマンドラインツールで検索・変換・整形するためのフィルタ言語ツール。
  • apt:Debian/Ubuntu系のLinuxでパッケージをインストール・管理するパッケージマネージャー。
  • sudo apt-get update:aptのアップデートを行う。
  • sudo apt-get install -y jq:jqを非対話でインストールするコマンド。
  • echo "Using jq: $(jq --version)":jqのバージョンを取得してログに出すためのコマンド。

ポイント
jqをインストールするために、aptをアップデートしてインストールして、バージョンをログとして出力させる。

  • aws-actions/configure-aws-credentials@v4:GitHubが発行するOIDC IDトークンを使い、AWS STSのAssumeRoleWithWebIdentityを実行する。
    • 指定のロールを引き受けた一時クレデンシャルを環境変数にエクスポートする。
      • これでaws cliやterraformなどはサインイン済みとして動作する。
  • AssumeRoleWithWebIdentity:AWS STSのAPI機能でOIDCなどのwebアイデンティティトークンを提示してIAMロールを一時的に引き受けるための仕組み。
    • 長期アクセスキーを配らずに外部IDプロバイダ(GitHub OIDCなど)から来たワークロードに短期クレデンシャルを発行出来る。
  • role-to-assume:Asuumeする際のロールのARN名。
  • role-session-name:Assumeした際のセッション名で、このセッション名がTrailとかに記録される。
  • aws-region:Assumeする際のリージョンを指定する。

上記1ブロック要約
Lint-summaryが終わったら始まるジョブで、ifの条件がlint_okのアウトプットがtrueであること。
つまりfmf,varidate,tflinkがsuccessならばジョブを開始する。
今回planも実行するので、id-tokenの権限を付与する。
terraform showはJSONで出力されるので、それを編集するためのjqをインストールし
terraformをセットアップし、OIDCでAssumeするための記述も行っている。

      - name: Terraform Plan (dev)
        id: tfplan
        working-directory: ${{ env.WORKDIR_DEV }}
        run: |
          set -eo pipefail
          terraform plan -no-color -input=false -lock-timeout=60s -out=tf.plan | tee plan.txt
          terraform show -json tf.plan > tfplan.json

          creates=$(jq '[.resource_changes[]? | select(.change.actions | index("create"))] | length' tfplan.json)
          replaces=$(jq '[.resource_changes[]? | select(.change.actions | index("replace"))] | length' tfplan.json)
          updates=$(jq '[.resource_changes[]? | select(.change.actions | index("update"))] | length' tfplan.json)
          deletes=$(jq '[.resource_changes[]? | select(.change.actions | index("delete"))] | length' tfplan.json)

          creates=$((creates + replaces))
          deletes=$((deletes + replaces))

          echo "creates=$creates" >> $GITHUB_OUTPUT
          echo "updates=$updates" >> $GITHUB_OUTPUT
          echo "deletes=$deletes" >> $GITHUB_OUTPUT
  • -input=false:対話入力を無効化するもの。
  • -lockfile=readonly:ロックファイルを書き換え無い設定。
    • 書き換えてしまったら想定と違う状態でinitされるし、書き換えたとしてもその反映をローカルに持ち帰れる訳では無い。
  • -upgrade=false:既存のロックファイル内でinitをする。
    • アップグレードをしない。
  • set -e
  • set -eo pipefail:そのシェル内で、このコマンド以降にエラーや失敗が起こると動作を停止させるコマンド。
    • GitHub Actions内ならrun:ステップの中だけで有効。
      • set:シェルの動作オプションを切り替え
      • -e:スクリプト内で実行される任意のコマンド失敗した場合にスクリプト全体を終了させる。
        • |パイプの処理だと、最後の処理が成功すれば問題ないとみなされる。
      • -o pipefail|で繋ぐパイプライン内のいずれかのコマンドが失敗した場合に全体のパイプラインを失敗扱いにする。
terraform plan -no-color -input=false -lock-timeout=60s -out=tf.plan | tee plan.txt
  • -no-color:カラー情報を入れないオプション。
    • ログを見る時は見にくくなる。
  • -input=false:非対話での実行を指定する。
  • -lock-timeout=60s:stateロックが掛かっていても60秒待機する。
  • -out=tf.plan:人間には読めないplan結果のバイナリファイルを出力させる。
    • これをレビューすればplan→レビューapplyのズレが最小限になる。
  • | tee plan.txt-out=tf.planの出力結果を、画面に出力させながらplan.txtへ書き込む。
    • |:前のコマンドの標準出力をteeへ受け渡す
    • tee:画面に表示しつつ、同時にファイルへの書き込みも行う。

ポイント
terraformの実行計画を立てるコマンドで、色情報は抜いて、非対話で、ロック待機60秒で、planのバイナリファイルをplan.txtへ書き込む。

terraform show -json tf.plan > tfplan.json
  • terraform show:Terraformの表示コマンド。
    • planファイルやstateを人間が読めたり、機械が読みやすいように整形して出力する。
  • -jsonterraform showのオプションで、出力を機械可読可能なJSON形式にする。
  • tf.plan:この引数がなければ現在のstateから出力するが、引数を指定いるためtf.planから出力する。
  • >:リダイレクト記号で、指定のファイルに上書きする。
    • >>は追記。
  • tfplan.json:出力先ファイル名

ポイント
terraformのバイナリファイルや、stateを出力形式を決めて出力出来て、前回コマンドで出力したplanのバイナリファイルを読み込んでJSON形式で出力させる。
それをtfplan.jsonがあれば上書きで、なければ作成する。

          creates=$(jq '[.resource_changes[]? | select(.change.actions | index("create"))] | length' tfplan.json)
          replaces=$(jq '[.resource_changes[]? | select(.change.actions | index("replace"))] | length' tfplan.json)
          updates=$(jq '[.resource_changes[]? | select(.change.actions | index("update"))] | length' tfplan.json)
          deletes=$(jq '[.resource_changes[]? | select(.change.actions | index("delete"))] | length' tfplan.json)

          creates=$((creates + replaces))
          deletes=$((deletes + replaces))
  • creates=$()()内の計算出力を代入する。
  • jq:jqコマンドの開始。
    • jqはJSONクエリ言語。
  • '[...]':jqのフィルタ式(シングルクォートで囲む)
  • .resource_changes[]?:トップオブジェクトからresource_changesキーを検索し、その中の要素を繰り返し取り出す。
    もし配列が空でも、エラーとならずに空配列として出力する。
    • .jqのデフォルトコンテキストで、クエリの開始地点としてJSON全体(ルートオブジェクト)を指す。
      • 明示的に書いていないが、.resource_changesはルートからの相対パス。
    • resource_changesJSONファイル内のトップレベルのキー。
    • []配列イテレータって名前のjqのフィルタ内で動作するjqの演算子。
      • 配列の各要素を1つずつ取り出し、ストリームとして出力。
        • ストリーム処理は1個ずつ受け渡す。
    • ?配列がからでもエラーとならずに空配列を出力。
    • |は、左の出力の値を右へ受け渡す。
  • select(.change.actions | index("create"))
    • select(...):は中の値を評価し、真ならその要素を残し、偽なら捨てる。
      • jqではfalseとnull以外は真(数値の0も真)。
    • .:今回で言うと、resource_changesの中から抽出した要素の中のパスが一番上からって意味。
    • change.actions:changeオブジェクトのactionsのフィールドを抽出してる。
    • |:は左の出力結果を右に受け渡す。
    • index("create")actionsのフィールドの中をindexして先頭から何番目にあるかの値を出力する。
      • 無ければnullを返す。
        • nullを返すのでストリーム処理で1要素ずつ、真で残るか、偽で捨てられるか決まる。
  • | length' tfplan.json
    • |:左の値を右に受け渡す。
    • length:配列の要素すうをカウントする関数。
    • ':jqに指示しているフィルタ部分の終端。
  • terraformのステータス
    • create:新規作成。
      • 新しいリソースを作成した状態。
    • replace
      • 既存のリソースを削除して、作成した状態。
    • update:そのまま更新。
      • 既存のリソースの変更が不要な、属性変更の状態。
    • delete:削除。
      • リソースを削除した状態。
  • creates=$((creates + replaces)):リプレイスは既存リソースを削除して作成なので、作成にカウントするために足して、それをcreatesへ代入する。
  • deletes=$((deletes + replaces)):リプレイスは既存リソースを削除して作成なので、削除にもカウントするために足してdeletesへ代入する。
  • echo "creates=$creates" >> $GITHUB_OUTPUT:createsをキーとして、createsを`GITHUB_OUTPUT`へ出力する。

上記1ブロック要約
エラーが発生する時点で処理が止まるように設定し、処理を開始。
planでの出力は色なし、非対話、ロック待機60秒、でplan結果のバイナリファイルをtf.planに出力し、その出力をplan.txtへ書き込む。
terraform showでplanのバイナリファイルをJSON形式でtfplan.jsonへ書き込む。
そのjsonファイルをjqでクエリをする。内容はresource_changesキー内の要素の数だけ繰り返し処理が行われて、change内のactionsのフィールドをindexで検索しnullならselectで捨てる。その配列の要素数をカウントして、変数へ代入する。
変数replacesは作成と削除に対応するので、どちらにもカウントする。
その後それぞれを$GITHUB_OUTPUTへ追記する形で出力する。

      - name: Build plan comment (dev)
        id: body
        working-directory: ${{ env.WORKDIR_DEV }}
        run: |
          {
            echo "### Terraform Plan \`dev\`  (+${{ steps.tfplan.outputs.creates }} / ~${{ steps.tfplan.outputs.updates }} / -${{ steps.tfplan.outputs.deletes }})"
            echo
            echo "**Base:** \`main\`  | **Ref:** \`${GITHUB_SHA::7}\`  | **Dir:** \`${{ env.WORKDIR_DEV }}\`"
            echo
            echo "<details><summary>Show full plan</summary>"
            echo
            echo '```'
            cat plan.txt
            echo '```'
            echo
            echo "</details>"
          } > body.md

      - name: Find existing PR (for comment)
        id: find-pr
        uses: actions/github-script@v7
        with:
          script: |
            const branch = context.ref.replace('refs/heads/', '');
            const { data: prs } = await github.rest.pulls.list({
              owner: context.repo.owner,
              repo: context.repo.repo,
              head: `${context.repo.owner}:${branch}`,
              base: 'main',
              state: 'open'
            });
            core.setOutput('number', prs.length ? prs[0].number.toString() : '');

      - name: Comment plan to PR (dev)
        if: steps.find-pr.outputs.number != ''
        run: gh pr comment ${{ steps.find-pr.outputs.number }} --body-file "${{ env.WORKDIR_DEV }}/body.md"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

      - name: Attach plan snippet to Job Summary (dev)
        run: |
          echo "## 🧮 Plan dev (+${{ steps.tfplan.outputs.creates }} / ~${{ steps.tfplan.outputs.updates }} / -${{ steps.tfplan.outputs.deletes }})" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "<details><summary>Show full plan</summary>" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo '```' >> $GITHUB_STEP_SUMMARY
          sed -n '1,400p' "${{ env.WORKDIR_DEV }}/plan.txt" >> $GITHUB_STEP_SUMMARY
          echo '```' >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          echo "</details>" >> $GITHUB_STEP_SUMMARY
  • } > body.md{...}内の記載をbody.mdへ上書きで書き込み。
  • const branch = context.ref.replace('refs/heads/', '');:ブランチ名をbranch変数へ格納する。
    • replaceメソッドで、文字列の先頭からrefs/heads/を空文字に置き換えることで実質的に削除を行う。
      • そのことによってブランチ名だけを取り出す。
  • const { data: prs } = await github.rest.pulls.list({:引数で指定しているPRの一覧を取得して、その中のdataプロパティを変数prsへ格納する。
    • それをconstで定数としている。
      • github.rest.pulls.listactions/github-script@v7で呼び出せる機能の1つで、PRの一覧を取得する。
        • 引数はオーナー名、リポジトリ名、ここからここ宛のPRでオープンになってるものを指定している。
    • data: prsはその中のdetaプロパティをprs変数へ格納している。
  • core.setOutput('number', prs.length ? prs[0].number.toString() : '');
    • core.setOutput():GitHub Actionsの@actions/coreライブラリが提供する関数で、現在のステップから他のステップへデータを渡すために使用する。
    • 'number':出力変数名のキー。
    • prs.length:PRの配列の要素数カウント。
    • ?:三項演算子。条件 ? 真の場合の値 : 偽の場合の値
      • prs.lengthが0の時は偽と評価される。
    • prs[0].number.toString():真の時はprsの最初のPRの.numberプロパティを文字列に変換して出力。
      • 偽の時は空文字を出力。
  • if: steps.find-pr.outputs.number != '':find-prステップの出力が空文字では無い場合が真。
    • つまりPRがある状態が真として処理を開始する。
run: gh pr comment ${{ steps.find-pr.outputs.number }} --body-file "${{ env.WORKDIR_DEV }}/body.md"
  • gh prコメント
  • gh:GitHub CLIのコマンド。
  • prghのサブコマンドで、『Pull Requestに関する操作』を使う宣言。
  • comment:さらにその下のサブコマンドで、『PRにコメントを書く』と言う意味。
  • ${{ steps.find-pr.outputs.number }}:PRのナンバーを指定する。
    • ghコマンドは自動的に現在のリポジトリを検出するので、ナンバーを指定するだけで良い。
  • --body-file:コメントの内容をファイルから読み込むためのオプション。
    • この記述がないと直接のコメント記載となる。(gh pr comment 123 --body "これはコメントです"
  • "${{ env.WORKDIR_DEV }}/body.md":ファイルがあるパスを記載する。

ポイント
GitHub CLIでプルリクにコメントを記載する

  • GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    • GITHUB_TOKENghコマンドが認証としてGITHUB_TOKENを探すので、GITHUB_TOKEN変数とする。
    • secrets.GITHUB_TOKENGitHub Actionsのワークフロー実行時に認証情報が渡される。
      • それをghコマンドが使える形に変数へ代入する。
    • envとrunの順番どちらでもOK。
      • ステップの順番は気を付ける必要がある。
  • sed -n '1,400p' "${{ env.WORKDIR_DEV }}/plan.txt" >> $GITHUB_STEP_SUMMARY
    • sed:テキストをストリーム処理するためのコマンド。
      • ファイルを開かずとも編集できる。
    • -n:自動出力を抑止し、明示的に出力指示した行だけを出すことが出来る。
    • 1,400:1〜400行を指定
    • p:その範囲を出力する
    • "${{ env.WORKDIR_DEV }}/plan.txt":処理するファイルのパス。
    • >> $GITHUB_STEP_SUMMARY:GITHUB_STEP_SUMMARYへリダイレクト(追記)。

上記1ブロック要約
プラン結果のcreates、updates、deletesの数を出力する。
次にPRが存在すれば、そのコメントにプラン結果を出力する。

  comment-why-skip:
    needs: [lint-summary]
    if: ${{ needs.lint-summary.outputs.lint_ok != 'true' && needs.lint-summary.outputs.pr_number != '' }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - name: Explain why plan was skipped
        run: |
          gh pr comment ${{ needs.lint-summary.outputs.pr_number }} --body "$(cat <<'MD'
          ### ⏭️ Plan skipped
          Lint/Validate/TFLint のいずれかが失敗したため、この push では **Terraform plan を実行していません**。

          - Format:  ${{ needs.lint-summary.outputs.fmt }}
          - Validate: ${{ needs.lint-summary.outputs.validate }}
          - TFLint:   ${{ needs.lint-summary.outputs.tflint }}

          失敗箇所を修正後、再 push してください。
          MD
          )"
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  • needs: [lint-summary]:lint-summaryステップが終わるまでジョブは走らない。
if: ${{ needs.lint-summary.outputs.lint_ok != 'true' && needs.lint-summary.outputs.pr_number != '' }}
  • if:この条件が偽ならこのジョブを実行しないと言う条件分岐。
  • needs.lint-summary.outputs.lint_ok:lint-summaryステップのアウトプットを参照している。
    • needs:依存関係にある他のジョブを参照する。
    • lint-summary:参照するジョブ名。
    • outputs:そのジョブが出力した値。
    • lint_ok:出力の変数名
  • != 'true':trueじゃないのが真。
    • つまりfmt、validate、tflintのどれかがfalseなら真になる。
  • &&:ifの条件が『fmt、validate、tflintが失敗』かつ『PRが存在する』場合って言う、2つの条件を満たした時。

ポイント
fmt、validate、tflintが失敗した時かつ、PRが存在する場合にジョブを実行する。

処理の流れ
ランナーはrun:ブロックの文字列を受け取り、まず${{...}}(Actionsの式)を置換する。
この時点では、まだシェルは動いていない。あくまで文字列の前処理。
ヒアドキュメントでは、区切り語をクォートしてるかどうかで、bashが本文に対して行う展開処理が切り替わる。

  • <<:ヒアドキュメントを始める合図で、複数行の文字列をそのままコマンドの標準入力に渡すために使える
  • MD:区切り語。ヒアドキュメントの開始。
  • "$(...)":コマンド置換の記法。さらにダブルクォートで囲むと改行してもコマンドの区切りにはならない。
  • '...':今回はシングルクォートで囲ってるので、変数展開せずに出力される。
    • ただし${{...}}のようなGitHub Actionsの式展開はされる。
  • GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}:ghコマンドのための認証を使えるようにGITHUB_TOKEN変数に格納する。

上記1ブロック要約
『fmt、validate、tflintが失敗』かつ『PRが存在する』場合にPRに対して失敗した理由をコメントする。

feature push用の既存のコード修正とplan実行を追加したのをテスト

Error: Cannot assume IAM Role

  with provider["registry.terraform.io/hashicorp/aws"].dns_master,
  on providers.tf line 23, in provider "aws":
  23: provider "aws" {

IAM Role (arn:aws:iam::252170044718:role/Route53CrossAccountManagerRole)
cannot be assumed.

There are a number of possible causes of this - the most common are:
  * The credentials used in order to assume the role are invalid
  * The credentials do not have appropriate permission to assume the role
  * The role ARN is not valid

Error: operation error STS: AssumeRole, https response error StatusCode: 403,

上記エラー内容

踏み台アカウントでGitHubとAssumeロールを作成したが、DNS管理アカウントと信頼関係を結んでいなかった。
DNSアカウントロールの信頼されたエンティティにGitHubロールを追加。
GitHubロールにAssumeを許可するロールにDNSアカウントロールを追加。

2回目feature push用の既存のコード修正とplan実行を追加したのをテスト

🔍 Terraform Check Results\n\n| Check | Status |\n|-------|--------|\n| Format | ✅ Pass |\n| Validate | ✅ Pass |\n| TFLint | ✅ Pass |\n

summaryの文字が崩れている。
Bash はダブルクォート内の \n を改行に展開されないのでechoで1行ずつ挿入し、catの出力にするように修正。

  run: |
    {
      echo "### 🔍 Terraform Check Results"
      echo
      echo "| Check | Status |"
      echo "|-------|--------|"

      if [ "${{ steps.fmt.outcome }}" = "success" ]; then
        echo "| Format   | ✅ Pass |"
      else
        echo "| Format   | ❌ Fail |"
      fi

      if [ "${{ steps.validate.outcome }}" = "success" ]; then
        echo "| Validate | ✅ Pass |"
      else
        echo "| Validate | ⚠️ Warning |"
      fi

      if [ "${{ steps.tflint.outcome }}" = "success" ]; then
        echo "| TFLint   | ✅ Pass |"
      else
        echo "| TFLint   | ⚠️ Warning |"
      fi
    } > summary.md

    {
      echo "summary<<EOF"
      cat summary.md
      echo "EOF"
    } >> "$GITHUB_OUTPUT"

3回目feature push用の既存のコード修正とplan実行を追加したのをテスト

Error: Unhandled error: HttpError: Resource not accessible by integration

PRへのコメントの箇所でエラー。
pull-requests権限をreadにしていたので、writeに修正

4回目feature push用の既存のコード修正とplan実行を追加したのをテスト

Run gh pr comment 13 --body-file "infra/static-website/environments/dev/body.md"
GraphQL: Resource not accessible by integration (addComment)
Error: Process completed with exit code 1.

やはり権限エラー。
pull-requestsが読み取り権限のみになってたので修正。

5回目feature push用の既存のコード修正とplan実行を追加したのをテスト

成功した!

infra配下の変更でfeature/*からpushすると
fmf,validate,tflink,を実施して、summaryとPRのコメントへチェック結果を記載。
そしてplanも実施してplan結果をsummaryとPRのコメントへ結果を記載。

フェーズ4:codexで/infra変更でmainのpushワークフローを作成

terraform-deploy-dev-on-main.yml
name: Terraform Deploy dev on main

on:
  push:
    branches: ['main']
    paths:
      - 'infra/**'
      - '.terraform-version'
      - '.github/workflows/terraform-deploy-dev-on-main.yml'

permissions:
  contents: read
  id-token: write

env:
  TF_PLUGIN_CACHE_DIR: /home/runner/.terraform.d/plugin-cache
  AWS_REGION: ap-northeast-1
  HUB_ACCOUNT_ID: "318574063927"
  WORKDIR_DEV: "infra/static-website/environments/dev"

concurrency:
  group: deploy-dev-main
  cancel-in-progress: false

jobs:
  plan-apply-dev:
    name: Plan + Apply (dev)
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform 1.9.2
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.2

      - name: Install jq
        run: |
          sudo apt-get update
          sudo apt-get install -y jq

      - name: Prepare plugin cache
        run: mkdir -p "${{ env.TF_PLUGIN_CACHE_DIR }}"

      - name: Cache Terraform plugins
        uses: actions/cache@v4
        with:
          path: ${{ env.TF_PLUGIN_CACHE_DIR }}
          key: ${{ runner.os }}-tfplugins-${{ hashFiles('**/.terraform.lock.hcl') }}
          restore-keys: ${{ runner.os }}-tfplugins-

      - name: Assume Hub (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ env.HUB_ACCOUNT_ID }}:role/GitHubActionsHubRole
          role-session-name: gha-deploy-dev-${{ github.run_id }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Terraform Init (dev backend)
        working-directory: ${{ env.WORKDIR_DEV }}
        run: terraform init -input=false -lockfile=readonly -upgrade=false

      - name: Terraform Plan (dev)
        id: plan
        working-directory: ${{ env.WORKDIR_DEV }}
        shell: bash
        run: |
          set -eo pipefail
          terraform plan -no-color -input=false -lock-timeout=60s -out=tf.plan | tee plan.txt
          terraform show -json tf.plan > tfplan.json

          creates=$(jq '[.resource_changes[]? | select(.change.actions | index("create"))] | length' tfplan.json)
          replaces=$(jq '[.resource_changes[]? | select(.change.actions | index("replace"))] | length' tfplan.json)
          updates=$(jq '[.resource_changes[]? | select(.change.actions | index("update"))] | length' tfplan.json)
          deletes=$(jq '[.resource_changes[]? | select(.change.actions | index("delete"))] | length' tfplan.json)

          creates=$((creates + replaces))
          deletes=$((deletes + replaces))

          echo "creates=$creates" >> "$GITHUB_OUTPUT"
          echo "updates=$updates" >> "$GITHUB_OUTPUT"
          echo "deletes=$deletes" >> "$GITHUB_OUTPUT"

      - name: Terraform Apply (dev)
        id: apply
        working-directory: ${{ env.WORKDIR_DEV }}
        continue-on-error: true
        shell: bash
        run: |
          set -o pipefail
          terraform apply -no-color -input=false -auto-approve tf.plan 2>&1 | tee apply.txt
          echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"

      - name: Add Job Summary
        if: always()
        shell: bash
        run: |
          echo "## 🚀 Terraform Deploy to dev (main)" >> $GITHUB_STEP_SUMMARY
          echo "- Commit: \`${GITHUB_SHA::7}\`" >> $GITHUB_STEP_SUMMARY
          echo "- Actor: @${GITHUB_ACTOR}" >> $GITHUB_STEP_SUMMARY
          echo "- Directory: \`${{ env.WORKDIR_DEV }}\`" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          if [ -f "${{ env.WORKDIR_DEV }}/plan.txt" ]; then
            echo "### 🧮 Plan Summary" >> $GITHUB_STEP_SUMMARY
            echo "- + Create: ${{ steps.plan.outputs.creates }}" >> $GITHUB_STEP_SUMMARY
            echo "- ~ Update: ${{ steps.plan.outputs.updates }}" >> $GITHUB_STEP_SUMMARY
            echo "- - Delete: ${{ steps.plan.outputs.deletes }}" >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo "<details><summary>Show full plan</summary>" >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            sed -n '1,400p' "${{ env.WORKDIR_DEV }}/plan.txt" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "</details>" >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
          fi

          status="success"
          if [ "${{ steps.apply.outputs.exit_code }}" != "0" ]; then
            status="failure"
          fi

          echo "### ✅ Apply Result: ${status}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY
          if [ -f "${{ env.WORKDIR_DEV }}/apply.txt" ]; then
            echo "<details><summary>Show apply log</summary>" >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            sed -n '1,300p' "${{ env.WORKDIR_DEV }}/apply.txt" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "</details>" >> $GITHUB_STEP_SUMMARY
          fi

      - name: Fail job if apply failed
        if: ${{ steps.apply.outputs.exit_code != '0' }}
        run: |
          echo "Apply failed with exit code: ${{ steps.apply.outputs.exit_code }}" >&2
          exit 1


プロンプト
GitHubActionsのワークフローを作成したい。
infra配下の変更で、mainにマージ、pushされると、dev環境にplan applyを実行するワークフローを作成したい。
 その結果をSummaryに記載したい。

codexのVS CODEのIDEでチャットに打ち込む。

  • set -eo pipefail:パイプの途中で失敗しても即終了出来るようにしている。
    • set -e:どれか1つでもコマンドが失敗したらスクリプトを終了させる。
    • set -o pipefail|パイプラインが途中で失敗したら、きちんとスクリプトを終了させる。
  • echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
    • exit_code:コマンドが終了した時に返すステータス番号を表す変数。
    • PIPESTATUS:直前に実行したパイプラインの各コマンドの終了コードを左から順に入れている。
  • tee:画面に出力して、ファイルにも出力。
  • 2>&1:標準エラーの出力先を標準出力と同じ場所へ向ける。
    • 2:標準エラー出力。
    • 1:標準出力。
    • &:1を標準出力として認識するための記載。
  • if [ -f "${{ env.WORKDIR_DEV }}/plan.txt" ];
    • [...]:テストコマンドと同義。
    • -f:は通常ファイルが存在するか。
      • 通常ファイルが存在すれば真となる。

infra変更でmainのpushワークフローを作成の機能要約
infra/配下の変更でmainにpushやマージが起きた時にワークフローが実行する。
Terraformのセットアップとjqのインストール、cacheのディレクトリ作成とcacheの設定をする。
HUBアカウントへAssumeをしてterraform initをする。
その次にterraform planを実施し、plan結果をJSONファイルに保存し、plan結果を集計し、GITHUB_OUTPUTへ出力させる。
その次にterraform applyをする。
SUMMARYにplan結果を出力。
applyの結果がエラーならエラーの出力をする。

フェーズ5:codex cloudでRCタグ昇格でstgへのワークフロー作成

terraform-deploy-stg-on-tag.yml
name: Terraform Deploy stg on tag

on:
  push:
    tags:
      - 'infra/v*.*.*-rc*'

permissions:
  contents: read
  id-token: write

env:
  TF_PLUGIN_CACHE_DIR: /home/runner/.terraform.d/plugin-cache
  AWS_REGION: ap-northeast-1
  HUB_ACCOUNT_ID: "318574063927"
  WORKDIR_STG: "infra/static-website/environments/stg"

concurrency:
  group: deploy-stg
  cancel-in-progress: false

jobs:
  plan:
    name: Plan (stg)
    runs-on: ubuntu-latest
    outputs:
      creates: ${{ steps.plan.outputs.creates }}
      updates: ${{ steps.plan.outputs.updates }}
      deletes: ${{ steps.plan.outputs.deletes }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform 1.9.2
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.2

      - name: Install jq
        run: |
          sudo apt-get update
          sudo apt-get install -y jq

      - name: Prepare plugin cache
        run: mkdir -p "${{ env.TF_PLUGIN_CACHE_DIR }}"

      - name: Cache Terraform plugins
        uses: actions/cache@v4
        with:
          path: ${{ env.TF_PLUGIN_CACHE_DIR }}
          key: ${{ runner.os }}-tfplugins-${{ hashFiles('**/.terraform.lock.hcl') }}
          restore-keys: ${{ runner.os }}-tfplugins-

      - name: Assume Hub (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ env.HUB_ACCOUNT_ID }}:role/GitHubActionsHubRole
          role-session-name: gha-plan-stg-${{ github.run_id }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Terraform Init (stg backend)
        working-directory: ${{ env.WORKDIR_STG }}
        run: terraform init -input=false -lockfile=readonly -upgrade=false

      - name: Terraform Plan (stg)
        id: plan
        working-directory: ${{ env.WORKDIR_STG }}
        shell: bash
        run: |
          set -eo pipefail
          terraform plan -no-color -input=false -lock-timeout=60s -out=tf.plan | tee plan.txt
          terraform show -json tf.plan > tfplan.json

          creates=$(jq '[.resource_changes[]? | select(.change.actions | index("create"))] | length' tfplan.json)
          replaces=$(jq '[.resource_changes[]? | select(.change.actions | index("replace"))] | length' tfplan.json)
          updates=$(jq '[.resource_changes[]? | select(.change.actions | index("update"))] | length' tfplan.json)
          deletes=$(jq '[.resource_changes[]? | select(.change.actions | index("delete"))] | length' tfplan.json)

          creates=$((creates + replaces))
          deletes=$((deletes + replaces))

          echo "creates=$creates" >> "$GITHUB_OUTPUT"
          echo "updates=$updates" >> "$GITHUB_OUTPUT"
          echo "deletes=$deletes" >> "$GITHUB_OUTPUT"

      - name: Upload plan artifacts
        uses: actions/upload-artifact@v4
        with:
          name: tf-plan
          path: |
            ${{ env.WORKDIR_STG }}/tf.plan
            ${{ env.WORKDIR_STG }}/plan.txt

      - name: Add Job Summary
        if: always()
        shell: bash
        run: |
          ref_name="${GITHUB_REF_NAME:-${GITHUB_REF#refs/*/}}"
          plan_outcome="${{ steps.plan.outcome }}"
          creates="${{ steps.plan.outputs.creates }}"
          updates="${{ steps.plan.outputs.updates }}"
          deletes="${{ steps.plan.outputs.deletes }}"

          creates=${creates:-0}
          updates=${updates:-0}
          deletes=${deletes:-0}
          plan_outcome=${plan_outcome:-failure}

          echo "## 🔍 stg 向け Terraform Plan" >> $GITHUB_STEP_SUMMARY
          echo "- タグ: \`${ref_name}\`" >> $GITHUB_STEP_SUMMARY
          echo "- コミット: \`${GITHUB_SHA::7}\`" >> $GITHUB_STEP_SUMMARY
          echo "- 実行ユーザー: @${GITHUB_ACTOR}" >> $GITHUB_STEP_SUMMARY
          echo "- ディレクトリ: \`${{ env.WORKDIR_STG }}\`" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          if [ "$plan_outcome" = "success" ]; then
            echo "### ✅ Plan 成功" >> $GITHUB_STEP_SUMMARY
          else
            echo "### ❌ Plan 失敗" >> $GITHUB_STEP_SUMMARY
          fi

          echo "" >> $GITHUB_STEP_SUMMARY
          echo "### 🧮 変更サマリ" >> $GITHUB_STEP_SUMMARY
          echo "- + 作成: ${creates}" >> $GITHUB_STEP_SUMMARY
          echo "- ~ 更新: ${updates}" >> $GITHUB_STEP_SUMMARY
          echo "- - 削除: ${deletes}" >> $GITHUB_STEP_SUMMARY

          if [ -f "${{ env.WORKDIR_STG }}/plan.txt" ]; then
            echo "" >> $GITHUB_STEP_SUMMARY
            echo "<details><summary>Plan を表示</summary>" >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            sed -n '1,400p' "${{ env.WORKDIR_STG }}/plan.txt" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "</details>" >> $GITHUB_STEP_SUMMARY
          fi

  apply:
    name: Apply (stg)
    needs:
      - plan
    runs-on: ubuntu-latest
    environment: infra-stg
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform 1.9.2
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.2

      - name: Install jq
        run: |
          sudo apt-get update
          sudo apt-get install -y jq

      - name: Prepare plugin cache
        run: mkdir -p "${{ env.TF_PLUGIN_CACHE_DIR }}"

      - name: Cache Terraform plugins
        uses: actions/cache@v4
        with:
          path: ${{ env.TF_PLUGIN_CACHE_DIR }}
          key: ${{ runner.os }}-tfplugins-${{ hashFiles('**/.terraform.lock.hcl') }}
          restore-keys: ${{ runner.os }}-tfplugins-

      - name: Download plan artifacts
        uses: actions/download-artifact@v4
        with:
          name: tf-plan
          path: ${{ env.WORKDIR_STG }}

      - name: Assume Hub (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ env.HUB_ACCOUNT_ID }}:role/GitHubActionsHubRole
          role-session-name: gha-apply-stg-${{ github.run_id }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Terraform Init (stg backend)
        working-directory: ${{ env.WORKDIR_STG }}
        run: terraform init -input=false -lockfile=readonly -upgrade=false

      - name: Terraform Apply (stg)
        id: apply
        working-directory: ${{ env.WORKDIR_STG }}
        continue-on-error: true
        shell: bash
        run: |
          set -o pipefail
          terraform apply -no-color -input=false -auto-approve tf.plan 2>&1 | tee apply.txt
          echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"

      - name: Add Job Summary
        if: always()
        shell: bash
        run: |
          ref_name="${GITHUB_REF_NAME:-${GITHUB_REF#refs/*/}}"
          creates="${{ needs.plan.outputs.creates }}"
          updates="${{ needs.plan.outputs.updates }}"
          deletes="${{ needs.plan.outputs.deletes }}"
          exit_code="${{ steps.apply.outputs.exit_code }}"

          creates=${creates:-0}
          updates=${updates:-0}
          deletes=${deletes:-0}
          exit_code=${exit_code:-N/A}

          echo "## 🚀 stg への Terraform 適用 (タグ)" >> $GITHUB_STEP_SUMMARY
          echo "- タグ: \`${ref_name}\`" >> $GITHUB_STEP_SUMMARY
          echo "- コミット: \`${GITHUB_SHA::7}\`" >> $GITHUB_STEP_SUMMARY
          echo "- 実行ユーザー: @${GITHUB_ACTOR}" >> $GITHUB_STEP_SUMMARY
          echo "- ディレクトリ: \`${{ env.WORKDIR_STG }}\`" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          echo "### 🧮 Plan 変更サマリ" >> $GITHUB_STEP_SUMMARY
          echo "- + 作成: ${creates}" >> $GITHUB_STEP_SUMMARY
          echo "- ~ 更新: ${updates}" >> $GITHUB_STEP_SUMMARY
          echo "- - 削除: ${deletes}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          if [ "$exit_code" = "0" ]; then
            echo "### ✅ Apply 成功" >> $GITHUB_STEP_SUMMARY
          else
            echo "### ❌ Apply 失敗" >> $GITHUB_STEP_SUMMARY
          fi
          echo "- 終了コード: ${exit_code}" >> $GITHUB_STEP_SUMMARY

          if [ -f "${{ env.WORKDIR_STG }}/apply.txt" ]; then
            echo "" >> $GITHUB_STEP_SUMMARY
            echo "<details><summary>Apply ログを表示</summary>" >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            sed -n '1,300p' "${{ env.WORKDIR_STG }}/apply.txt" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "</details>" >> $GITHUB_STEP_SUMMARY
          fi

      - name: Fail job if apply failed
        if: ${{ steps.apply.outputs.exit_code != '0' }}
        run: |
          echo "Terraform Apply が終了コード ${{ steps.apply.outputs.exit_code }} で失敗しました" >&2
          exit 1

codex cloudでのプロンプト

プロンプト
GitHubActionsを作成して欲しい
v*.*.* → prdにデプロイ(plan→手動承認→apply)
Suumaryにも結果を記載して欲しい

codex cloudでチャットに打ち込み

承認を挟むのでGitHubの設定

リポジトリの設定から設定し、このnameをGitHub Actions内で記述することとなる。
リポジトリ → Settings → Environments → New environment

コードの内容

  • outcome:そのステップのGitHub Actionsの実行結果を表す文字列。
    • success:runが正常終了し、アクションが成功。
    • failure:コマンド失敗し、アクション内部でエラーとなっている。
    • cancelled:手動でワークフローをキャンセル。
    • skipped:何かの条件で、実行されてなかった。
  • plan_outcome=${plan_outcome:-failure}:plan_outcomeに値が無ければfailureを値として代入する。
    • シェルのパラメータ展開の演算子。
    • ${...=...}:右の値を左の値に代入する。
      • 値が未設定なら、代入。
      • 値が空文字なら、代入しない。
      • 値があったら、代入しない
    • ${...=:...}:右の値を左の値に、空文字の時も代入する
      • 値が未設定なら、代入。
      • 値が空文字でも代入する。
      • 値があったら代入しない。
      • :は空文字の時どうするか。
    • ${...-...}
      • 値が未設定は、代入せずに右の値を使う。
      • 値が空文字は、空文字を使う。
    • ${...-:...}
      • 値が未設定は、代入せずに右の値を使う。
      • 値が空文字でも右の値を使う。
  • actions/upload-artifact@v4:Run間での受け渡しのための保管領域に保管する。
    • 環境変数では、ステップ間でしか共有出来ない。
      • サイズ制限も存在する。
  • actions/download-artifact@v4:terraform applyの際に、planで計画した計画でapplyするためで、こうすることで一貫性が保てる。

1回目テスト実行

タグの付与。

tano1:0603game tano$ git tag -a v1.0.0-rc.1 -m "first release candidate"
tano1:0603game tano$ git push origin v1.0.0-rc.1
Enumerating objects: 1, done.
Counting objects: 100% (1/1), done.
Writing objects: 100% (1/1), 173 bytes | 173.00 KiB/s, done.
Total 1 (delta 0), reused 0 (delta 0), pack-reused 0 (from 0)
To https://github.com/YusukeTano/0603game-tano-public.git
 * [new tag]         v1.0.0-rc.1 -> v1.0.0-rc.1
tano1:0603game tano$ 

成功したので、次へ。

フェーズ6:claude codeでタグ昇格でprdへのワークフロー

terraform-deploy-prd-on-tag.yml
name: Terraform Deploy prd on tag

on:
  push:
    tags:
      - 'infra/v[0-9]+.[0-9]+.[0-9]+' # より厳密なパターン
      - '!v*-*'                  # プレリリース版を除外

permissions:
  contents: read
  id-token: write

env:
  TF_PLUGIN_CACHE_DIR: /home/runner/.terraform.d/plugin-cache
  AWS_REGION: ap-northeast-1
  HUB_ACCOUNT_ID: "318574063927"
  WORKDIR_PRD: "infra/static-website/environments/prd"

concurrency:
  group: deploy-prd
  cancel-in-progress: false

jobs:
  plan:
    name: Plan (prd)
    runs-on: ubuntu-latest
    outputs:
      creates: ${{ steps.plan.outputs.creates }}
      updates: ${{ steps.plan.outputs.updates }}
      deletes: ${{ steps.plan.outputs.deletes }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform 1.9.2
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.2

      - name: Install jq
        run: |
          sudo apt-get update
          sudo apt-get install -y jq

      - name: Prepare plugin cache
        run: mkdir -p "${{ env.TF_PLUGIN_CACHE_DIR }}"

      - name: Cache Terraform plugins
        uses: actions/cache@v4
        with:
          path: ${{ env.TF_PLUGIN_CACHE_DIR }}
          key: ${{ runner.os }}-tfplugins-${{ hashFiles('**/.terraform.lock.hcl') }}
          restore-keys: ${{ runner.os }}-tfplugins-

      - name: Assume Hub (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ env.HUB_ACCOUNT_ID }}:role/GitHubActionsHubRole
          role-session-name: gha-plan-prd-${{ github.run_id }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Terraform Init (prd backend)
        working-directory: ${{ env.WORKDIR_PRD }}
        run: terraform init -input=false -lockfile=readonly -upgrade=false

      - name: Terraform Plan (prd)
        id: plan
        working-directory: ${{ env.WORKDIR_PRD }}
        shell: bash
        run: |
          set -eo pipefail
          terraform plan -no-color -input=false -lock-timeout=60s -out=tf.plan | tee plan.txt
          terraform show -json tf.plan > tfplan.json

          creates=$(jq '[.resource_changes[]? | select(.change.actions | index("create"))] | length' tfplan.json)
          replaces=$(jq '[.resource_changes[]? | select(.change.actions | index("replace"))] | length' tfplan.json)
          updates=$(jq '[.resource_changes[]? | select(.change.actions | index("update"))] | length' tfplan.json)
          deletes=$(jq '[.resource_changes[]? | select(.change.actions | index("delete"))] | length' tfplan.json)

          creates=$((creates + replaces))
          deletes=$((deletes + replaces))

          echo "creates=$creates" >> "$GITHUB_OUTPUT"
          echo "updates=$updates" >> "$GITHUB_OUTPUT"
          echo "deletes=$deletes" >> "$GITHUB_OUTPUT"

      - name: Upload plan artifacts
        uses: actions/upload-artifact@v4
        with:
          name: tf-plan-prd
          path: |
            ${{ env.WORKDIR_PRD }}/tf.plan
            ${{ env.WORKDIR_PRD }}/plan.txt

      - name: Add Job Summary
        if: always()
        shell: bash
        run: |
          ref_name="${GITHUB_REF_NAME:-${GITHUB_REF#refs/*/}}"
          plan_outcome="${{ steps.plan.outcome }}"
          creates="${{ steps.plan.outputs.creates }}"
          updates="${{ steps.plan.outputs.updates }}"
          deletes="${{ steps.plan.outputs.deletes }}"

          creates=${creates:-0}
          updates=${updates:-0}
          deletes=${deletes:-0}
          plan_outcome=${plan_outcome:-failure}

          echo "## 🔍 prd 向け Terraform Plan" >> $GITHUB_STEP_SUMMARY
          echo "- タグ: \`${ref_name}\`" >> $GITHUB_STEP_SUMMARY
          echo "- コミット: \`${GITHUB_SHA::7}\`" >> $GITHUB_STEP_SUMMARY
          echo "- 実行ユーザー: @${GITHUB_ACTOR}" >> $GITHUB_STEP_SUMMARY
          echo "- ディレクトリ: \`${{ env.WORKDIR_PRD }}\`" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          if [ "$plan_outcome" = "success" ]; then
            echo "### ✅ Plan 成功" >> $GITHUB_STEP_SUMMARY
          else
            echo "### ❌ Plan 失敗" >> $GITHUB_STEP_SUMMARY
          fi

          echo "" >> $GITHUB_STEP_SUMMARY
          echo "### 🧮 変更サマリ" >> $GITHUB_STEP_SUMMARY
          echo "- + 作成: ${creates}" >> $GITHUB_STEP_SUMMARY
          echo "- ~ 更新: ${updates}" >> $GITHUB_STEP_SUMMARY
          echo "- - 削除: ${deletes}" >> $GITHUB_STEP_SUMMARY

          if [ -f "${{ env.WORKDIR_PRD }}/plan.txt" ]; then
            echo "" >> $GITHUB_STEP_SUMMARY
            echo "<details><summary>Plan を表示</summary>" >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            sed -n '1,400p' "${{ env.WORKDIR_PRD }}/plan.txt" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "</details>" >> $GITHUB_STEP_SUMMARY
          fi

  apply:
    name: Apply (prd)
    needs:
      - plan
    runs-on: ubuntu-latest
    environment: infra-prd
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Terraform 1.9.2
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: 1.9.2

      - name: Install jq
        run: |
          sudo apt-get update
          sudo apt-get install -y jq

      - name: Prepare plugin cache
        run: mkdir -p "${{ env.TF_PLUGIN_CACHE_DIR }}"

      - name: Cache Terraform plugins
        uses: actions/cache@v4
        with:
          path: ${{ env.TF_PLUGIN_CACHE_DIR }}
          key: ${{ runner.os }}-tfplugins-${{ hashFiles('**/.terraform.lock.hcl') }}
          restore-keys: ${{ runner.os }}-tfplugins-

      - name: Download plan artifacts
        uses: actions/download-artifact@v4
        with:
          name: tf-plan-prd
          path: ${{ env.WORKDIR_PRD }}

      - name: Assume Hub (OIDC)
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ env.HUB_ACCOUNT_ID }}:role/GitHubActionsHubRole
          role-session-name: gha-apply-prd-${{ github.run_id }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Terraform Init (prd backend)
        working-directory: ${{ env.WORKDIR_PRD }}
        run: terraform init -input=false -lockfile=readonly -upgrade=false

      - name: Terraform Apply (prd)
        id: apply
        working-directory: ${{ env.WORKDIR_PRD }}
        continue-on-error: true
        shell: bash
        run: |
          set -o pipefail
          terraform apply -no-color -input=false -auto-approve tf.plan 2>&1 | tee apply.txt
          echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"

      - name: Add Job Summary
        if: always()
        shell: bash
        run: |
          ref_name="${GITHUB_REF_NAME:-${GITHUB_REF#refs/*/}}"
          creates="${{ needs.plan.outputs.creates }}"
          updates="${{ needs.plan.outputs.updates }}"
          deletes="${{ needs.plan.outputs.deletes }}"
          exit_code="${{ steps.apply.outputs.exit_code }}"

          creates=${creates:-0}
          updates=${updates:-0}
          deletes=${deletes:-0}
          exit_code=${exit_code:-N/A}

          echo "## 🚀 prd への Terraform 適用 (タグ)" >> $GITHUB_STEP_SUMMARY
          echo "- タグ: \`${ref_name}\`" >> $GITHUB_STEP_SUMMARY
          echo "- コミット: \`${GITHUB_SHA::7}\`" >> $GITHUB_STEP_SUMMARY
          echo "- 実行ユーザー: @${GITHUB_ACTOR}" >> $GITHUB_STEP_SUMMARY
          echo "- ディレクトリ: \`${{ env.WORKDIR_PRD }}\`" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          echo "### 🧮 Plan 変更サマリ" >> $GITHUB_STEP_SUMMARY
          echo "- + 作成: ${creates}" >> $GITHUB_STEP_SUMMARY
          echo "- ~ 更新: ${updates}" >> $GITHUB_STEP_SUMMARY
          echo "- - 削除: ${deletes}" >> $GITHUB_STEP_SUMMARY
          echo "" >> $GITHUB_STEP_SUMMARY

          if [ "$exit_code" = "0" ]; then
            echo "### ✅ Apply 成功" >> $GITHUB_STEP_SUMMARY
          else
            echo "### ❌ Apply 失敗" >> $GITHUB_STEP_SUMMARY
          fi
          echo "- 終了コード: ${exit_code}" >> $GITHUB_STEP_SUMMARY

          if [ -f "${{ env.WORKDIR_PRD }}/apply.txt" ]; then
            echo "" >> $GITHUB_STEP_SUMMARY
            echo "<details><summary>Apply ログを表示</summary>" >> $GITHUB_STEP_SUMMARY
            echo "" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            sed -n '1,300p' "${{ env.WORKDIR_PRD }}/apply.txt" >> $GITHUB_STEP_SUMMARY
            echo '```' >> $GITHUB_STEP_SUMMARY
            echo "</details>" >> $GITHUB_STEP_SUMMARY
          fi

      - name: Fail job if apply failed
        if: ${{ steps.apply.outputs.exit_code != '0' }}
        run: |
          echo "Terraform Apply が終了コード ${{ steps.apply.outputs.exit_code }} で失敗しました" >&2
          exit 1
planモードでプロンプト
GitHubActionsを作成して欲しい
v*.*.* → prdにデプロイ(plan→手動承認→apply)
Suumaryにも結果を記載して欲しい

テスト成功

問題無く成功。

Discussion