本記事のテーマはGitHub Actionsです。個人的に
- 普段GitHub Actionsを雰囲気で運用している人
- GitHub Actionsをコピペや生成AIで乗り切っている人
- 他者が書いたコードの意味をより深く理解したい人
本記事でGitHub Actionsの基本は説明しません。グッドプラクティスを含めて基礎から学びたい人は、拙著
GitHub Actionsの設計指針
GitHub ActionsはCI/
- フェイルファスト:すばやく失敗を検出し、効果的にフィードバックを得る
- ムダの最小化:不要なワークフロー実行を避け、待ち時間とコストを減らす
- セキュリティ:最小権限の原則を守り、ワークフローの侵害リスクを下げる
- メンテナンス性:デバッグや動作確認がしやすく、読み解きやすいコードにする
本記事ではこれらの設計指針を念頭に、11個のグッドプラクティスを説明します。そして紹介したグッドプラクティスを最後に集約し、便利なテンプレートコードへ落とし込みます。
フェイルファスト
GitHub Actionsは繰り返し実行します。そのためすばやく失敗を検出し、効果的にフィードバックを得ることが大切です。このようなフェイルファストの実現には、
タイムアウトを常に指定する
GitHub Actionsのデフォルトタイムアウトは360分です。無制限の通信待ちなどが発生すると、6時間も動き続けます。これはさすがに長すぎます。タイムアウトを指定しましょう。実装例は次のとおりです。
timeout-minutes: 5 # 分単位でタイムアウトを指定
これで意図せぬ異常が発生しても、すばやく検知できます。ユースケースに合わせて、適切なタイムアウトを設定しましょう。筆者自身はたいてい5分に設定します。
デフォルトシェルでBashのパイプエラーを拾う
Ubuntuランナーではシェル指定省略時にBashが起動します。ところがこのBash、パイプ処理中のエラーを無視します。エラー時に意図せず処理を継続するため、結果として不具合の温床になります。デバッグの難易度も上がるため、パイプエラーを無視するメリットは皆無です。
そこでパイプ処理中のエラーを拾えるよう、Bashの挙動を変更しましょう。通常のBashならpipefail
オプションを有効化して、パイプエラーを拾うのが定石です。ただGitHub Actionsには、もっと楽な方法があります。次のようにデフォルトシェルを定義するのです。
defaults:
run:
shell: bash # ワークフローで使うシェルをまとめて指定
GitHub ActionsではBashの利用を明示的に宣言すると、なぜかpipefail
オプションが有効化されます[1]。つまりデフォルトシェルを定義するだけで、pipefail
オプションが全ステップで有効になるわけです。より正確にいえば、次のオプションでBashが起動します。
bash --noprofile --norc -eo pipefail {0}
デフォルトシェルにデメリットはほぼないです。あらゆるワークフローへ組み込みましょう。
「actionlint」ですばやく構文エラーをチェックする
actionlintはGitHub Actions向けの静的解析ツールです。.github/
ディレクトリ配下のYAMLファイルをまとめてチェックし、構文エラーや非推奨構文などを検出します[2]。お試しならDockerイメージの利用が手軽です。次のように実行します。
docker run --rm -v "$(pwd):$(pwd)" -w "$(pwd)" rhysd/actionlint
actionlintの強みは、GitHub Actionsで実行する前に問題を検出できる点です。ワークフロー構文のタイプミスなども拾えるため、実運用では想像以上に活躍します。
ムダの削減
待ち時間ほどムダなものはありません。GitHub Actionsで待ち時間を削減するもっともよい方法は、不要なワークフローを実行しないことです。そもそも実行しなければ、待ち時間はゼロです。うれしいことにコストも削減できます。そこでムダを削減するプラクティスとして、
Concurrencyで古いワークフローを自動キャンセルする
プルリクエストで起動するワークフローは、最新コード以外での実行がたいてい不要です。自動テストや静的解析はその典型で、古いコードで実行されるワークフローはムダです。そこで次のように実装し、古いワークフローは自動キャンセルしましょう。
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true # 実行中ワークフローのキャンセルを有効化
キャンセル条件はgroup
キーへ指定します。このコード例では
不要なイベントで起動しないようにフィルタリングする
GitHub Actionsはイベントをトリガーにします。このトリガーになるイベントですが、特定の条件でフィルタリングできます。pull_
イベントやpush
イベントなら、ファイルパスやブランチで起動条件を調整可能です。Globも利用でき、たとえば次のように実装します。
on:
pull_request:
paths: ['**.go'] # Goのファイルが変更されたら実行
push:
branches: ['main'] # デフォルトブランチなら実行
tags: ['v*'] # バージョンタグが作成されたら実行
pull_
イベントのように実行頻度が高い場合、少しチューニングするだけでワークフローの起動が劇的に減ります。公式ドキュメントではアクティビティタイプなど、他のフィルタリング手法も紹介されています。ぜひ一度ワークフローの起動条件を見直してみましょう。
最安値のUbuntuランナーを優先する
プライベートリポジトリの場合、GitHub Actionsは使用時間に応じて課金されます[3]。この使用時間はワークフローの実行時間へ、ランナーごとに設定されている乗率をかけて計算します。
- 「使用時間」
= 「実行時間」 × 「ランナーごとに異なる乗率」
この乗率はUbuntuランナーがもっとも小さいです。そのため可能な限りUbuntuランナーを選択しましょう。これだけでコスト削減につながります。実装例は次のとおりです。
runs-on: ubuntu-latest
セキュリティ
GitHub Actionsは便利ですが、セキュリティと無縁ではいられません。最小権限を徹底し、侵害リスクを下げる設計が重要です。そこで
GITHUB_TOKENのパーミッションはジョブレベルで定義する
GITHUB_
はワークフロー実行時に、自動生成されるクレデンシャルです。このクレデンシャルを利用すれば、ワークフローからGitHub APIへ簡単にアクセスできます。
GITHUB_
の権限はパーミッションで制御し、最小権限での運用が鉄則です。最小権限で運用すれば、攻撃されても被害を小さくできます。またパーミッションの設定方法は3つあり、次のような優先順位が存在します。
- ジョブレベルへ定義したパーミッション
- ワークフローレベルへ定義したパーミッション
- リポジトリに設定されたデフォルトパーミッション
それでは最小権限のパーミッションを設定しましょう。まず実践したいのが、一度ワークフローレベルで全パーミッションを無効化することです。
permissions: {} # 全パーミッションの無効化
これでデフォルトパーミッションが使われなくなります。ただしこのままだと、コードの参照すらできません。そこであらためて、必要最小限のパーミッションをジョブレベルで定義します。
permissions:
contents: read # コードの読み込みを許可
このようにすれば多少記述量は増えますが、不要なパーミッションが自然と排除されます。ほぼ思考コストなしに最小権限を達成できるため、習慣にする価値はあります。
アクションはコミットハッシュで固定する
アクションのバージョンはよく、次のようにGitタグで指定します。
- uses: actions/checkout@v4
しかしGitタグは可変です。攻撃者がコードを改ざんしてGitタグも上書きすれば、アクション経由でワークフローを侵害できます[4]。そこでコミットハッシュの出番です。コミットハッシュはコミットごとに生成され、事実上一意な値になります[5]。そのためコミットハッシュを次のように指定すれば、アクションを不変リソースとして扱えます。
- uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
これで仮にアクションのコードが改ざんされても、影響を受けなくなります。ただしコミットハッシュのバージョンが安全か、確認するのはあなたの仕事です。コミットハッシュで固定しても、そもそも侵害されていたら無意味です。面倒でもコードを読む必要があります。
メンテナンス性
GitHub Actionsもコードの一種です。デバッグや動作確認がしやすく、読み解きやすいコードが望まれます。そこで役立つプラクティスが、
Bashトレーシングオプションでログを詳細に出力する
デバッグの第一歩は、なにが起きているか把握することです。そこで役立つのが、Bashのトレーシングオプションです。set -x
コマンドで有効化できます。これは簡易的なPrintデバッグで、
steps:
- run: |
set -x # 最初に一行追加するだけで、ログ出力が詳細になる
date
hostname
これで次のようにログへ出力される情報が増やせます。
単純ですがアクティブに開発している間だけでも入れておくと、デバッグが捗ります。
workflow_dispatchイベントで楽に動作確認する
ワークフローには
そこで活用したいのがworkflow_
イベントです。このイベントを次のように組み込むと、ワークフローの手動実行が可能になります。
on:
workflow_dispatch:
workflow_
イベントは起動タイミングが自由なだけでなく、任意のブランチで起動可能です。つまりデフォルトブランチへマージする前に、ワークフローの動作確認ができます[6]。特定のイベントでしか取得できない値がある場合は、次のように入力パラメータで注入します。
env:
PR_NUMBER: ${{ inputs.pr_number || github.event.pull_request.number }}
こうすることでワークフローのロジックは、workflow_
イベントでもほとんど検証できます。任意のタイミング・
ワークフローの背景情報をコメントへ書き残す
実装者にとって、ワークフローは自明に感じることが多いです。つい
コードがあるので
そこでワークフローの目的や影響範囲を、コードの冒頭にコメントで書きましょう。ワークフローの背景情報が数行書いてあるだけで、読む人の理解を大きく助けます。
# なにをするワークフローか手短に記述
#
# ワークフローの目的や影響範囲、参考URLなどを数行で書く。
# 実装詳細ではなく、コードから読み取れない背景情報を中心にする。
---
name: ...
賭けてもいいです。あなたが実装したワークフローは、他人にとって自明ではありません。大作にする必要はないので、手がかりをきちんと残しましょう。効果は絶大です。
テンプレートコード
最後に本記事で登場した、11個のグッドプラクティスをおさらいします。
- タイムアウトを常に指定する
- デフォルトシェルでBashのパイプエラーを拾う
「actionlint」 ですばやく構文エラーをチェックする - Concurrencyで古いワークフローを自動キャンセルする
- 不要なイベントで起動しないようにフィルタリングする
- 最安値のUbuntuランナーを優先する
- GITHUB_
TOKENのパーミッションはジョブレベルで定義する - アクションはコミットハッシュで固定する
- Bashトレーシングオプションでログを詳細に出力する
- workflow_
dispatchイベントで楽に動作確認する - ワークフローの背景情報をコメントへ書き残す
個々のプラクティスは難しくないですが、少し数が多いです。そこでグッドプラクティスを実践しやすいよう、テンプレートコードを作成しました。よければご活用ください。
# なにをするワークフローか手短に記述
#
# ワークフローの目的や影響範囲、参考URLなどを数行で書く。
# 実装詳細ではなく、コードから読み取れない背景情報を中心にする。
---
name: Example
on:
# 動作確認しやすいように手動起動をサポート
workflow_dispatch:
# プルリクエストはファイルパスでフィルタリング
pull_request:
paths: [".github/workflows/**.yml", ".github/workflows/**.yaml"]
# ワークフローレベルでパーミッションをすべて無効化
permissions: {}
# デフォルトシェルでパイプエラーを有効化
defaults:
run:
shell: bash
# ワークフローが複数起動したら自動キャンセル
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
example:
# もっとも安価なUbuntuランナーを利用
runs-on: ubuntu-latest
# 6時間も待たされないようにタイムアウトを設定
timeout-minutes: 5
# ジョブレベルで必要最小限のパーミッションを定義
permissions:
contents: read
steps:
# アクションはコミットハッシュで固定
- name: Checkout
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
# Bashトレーシングオプションの有効化でログを詳細化
- name: Run actionlint
run: |
set -x
docker run --rm -v "$(pwd):$(pwd)" -w "$(pwd)" rhysd/actionlint:1.7.3
まとめ
本記事ではGitHub Actionsのグッドプラクティスを紹介し、最後にテンプレートコードを実装しました。盛りだくさんでしたが、本記事で登場していないプラクティスは他にも多数あります。
GitHub Actionsをもっと詳しく知りたい人は、