目次
はじめに:差分に基づく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 plan
を terraform/
内の変更があったディレクトリに対してだけ実行します。具体的には以下の処理を行います:
setup
ジョブで、変更があったサービスのディレクトリを抽出します。各サービスディレクトリについて、現在のブランチとベースブランチとの差分を取り、差分が存在すればそのディレクトリ名をchanged_dirs
配列に追加します。plan
ジョブで、変更があったサービスのディレクトリごとに、各環境(dev
、stg
、prd
)について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に支援してもらって書いてみました!文体のチューニングが難しいですね。)