かなで技術日誌

プログラミングやエンジニアリング周りについて

主なアウトプットはScrapboxObsidianにまとめてます。

GitHub ActionsとTerraformで差分に基づく条件付きCI実行の実装

目次

  1. はじめに:差分に基づくCI実行の必要性
  2. Terraformリポジトリのディレクトリ構造
  3. GitHub Actionsの設定手順
  4. 差分の判定とディレクトリの取得
  5. まとめ

はじめに:差分に基づくCI実行の必要性

開発者の日々の作業において、無駄な時間とリソースを浪費することは避けたいものです。特に、GitHub Actionsを使ったCIが、全ディレクトリに対してトリガーされるとなると、その浪費は顕著になります。そのため、今回は変更があったディレクトリに対してのみCIを実行する方法を、具体的な手順とともに紹介します。この手法により、CIの実行時間が大幅に短縮され、リソースの使用も最小限に抑えられます。

以下では、Pull Requestをトリガーに、その差分に基づいてCIを実行する具体的な手法について詳しく解説します。具体的には、Terraformのリポジトリを例に、GitHub Actionsでの任意のディレクトリ配下の全てのディレクトリを取得し、特定の条件下でコマンドを実行する方法を説明します。

Terraformリポジトリのディレクトリ構造

本記事では、以下のようなディレクトリ構造を持つTerraformリポジトリを例とします。これは、各サービスと環境ごとにTerraform設定を分けて管理している例です。

root/
 └ terraform/
    ├── service1/
    │   ├── envs/
    │   │   ├── dev/
    │   │   │   └── main.tf
    │   │   ├── prd/
    │   │   │   └── main.tf
    │   │   └── stg/
    │   │       └── main.tf
    ├── service2/
    ├── service3/
    └── service4/
  • terraform ディレクトリの直下には、各サービスのディレクトリ(service1, service2 など)があります。
  • 各サービスのディレクトリ内には、環境ごとのディレクトリ(dev, prd, stg)があります。
  • 環境ディレクトリ内には、その環境で適用されるTerraformの設定を含む main.tf ファイルがあります。

次のセクションでは、このリポジトリ構造を基にしたCIの設定方法を解説します。

GitHub ActionsでのCI設定

本節では、上述のディレクトリ構造を持つTerraformリポジトリに対し、GitHub Actionsを使って差分に基づくterraform planの実行設定を行う方法をご紹介します。まず、設定に必要なGitHub Actionsのワークフローファイルの内容を以下に示します。

name: 'Terraform Plan'
on:
  pull_request:
    paths:
      - 'terraform/**'
env:
  TARGET_DIR: 'terraform'
jobs:
  setup:
    runs-on: ubuntu-latest
    outputs:
      dirs: ${{ steps.dirs.outputs.dirs }}
    steps:
      - name: 'Checkout'
        uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - name: 'Output dirs'
        id: 'dirs'
        run: |
          changed_dirs=()
          while IFS= read -r dir; do
            dir=${dir%*/}
            git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} --depth=1
            DIFF=$(git diff --name-only HEAD ${{ github.base_ref }} --relative -- ./${{ env.TARGET_DIR }}/$dir/ | wc -l)
            echo "$dir diff: $DIFF"
            if [ "$DIFF" != "0" ]; then
              changed_dirs+=("$dir")
            fi
          done < <(find ./${{ env.TARGET_DIR }} -maxdepth 1 -type d | tail -n +2 | sed 's|^./${{ env.TARGET_DIR }}/||')
          printf -v joined_dirs "\"%s\", " "${changed_dirs[@]}"
          arr=$(echo "[${joined_dirs%, }]" | jq -c)
          if [ "$arr" = '[""]' ]; then
            echo "No changed directories."
          fi
          echo "dirs=$arr" >> $GITHUB_OUTPUT
  plan:
    name: 'Terraform Plan'
    if: needs.setup.outputs.dirs != '[""]'
    needs: setup
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        envs: [ dev, stg, prd ]
        resources: ${{ fromJson(needs.setup.outputs.dirs) }}
    env:
      AWS_DEFAULT_REGION: ap-northeast-1
      AWS_DEFAULT_OUTPUT: json
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      TERRAFORM_WORKING_DIR: ${{ env.TARGET_DIR }}/${{ matrix.resources }}/environments/${{ matrix.envs }}
      TERRAFORM_VERSION: '1.4.6'
    permissions:
      id-token: write
      contents: read
      pull-requests: write
    steps:
      - name: 'Checkout'
        uses: actions/checkout@v3
      - name: 'SetUp 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.3.0
      - name: 'Setup Terraform'
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: ${{ env.TERRAFORM_VERSION }}
      - name: 'Terraform Format'
        run: |
          terraform fmt -check -recursive
        working-directory: ${{ env.TERRAFORM_WORKING_DIR }}
      - name: 'Terraform Init'
        run: |
          terraform init -lock-timeout=300s
        working-directory: ${{ env.TERRAFORM_WORKING_DIR }}
      - name: 'Terraform Validate'
        run: |
          terraform validate -no-color
        working-directory: ${{ env.TERRAFORM_WORKING_DIR }}
      - name: 'Terraform Plan'
        run: |
          tfcmt -var "target:${{ matrix.resources }}-${{ matrix.envs }}" plan -patch -- terraform plan -no-color
        working-directory: ${{ env.TERRAFORM_WORKING_DIR }}

このワークフローファイルは、プルリクエストに対する terraform planterraform/ 内の変更があったディレクトリに対してだけ実行します。具体的には以下の処理を行います:

  1. setup ジョブで、変更があったサービスのディレクトリを抽出します。各サービスディレクトリについて、現在のブランチとベースブランチとの差分を取り、差分が存在すればそのディレクトリ名を changed_dirs 配列に追加します。
  2. plan ジョブで、変更があったサービスのディレクトリごとに、各環境(devstgprd)について terraform plan を実行します。各実行結果はプルリクエストにコメントとして出力されます。

なお、plan ジョブでは tfcmt を使用して terraform plan の結果をプルリクエストにコメント出力しています。これは便利なツールなので、ぜひ利用してみてください。

差分の判定とディレクトリの取得

この章では、PRで変更されたディレクトリを正確に判定し、それらのディレクトリのみを対象にTerraformのplanを実行する方法について説明します。GitHub Actionsのworkflowファイルに含まれるsetupジョブがこの役割を果たします。

まず、actions/checkout@v3を使用して、Gitリポジトリをチェックアウトします。ここではPRのヘッドSHAを使用します。

- name: 'Checkout'
  uses: actions/checkout@v3
  with:
    ref: ${{ github.event.pull_request.head.sha }}

その後、以下のShellスクリプトを使用して、変更が発生したディレクトリを判定します。

changed_dirs=()
while IFS= read -r dir; do
  dir=${dir%*/}
  git fetch origin ${{ github.base_ref }}:${{ github.base_ref }} --depth=1
  DIFF=$(git diff --name-only HEAD ${{ github.base_ref }} --relative -- ./${{ env.TARGET_DIR }}/$dir/ | wc -l)
  echo "$dir diff: $DIFF"
  if [ "$DIFF" != "0" ]; then
    changed_dirs+=("$dir")
  fi
done < <(find ./${{ env.TARGET_DIR }} -maxdepth 1 -type d | tail -n +2 | sed 's|^./${{ env.TARGET_DIR }}/||')
printf -v joined_dirs "\"%s\", " "${changed_dirs[@]}"
arr=$(echo "[${joined_dirs%, }]" | jq -c)
if [ "$arr" = '[""]' ]; then
  echo "No changed directories."
fi
echo "dirs=$arr" >> $GITHUB_OUTPUT

このスクリプトは、terraformディレクトリ直下のすべてのディレクトリ(各リソースのディレクトリ)に対して、ヘッドSHAとベースブランチとの間で変更があったかどうかを確認します。変更があったディレクトリだけがchanged_dirs配列に追加されます。最終的に、この配列はJSON形式でGitHub Actionsの出力に設定され、次のplanジョブで使用されます。

この方法により、変更が発生したディレクトリに対してだけterraform planが実行されるようになります。これにより、差分に基づくCI実行が可能となります。

まとめ

本記事で紹介した差分に基づくCI実行の方法は、大規模なTerraformリポジトリを管理する際の助けになるでしょう。GitHub Actionsの力を借りて、Pull Requestで変更が発生したディレクトリだけを対象にterraform planを実行することで、CIパイプラインの効率を大幅に向上することができます。

また、このアプローチはTerraformだけでなく、他のInfrastructure as Code (IaC)ツールやワークフローにも適用可能です。もっと良い方法があればぜひ教えてください!

(この記事はChatGPTに支援してもらって書いてみました!文体のチューニングが難しいですね。)