目次

GitHub Actions Workflow Best Practices

こんにちは。2022年新卒入社の上田です。現在は本配属前のジョブロで ABEMA に所属しております。

この記事では GitHub Actions workflow/job 作成に関するベストプラクティスをまとめました。
workflow/job 追加時に参照できるチェックリストとしてご利用いただけるような一記事として公開しています。堅牢で安全な CI/CD pipeline を作る参考になれば幸いです。

(ジョブロ:メディア事業部における新卒研修の一環として二ヶ月間実施される本配属前のジョブローテーション研修)

自己紹介

GitHub Actions は Beta v1 の頃から利用しており、個人としても
peaceiris/actions-gh-pages

などをはじめとして、いくつかのサードパーティー Action を作成してきました。

私が日頃 GitHub Actions を利用する上で意識していることをこの記事で共有したいと思います。

timeout-minutes で最大実行可能時間を指定

プロセスが終了しない限り、デフォルトで job は6時間実行されます。例えば step 内のプロセスが hang-up した際にはそれを検知することができずにそのまま6時間実行されてしまうため、早めの異常発見、実行時間節約のために上限を短く設定しておくとよいでしょう。


jobs:
  main:
    runs-on: ubuntu-20.04
    permissions:
      contents: read
    timeout-minutes: 5

普段の平均実行時間を実行ログから調べ、最大値以上を設定値の目安とすれば十分でしょう。

個人的な具体的を挙げると

  • 普段の平均実行時間が5分(最大5分30秒)であれば timeout-minutes: 10
  • 普段の平均実行時間が30分(最大35分)であれば timeout-minutes: 45

というように決めています。

また、現在 (2022-06-30) workflow 全体で timeout-minutes を指定することはできないため、各 job で設定する必要があります。

The maximum number of minutes to let a job run before GitHub automatically cancels it. Default: 360


timeout-minutes | Workflow syntax for GitHub Actions – GitHub Docs

セキュリティリスク軽減のために意識したい項目

以降のセクションでは workflow のセキュリティリスクを軽減するために実施できる対策を紹介します。

その他の実施可能な対策は、公式ドキュメントも参考になります。


Security hardening for GitHub Actions – GitHub Docs

permissions で GITHUB_TOKEN の権限を管理

GitHub Actions runner ではワークフロー内で GitHub に対する認証に利用できるトークンが生成されています。${{ secrets.GITHUB_TOKEN }}${{ github.token }} で参照して Git 関連の認証や Issue/Pull-request の操作のために利用できて非常に便利です。

ですが GITHUB_TOKEN はデフォルトで多くの read/write 権限を持つため意図しない権限付与が行われてしまいます。GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}GITHUB_TOKEN 環境変数を作成していない場合でも、スクリプトやサードパーティー Action からコンテキスト github.token を参照することが可能なため、悪意のあるスクリプトが workflow に混入して実行された場合に脆弱です。

Workflow/job 単位で GITHUB_TOKEN へ与える権限を設定することが可能なため、できる限り設定しておきましょう。

例えば actions/checkout で git clone するだけであれば contents: read を指定するだけで十分です。


jobs:
  main:
    runs-on: ubuntu-20.04
    permissions:
      contents: read
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v3

一切の権限も渡したくない場合は以下のように指定できます。


jobs:
  main:
    runs-on: ubuntu-20.04
    permissions: {}
    timeout-minutes: 5

pull-request にコメントを投稿したり commit status を更新するような action やスクリプトを利用するなら checks: writepull-requests: write を渡します。


jobs:
  main:
    runs-on: ubuntu-20.04
    permissions:
      checks: write
      pull-requests: write
    timeout-minutes: 5

参考:


Automatic token authentication – GitHub Docs


Workflow syntax for GitHub Actions – GitHub Docs

OIDC により管理する認証情報を減らす

例えば AWS CLI を GitHub Actions で実行する際の認証方法として、認証情報 (access key id や secret access key) を secrets として登録して利用する方法以外に OIDC (OpenID Connect) が利用できます。secrets を GitHub Actions runner に読み込む必要がなくなるため、まずは OIDC の利用を検討しましょう。

GCP の導入例


Configuring OpenID Connect in Google Cloud Platform – GitHub Docs


name: List services in GCP

on:
  pull_request:
    branches:
      - main

jobs:
  main:
    runs-on: ubuntu-20.04
    permissions:
      id-token: write
    steps:
    - id: 'auth'
      name: 'Authenticate to GCP'
      uses: 'google-github-actions/auth@v0.3.1'
      with:
          create_credentials_file: 'true'
          workload_identity_provider: '<example-workload-identity-provider>'
          service_account: '<example-service-account>'
    - id: 'gcloud'
      name: 'gcloud'
      run: |-
        gcloud auth login --brief --cred-file="${{ steps.auth.outputs.credentials_file_path }}"
        gcloud services list

AWS の導入例


Configuring OpenID Connect in Amazon Web Services – GitHub Docs


name: AWS example workflow

on:
  push:
    branches:
      - main

env:
  BUCKET_NAME : "<example-bucket-name>"
  AWS_REGION : "<example-aws-region>"

jobs:
  main:
    runs-on: ubuntu-20.04
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/checkout@v3
      - uses: aws-actions/configure-aws-credentials@v1
        with:
          role-to-assume: arn:aws:iam::1234567890:role/example-role
          role-session-name: samplerolesession
          aws-region: ${{ env.AWS_REGION }}

      - name:  Copy index.html to s3
        run: |
          aws s3 cp ./index.html s3://${{ env.BUCKET_NAME }}/

利用する Action のバージョン固定

Action を利用する場合には、

  1. Commit hash 指定
  2. タグ指定
  3. Major タグ指定

の優先順位で、実行する action のバージョンを固定しておくことがベストです。

例えば master, main などの branch 指定での実行は

  • そのブランチに破壊的変更が入った場合に step が突然失敗するようになる
  • 悪意のある変更が含まれてしまった場合に脆弱

なため絶対に利用しないようにしましょう。

悪意のある変更が含まれてしまった場合に脆弱

なのはタグ指定の場合も同じ(git tag は上書き可能)であるため、Commit hash 指定での利用が最善です。

特に GitHub Actions 公式ではないサードパーティー action はなるべく、

  • 利用したい Commit hash を決める
  • その Commit hash において action のソースコードに悪意のある記述が含まれていないか確認
  • TypeScript/webpack/ncc が利用されている場合は Git 管理下にあるビルドアセットに手動で変更が加えられていないか確認

の手順を踏んだ上で Commit hash 指定で利用しましょう。

Best

actions/checkout を例に具体例を挙げていきます。

Action の 2541b1294d2704b0964813337f33b291d3f8596b (v3.0.2) の内容に問題がないことを確認して、コミットハッシュ指定で利用します。


- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b

Good

major tag 指定での利用。


- uses: actions/checkout@v3

特定タグ指定での利用。


- uses: actions/checkout@v3.0.2

ダメ絶対

Action のブランチ指定利用は避けましょう。


- uses: actions/checkout@main

利用する Action の継続的アップデート

Dependabot を利用して action の commit hash 指定やタグ指定で利用している Action のバージョンを更新することができます。


Configuration options for the dependabot.yml file – GitHub Docs

以下のような内容で .github/dependabot.yml を用意するだけで導入できます。


version: 2
updates:

- package-ecosystem: "github-actions"
  directory: "/"
  schedule:
    interval: "daily"
    time: "11:00"
    timezone: "Japan"
  commit-message:
    prefix: "ci"

導入メリットとしては、

利用する Action の最新版を検知できます。
たまに複数バージョンをまとめてアップデートするよりも、最新版がリリースされるたびに検証して更新する方が確認する差分が少ないため、運用としては楽だと思います。
対象 Action の新機能リリースや機能廃止に気づけるという副次的なメリットもあります。

更新作業が楽になる
すべての workflow において更新を施した pull-request を Dependabot が作成してくれるため、開発者がファイルを更新する必要はありません。
CI の PASS と Action の更新内容を確認すればあとはマージするだけです。

Dependabot で継続的に GitHub Actions workflow もメンテナンスしていきましょう!

なるべくサードパーティー Action は使わずに Makefile などにまとめる

結論から述べると「サードパーティー Action を利用しないことを最初に検討する」ことが良いと思っています。

具体例

例えば abema という実行可能バイナリが提供されているコマンドがあり、それをラップした abema/login-action が公開されていると想定して abema/login-action の利用が最善ではないことを具体例を示しながら解説します。

abema login を実行するためだけに abema/login-action を利用するのは最善なのか

を考えてみると abema login を実行したいだけだが abema/login-action 経由で abema を実行しているために、

  • abema login のオプションに加えて abema/login-action のオプションを把握する手間が増えている
  • abema/login-action の動作を手元で確認できない
  • abema/login-action という依存・障害点・セキュリティリスクが増えている

など上記の懸念点が想定され、サードパーティー Action を使うのが最善ではないケースがあることが分かります。

abema login のケースでは、

  • abema を workflow でインストールする
  • abema login を workflow で直接実行する、もしくは Makefile にまとめて make 経由で実行する

のが良い選択肢だと考えられます。

特に、実行バイナリが提供されているコマンドをラップしたようなサードパーティー Action は導入するメリットは特にないと思います。

(歴史的背景として GitHub Actions Beta v1 はすべて Docker Action として提供されていたために、このようなタイプのサードパーティー Action が有効だったという背景があります)

障害点・依存を増やすことにもなるため、サードパーティー Action の導入には慎重になりたいです。

remote/local の互換性を高めよう

個人的な見解ですが「CI/CD workflow は手順書としての役割もある」と思います。

CI/CD で実施する内容を README に書いておくこともありますが、実際に実行されているのは workflow の内容です。workflow を一時情報として参照できると workflow の内容をドキュメントに反映するような二重管理をなくすことができます。

Workflow で実行するタスクはローカルでも実行できる形式で記述されていることが望ましいです。
YAML に実行コマンドを記述することにより workflow が手順書としても機能し、サードパーティー Action が記述されている場合と比べて何をしているのか把握しやすくなります。記述量が多くなりそうであれば make, npm-scripts などタスクランナーのタスクとして実行するようにできます。
例えば make abema-login の形式で実行できるようにすることにより workflow が簡略化され、ローカルでも検証しやすいです。

ローカル実行しやすい形であれば GitHub Actions の障害でデプロイ等が出来ない場合でも、最悪すぐローカルで手動実行することができます。

GitHub Actions への依存を減らす意味でも、サードパーティー Action への依存は減らしておきたいです。

gh が便利です

GitHub 関連の単純なオペレーションを行いたい場合はまず gh の利用が検討できます。
GitHub Actions runner にプリインストールされており、すぐに利用できます。


cli/cli: GitHub’s official command line tool


      - name: List Issues
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: gh issue list

インストールスクリプト・バイナリは信用しない

インストールスクリプトなどを利用してツールを GitHub Actions runner 上にインストールする場合には以下を考慮したいです。

  • インストールスクリプトのバージョン管理と改ざん検知
  • バイナリのバージョン管理と改ざん検知

改ざん検知していれば防ぐことができたであろうインシデントの例:
Bash Uploader Security Update – Codecov

セキュリティリスクを減らすためにもぜひともしっかり対策しておきたいです。

対策例

仮定する状況

インストールスクリプト・ビルド済み実行可能バイナリが提供されているツールをインストールして利用したい

例として golangci-lint を用います。

改ざん対策・検知を施す場合のツール導入手順は以下となります。

  • インストールするバージョンを決める: 1.43.0
  • 1.43.0 のインストールスクリプトに悪意のある記述がないかを検証
    • 問題なければそのスクリプトのチェックサム(checksum)を確認する sha256sum install-golangci-lint.sh
  • ビルド済みバイナリに対しても同様の確認をする
    • 今回は install-golangci-lint.sh でビルド済みバイナリのチェックサムが行われている
  • インストールの際にチェックサムを検証する
  • 一連のコマンドをインストールタスクとして Makefile などのタスクランナーに登録して Workflow で呼び出して利用する

ビルド済みバイナリのチェックサムに関して補足ですが GitHub Release assets を信頼していることが前提となります。
メンテナーのアカウントが乗っ取られてバイナリとチェックサムファイルがどちらもすり替えられた場合や GitHub の機能が乗っ取られてすり替えられた場合には対処できません。

以下、インストール時のチェックサム例。


    - name: Install golangci-lint
      run: |
        GOLANGCI_LINT_VERSION="v1.43.0"
        curl -sSfL "https://raw.githubusercontent.com/golangci/golangci-lint/${GOLANGCI_LINT_VERSION}/install.sh" > ./install-golangci-lint.sh
        echo "294771225087ee48c8e0a45a99ac82ed8f9c6e9d384e692ab201986479c8594f  install-golangci-lint.sh" | shasum -a 256 -c
        cat ./install-golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin "${GOLANGCI_LINT_VERSION}"
        rm ./install-golangci-lint.sh
        golangci-lint --version
    - name: lint
      working-directory: ${{ matrix.config }}
      run: golangci-lint run --deadline=300s

これによりインストールスクリプト・ビルド済みバイナリの改ざんを検知して処理を中断させることが可能となります。

あらゆるバージョンを固定する

ライブラリバージョンや Action version は当然として、

  • runner version: ubuntu-20.04 など
  • スクリプト内でインストールして使うツールのバージョン

などもなるべく固定しましょう。


jobs:
  main:
    runs-on: ubuntu-20.04  # runner OS version
    permissions:
      contents: read
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v3
      - run: make install  # インストールするツールのバージョン

例えば ubuntu-latest はある日を境に新しい Ubuntu LTS を指すように更新されるでしょう。
latest で利用しているとその更新タイミングをコントロール下に置くことができません。

バージョンを固定しておくことにより main branch の CI が突然落ちるようになる、というケースを減らすことができます。

まとめ

GitHub Actions workflow/job を堅牢で安全にするために気をつける点を紹介しました。
この記事がチェックリストとして参考になれば幸いです。

ここ最近は連日のように猛暑日が続いております。皆様も体調には十分お気を付けてお過ごしください。