はじめに
いざTerraformでインフラを管理しようとなった際に、どうすれば安全なパイプラインを構築できるものか、迷うことかと思います。
この記事ではTerraformのCI/CDをGitHub Actionsで構築する方法を紹介します。
前提
この記事は、以下の記事の続きとなっております。
もしも前提となるコンテキストが必要となる場合、こちらの記事もご覧ください。

また、本パイプラインは以下を前提としています。
- AWSのリソースを管理する
- 環境はstgとprdの2環境を想定する
- planは自動で、applyはGitHub Actionsから手動でキックする
概要
結論として、以下のリポジトリにコードを置いています。
https://github.com/gonkunkun/terraform-template/tree/main/.github/workflows
Planについて
Planの実行は以下の仕様となっています。
- PRの作成/PRの更新時にPlanが実行される
- stgとprdの2環境に対するPlanが同時に実行される
- コードの変更がない場合にはPlanをスキップする(クレジットの節約のため)
- Planの実行結果はPRのコメントに貼り付けられる
Applyについて
CDにおいては、GitHubの無料枠の制約を受けています。
具体的には、有料枠のみで提供されている以下の機能が使えない制約がありました。
- ブランチの保護ルールを設定する
- GitHub ActionsのApply前のStepに承認プロセス(Approve)を設ける
そこで、いくつか妥協を重ねつつ、Applyの実行を以下の仕様としました。
- mainブランチの状態に対してApplyを実行する(これは仕様というか運用ルールです)
- ここは運用対処で凌いでいる
 
- mainブランチとApply対象の環境を指定して、apply用のCDをキックする
- CDキック後は、Plan -> 数秒間のwait(Planの確認用) -> Applyの順でパイプラインが動作する
- 承認プロセスを挟めないので、手動でキックしている
 (複数人が同時にmainにマージすると、PRのPlan結果と異なるApplyが適用されるリスクがあります)
- 擬似的に承認プロセスを挟めるようなモジュールもあったものの、承認待ちの間もクレジットを消費してしまうので、節約の観点で諦めました
 
- 承認プロセスを挟めないので、手動でキックしている
詳細
重複したコードが多いので、もっと綺麗に整理することもできると思います。
事前準備
- Terraformユーザ用のクレデンシャルはGitHubリポジトリ側に設定する
都度都度クレデンシャルを持ってくる方がいいのかもしれないですが、今回は楽なやり方を選んでます。
Plan
name: Terraform Plan
on:
  pull_request:
    types:
      - opened
      - synchronize
    branches:
      - '*'
permissions:
  contents: read
  pull-requests: write
jobs:
  check_changed_dirs:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix_dirs }}
    steps:
      - run: echo "fetch_depth=$(( commits + 1 ))" >> $GITHUB_ENV
        env:
          commits: ${{ github.event.pull_request.commits }}
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          fetch-depth: ${{ env.fetch_depth }}
      - name: Set Matrix
        id: set-matrix
        run: |
          git fetch origin ${{ github.base_ref }}
          changed_files=$(git diff --name-only origin/${{ github.base_ref }})
          terraform_dirs=$(echo "$changed_files" | grep -E '^[^/]+/.*\.tf$' | awk -F/ '{print $1}' | sort -u || echo "")
          if [ -z "$terraform_dirs" ]; then
            echo "No Terraform directories changed."
          else
            echo "Changed directories: $terraform_dirs"
          fi
          echo "matrix_dirs=$(echo $terraform_dirs | jq -R -s -c 'split("\n") | map(select(. != ""))')" >> "$GITHUB_OUTPUT"
        shell: bash
  plan:
    name: Plan
    runs-on: ubuntu-latest
    needs: check_changed_dirs
    if: ${{ needs.check_changed_dirs.outputs.matrix != '[]' && needs.check_changed_dirs.outputs.matrix != '' }}
    strategy:
      fail-fast: false
      matrix:
        dir: ${{ fromJson(needs.check_changed_dirs.outputs.matrix) }}
        env: [stg, prd]
    steps:
      # Setup
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Setup AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: "ap-northeast-1"
      - name: Get Terraform version
        id: terraform-version
        uses: bigwheel/get-terraform-version-action@v1.2.0
        with:
          path: ${{ matrix.dir }}
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ steps.terraform-version.outputs.terraform-version }}
      # Cache Terraform plugins
      - name: Cache Terraform plugin directory
        uses: actions/cache@v3
        with:
          path: .terraform
          key: terraform-plugin-${{ runner.os }}-${{ hashFiles('**/*.tf*') }}
      - name: Run Terraform Format Check
        run: terraform fmt -recursive -check
      # Run Terraform Init
      - name: Terraform Init
        run: |
          terraform init -backend-config="envs/${{ matrix.env }}/backend.hcl"
        working-directory: ${{ matrix.dir }}
      - name: Run Terraform Validate Check
        run: terraform validate
        working-directory: ${{ matrix.dir }}
      # Run Terraform Plan
      - name: Terraform Plan
        run: |
          terraform plan -var="env=${{ matrix.env }}" -out=tfplan-${{ matrix.dir }}-${{ matrix.env }}.out
        working-directory: ${{ matrix.dir }}
      # Post results to PR
      - name: Post Plan Results to PR
        uses: borchero/terraform-plan-comment@v2
        with:
          token: ${{ github.token }}
          header: "terraform-plan Dir: ${{ matrix.dir }} (${{ matrix.env }})"
          planfile: tfplan-${{ matrix.dir }}-${{ matrix.env }}.out
          working-directory: ${{ matrix.dir }}ref: https://github.com/gonkunkun/terraform-template/blob/main/.github/workflows/terraform-plan.yml
アウトプットのイメージとしては以下の感じです。
 
planの結果のポストは、以下の記事で解説しています。

Apply
name: Terraform Apply (manual)
on:
  push:
    branches:
      - main
  workflow_dispatch:
    inputs:
      environment:
        description: 'Select the environment (e.g., stg, prd)'
        required: true
        type: choice
        options:
          - stg
          - prd
        default: stg
permissions:
  contents: read
  pull-requests: write
jobs:
  pre-plan:
    name: Pre-Plan
    runs-on: ubuntu-latest
    # needs: check_terraform_dirs
    if: github.event_name != 'workflow_dispatch'
    strategy:
      fail-fast: false
      matrix:
        dir: ["workloads"]
        env: [stg, prd]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          ref: ${{ github.base_ref }}
      - name: Setup AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: "ap-northeast-1"
      - name: Get Terraform version
        id: terraform-version
        uses: bigwheel/get-terraform-version-action@v1.2.0
        with:
          path: ${{ matrix.dir }}
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ steps.terraform-version.outputs.terraform-version }}
      # Cache Terraform plugins
      - name: Cache Terraform plugin directory
        uses: actions/cache@v3
        with:
          path: .terraform
          key: terraform-plugin-${{ runner.os }}-${{ hashFiles('**/*.tf*') }}
      # Run Terraform Init
      - name: Terraform Init
        run: |
          terraform init -backend-config="envs/${{ matrix.env }}/backend.hcl"
        working-directory: ${{ matrix.dir }}
      # Run Terraform Plan
      - name: Terraform Plan
        run: |
          terraform plan -var="env=${{ matrix.env }}"
        working-directory: ${{ matrix.dir }}
  apply:
    name: Apply to specific env
    runs-on: ubuntu-latest
    # needs: check_terraform_dirs
    if: github.event_name == 'workflow_dispatch'
    strategy:
      matrix:
        dir: ["workloads"]
        env: ["${{ github.event.inputs.environment }}"]
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
        with:
          ref: ${{ github.ref_name }}
      - name: Ensure Main is Up-to-Date
        run: |
          git fetch origin ${{ github.ref_name }}
          git diff --quiet origin/${{ github.ref_name }} || (echo "Main has diverged. Please rebase your branch." && exit 1)
      - name: Setup AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: "ap-northeast-1"
      - name: Get Terraform version
        id: terraform-version
        uses: bigwheel/get-terraform-version-action@v1.2.0
        with:
          path: ${{ matrix.dir }}
      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: ${{ steps.terraform-version.outputs.terraform-version }}
      # Run Terraform Init
      - name: Terraform Init - ${{ matrix.env }}
        run: |
          terraform init -backend-config="envs/${{ matrix.env }}/backend.hcl"
        working-directory: ${{ matrix.dir }}
      # Run Terraform Plan
      - name: Terraform Plan
        if: github.event_name == 'workflow_dispatch'
        run: |
          terraform plan -var="env=${{ matrix.env }}"
          sleep 6
        working-directory: ${{ matrix.dir }}
      # Run Terraform Apply
      - name: Terraform Apply - ${{ matrix.env }}
        if: github.event_name == 'workflow_dispatch'
        run: |
          terraform apply -var="env=${{ matrix.env }}" -auto-approve
        working-directory: ${{ matrix.dir }}
ref: https://github.com/gonkunkun/terraform-template/blob/main/.github/workflows/terraform-apply.yml
GitHub Actionsから手動でワークフローを実行します。

おわりに
ここまで読んでいただきありがとうございました。
良いTerraformライフを!
 
																	 
																	 
																	 
																	 
																	 
																	 
						
						
						
												 
										
					