🍓

GitHub CI/CD実践ガイドの著者の記事を写経してグッドプラクティス11を学び、+1してみる

2025/03/04に公開

GitHub CI/CD実践ガイドの著者が提唱するグッドプラクティスを実践してみました。

グッドプラクティス11

1.タイムアウトを常に指定する
2.デフォルトシェルでBashのパイプエラーを拾う
3.「actionlint」ですばやく構文エラーをチェックする
4.Concurrencyで古いワークフローを自動キャンセルする
5.不要なイベントで起動しないようにフィルタリングする
6.最安値のUbuntuランナーを優先する
7.GITHUB_TOKENのパーミッションはジョブレベルで定義する
8.アクションはコミットハッシュで固定する
9.Bashトレーシングオプションでログを詳細に出力する
10.workflow_dispatchイベントで楽に動作確認する
11.ワークフローの背景情報をコメントへ書き残す

+1

1.OIDCとAWSSTSを使用した認証を使用する

PR作成時にterraform [validate,plan]を実行するworkfllowを作成してみます

1.タイムアウトを常に指定する

すばやく失敗を検出し、効果的にフィードバックを得ることが大切

  • GitHub Actionsのデフォルトタイムアウトは360分なので、意図しない通信障害に気づかないと半日実行し続けてしまいますね。VPC含むNetwokrをまるっと作成するPlanも発生するのでタイムアウトは10分にしました。

  • ジョブに記述

timeout-minutes: 10

2.デフォルトシェルでBashのパイプエラーを拾う

パイプ処理中のエラーを無視します。エラー時に意図せず処理を継続するため、結果として不具合の温床になります。GitHub ActionsではBashの利用を明示的に宣言すると、なぜかpipefailオプションが有効化されます

  • パイプ(|)を使ったコマンドの実行中に、途中のコマンドでエラーが発生しても、そのまま処理が続行されてしまうのですが、Bashの利用を明示的に宣言しておくことで、pipefailオプションが有効化になってパイプ処理中のエラーを確実に検出し、意図しない処理の継続を防ぎます。
    例:bashスクリプトでのpipefailオプションの有効化
set -o pipefail
  • ジョブに記述
defaults:
  run:
    shell: bash

3.「actionlint」ですばやく構文エラーをチェックする

.github/workflowsディレクトリ配下のYAMLファイルをまとめてチェックし、構文エラーや非推奨構文などを検出します

  • .github/workflowsディレクトリ配下にactionlintを作成します
Actionlint Check
name: 'Actionlint Check'

on:
  push:
    branches:
      - '**'
  pull_request:
    branches:
      - 'develop'

jobs:
  actionlint:
    name: 'Actionlint'
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Checkout Repository
        uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

      # docker runを使用してactionlintを実行する
      - name: RunActionlint
        run: |
          set -x
          docker run --rm -v "${{ github.workspace }}:${{ github.workspace }}" -w "${{ github.workspace }}" rhysd/actionlint:latest

4.Concurrencyで古いワークフローを自動キャンセルする

プルリクエストで起動するワークフローは、最新コード以外での実行がたいてい不要です。自動テストや静的解析はその典型で、古いコードで実行されるワークフローはムダです。

  • プルリクエストが更新されるたびにワークフローが実行されると、古いコードに対してもワークフローが動いてしまいます。しかし、自動テストや静的解析の目的は「最新のコードが問題ないかを確認すること」なので、過去のコミットに対するワークフローの実行は無駄になります。
  • ワークフロー全体に対して汎用的な設定を記述
# PRで最新のコードだけを実行対象
group: pr-${{ github.event.pull_request.number }}
# pushも含む汎用的
group: ${{ github.workflow }}-${{ github.ref }} 
# 古いワークフローを自動キャンセルし、リソースを節約
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

5.不要なイベントで起動しないようにフィルタリングする

pull_requestイベントやpushイベントなら、ファイルパスやブランチで起動条件を調整可能です

  • イベントに記述
on:
  pull_request:
    paths: '**.tf'

6.最安値のUbuntuランナーを優先する

GitHub Actionsは使用時間に応じて課金されます。この使用時間はワークフローの実行時間へ、ランナーごとに設定されている乗率をかけて計算します。
「使用時間」=「実行時間」×「ランナーごとに異なる乗率」
この乗率はUbuntuランナーがもっとも小さいです。

  • ジョブに記述
runs-on: ubuntu-latest

7.GITHUB_TOKENのパーミッションはジョブレベルで定義する

GITHUB_TOKENの権限はパーミッションで制御し、最小権限での運用が鉄則です。

  • ワークフロー全体に対して記述
permissions:
  contents: read

8.アクションはコミットハッシュで固定する

Gitタグは可変です。攻撃者がコードを改ざんしてGitタグも上書きすれば、アクション経由でワークフローを侵害できます

  • stepに記述
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2

9.Bashトレーシングオプションでログを詳細に出力する

デバッグの第一歩は、なにが起きているか把握することです。そこで役立つのが、Bashのトレーシングオプションです。

  • stepに記述
set -x

10.workflow_dispatchイベントで楽に動作確認する

workflow_dispatchイベントは起動タイミングが自由なだけでなく、任意のブランチで起動可能です。つまりデフォルトブランチへマージする前に、ワークフローの動作確認ができます

  • 手動実行が必要なため、CI/CDパイプライン全体の自動化フローが乱れる可能性もあると思ったので採用を見送りました。

11.ワークフローの背景情報をコメントへ書き残す

あなたが実装したワークフローは、他人にとって自明ではありません。

  • 拝承いたします。

+1.OIDCとAWS STSを使用した認証を使用する

  • シークレット変数でアクセスキーとシークレットキーを運用するのではなく、OIDCを使用することでAWS STSと連携する。
permissions:
  id-token: write  # OIDCトークン発行権限

# 2. AWS認証情報の設定(OIDC経由でIAMロールを引き受ける)
- name: Configure AWS Credentials via OIDC
  uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0
  with:
    role-to-assume: arn:aws:iam::xxxxxxxxx:role/github-actions-role
    aws-region: ap-northeast-1

グッドプラクティス11+1を適用したワークフロー

Terraform PR
name: 'Terraform PR Validate'

on:
  pull_request:
    paths: '**.tf'
    branches:
      - 'develop'

permissions:
  contents: read
  id-token: write  # OIDCトークン発行権限

# 古いワークフローを自動キャンセルし、リソースを節約
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  terraform:
    name: 'TerraformValidateonChangedDirectories'
    runs-on: ubuntu-latest
    timeout-minutes: 5
    env:
      AWS_DEFAULT_REGION: 'ap-northeast-1'
    defaults:
      run:
        shell: bash

    steps:
      # 1. PRの最新コミットをチェックアウト
      - name: CheckoutRepository
        uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
        with:
          ref: ${{ github.head_ref }}

      # 2. AWS認証情報の設定(OIDC経由でIAMロールを引き受ける)
      - name: Configure AWS Credentials via OIDC
        uses: aws-actions/configure-aws-credentials@ececac1a45f3b08a01d2dd070d28d111c5fe6722 # v4.1.0
        with:
          role-to-assume: arn:aws:iam::xxxxxxxxx:role/github-actions-role
          aws-region: ap-northeast-1

      # 3. Terraform CLIのセットアップ
      - name: SetupTerraform
        uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
        with:
          terraform_version: '1.11.0'

      # 4. PRのターゲットブランチをフェッチ
      - name: FetchBaseBranch
        run: |
          set -x
          git fetch origin ${{ github.base_ref }}

      # 5. 変更された*.tfファイルが含まれるディレクトリを抽出
      - name: ListChangedTerraformDirectories
        id: list_dirs
        run: |
          set -x
          changed_dirs=$(git diff --name-only origin/${{ github.base_ref }} HEAD | grep '\.tf$' | xargs -n1 dirname | sort -u)
          echo "ChangeddirectorieswithTerraformfiles:"
          echo "$changed_dirs"
          {
            echo "dirs<<EOF"
            echo "$changed_dirs"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

      # 6. 変更された各ディレクトリでTerraformの初期化と検証を実行(-backend=falseで状態取得をスキップ)
      - name: TerraformValidateinChangedDirectories
        run: |
          set -x
          IFS=$'\n' read -r -a dirs <<< "${{ steps.list_dirs.outputs.dirs }}" || true
          if [ ${#dirs[@]} -eq 0 ]; then
            echo "NochangedTerraformdirectoriesfound."
          else
            for d in "${dirs[@]}"; do
              echo "Processingdirectory:$d"
              (cd "$d" && terraform init -backend=false && terraform validate)
            done
          fi

Discussion