GitHub Actions入門 ── ワークフローの基本的な構造からOIDCによる外部サービス認証まで

GitHubが公式に提供するGitHub Actionsは、後発ながらよく使われるワークフローエンジンとなっています。本記事では、藤吾郎(gfx)さんが、典型的なCI/CDのユースケースに即したワークフローの設定と管理について解説するとともに、注目されているGitHub OIDC(OpenID Connect)の利用についても紹介します。

GitHub Actions入門 ── ワークフローの基本的な構造からOIDCによる外部サービス認証まで

GitHub Actionsは、GitHubが提供するCI/CDのためのワークフローエンジンです。ワークフローエンジンは、ビルド、テスト、デプロイといったCI/CD関連のワークフローを実行し、定期実行するワークフローを管理するなど、開発におけるソフトウェア実行の自動化を担います。

GitHub Actions - アイデアからリリースまでのワークフローを自動化

GitHub Actionsは2018年にリリースされた比較的新しいサービスですが、GitHub自身に組み込まれていることから、GitHubユーザーにとっては「デフォルトのCI/CD」として好まれ、2022年現在はもっともメジャーなCI/CDのためのワークフローエンジンの1つになりました。

この記事では、GitHub Actionsの基本的な構造と典型的なユースケースにおけるワークフローの記述方法を紹介していきます。ビルドマトリックス、タイムアウト、GitHub APIを利用するための認証・認可の管理といった実践的な技術に加えて、2021年11月から提供されているGitHub OIDC(OpenID Connect)を利用した外部サービスの認証についても紹介します。

GitHub Actionsとは何か? どういう特徴があるか?

GitHub Actionsは、ワークフローエンジンです。ワークフローエンジンとして著名なソフトウェアやサービスには、他にJenkinsやCircle CIなどがあります。GitHub Actionsが競合に対して優れているのは、GitHubサービスの一部品であることです。

これにより、まずセットアップが非常に簡単です。必要なのはGitHubアカウントとリポジトリへのアクセス権だけなので、新たなユーザーやロール、パーミッションの管理をするサービスを増やさなくてすみます。

また、全ての設定がGitHubに存在するため、管理がとてもシンプルです。JenkinsやCircle CIも多くの設定をファイルとしてリポジトリに入れて管理することできますが、GitHub Actionsと比べてワークフローエンジンのサービス側で持たなければならない設定が少なくありません。

このようにGitHubサービスの一部であるがゆえに、設定の管理がシンプルであることが、GitHub Actionsの大きな利点です。

一方で、後発のサービスゆえに足りない機能もあります。例えば、JenkinsやCircle CIではビルド時間の観測やチャート化が標準で備わっていますが、GitHub Actionsにはありません。ビルド時間は、開発生産性の指標の1つですが、これを標準で一望できる機能はまだ提供されていません(もっともGitHub Actionsは改善速度も早いので半年後には実装されているかもしれません)

GitHub Actionsにおけるワークフローの基本的な構造

GitHub Actionsにおけるワークフロー(workflow)とは、自動化するタスクの実行単位です。ワークフローはリポジトリの中で発生するイベントや、特定の時刻を起点としたイベントなどによって起動されます。

例えば「コードベースの変更ごとに、ビルドとテストを走らせる」ことを考えます。この「コードベースの変更ごとに」が、イベント(event)と呼ばれる要素です。イベントはランナー(runner)というワーカーインスタンスを起動し、ランナーの中で1つ以上のジョブ(job)を実行します。1つのジョブは複数のステップ(step)を持ち、これが実行の最小単位となってビルドやテストなどを行います。

このイベント、ランナー、ジョブ、ステップを記述するのが、ワークフロー(workflow)です。ワークフローは、YAMLファイルとしてリポジトリの中でバージョン管理の対象となります。

GitHub Actionsにはさらに、アクションというコンポーネントもあります。これはワークフローの中のステップとして利用できるソフトウェアパッケージです。例えばactions/checkoutというアクションは、リポジトリからgit clonegit checkoutをするためのものです。これは次のGitHubリポジトリに対応しており、このactionsはGitHub公式のアクション用Organizationです。

actions/checkout: Action for checking out a repo

ワークフローを定義してみる

それではGitHub Actionsのワークフローを実際に作ってみましょう。ワークフローの作成にあたってはいくつか注意点があります。GitHub Actionsを使うのが初めてなら、まず空のリポジトリで試してみることをおすすめします。本記事でも空のリポジトリでワークフローの作成を行うことを前提とします。

なお、本記事で設定するワークフローは、筆者が事前に用意した次のリポジトリで確認できます。

gfx/github-actions-showcase: A set of examples for GitHub Actions

ワークフローファイルの作成

ワークフローファイルの実体はただのYAMLファイルです。普通のファイルなのでどのような方法でも作れますが、リポジトリの「Actions」タブからGitHubの用意したテンプレートをもとに作ることが多いでしょう。

ワークフローが1つもないリポジトリでは、「Actions」タブは以下のような画面になっていると思います。

Get Started with Github Actions

ここでは「set up a workflow yourself」を選んでください。これは一番単純なワークフローのテンプレートで、内容はコメントを除くと次のようなものになっているはずです。

name: CI

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Run a one-line script
        run: echo Hello, world!

      - name: Run a multi-line script
        run: |
          echo Add other actions to build,
          echo test, and deploy your project.

トップレベルのnameキーはワークフロー名、onキーはイベントトリガーの設定、jobsキーはジョブの設定を表します。これはそれぞれ後述の節で解説します。runキーにはシェルスクリプトを書きます。

これの内容をhello.ymlという名前でデフォルトブランチに対してコミットしてください。もちろんプルリクエスト経由でもかまいません。すると、git pushやプルリクエストの作成ごとにこのhello.ymlワークフローが実行されます。実行の記録はプルリクエストやコミット、またはリポジトリのActionsタブから参照できます。

Create hello.yml by gfx · Pull Request #1 · gfx/github-actions-showcase

ところで、ワークフローの一部の機能はデフォルトブランチにマージされてはじめて有効になるものがあります。例えばこのhello.ymlにおけるon.workflow_dispatchというイベントトリガーは、ワークフローを手動で起動するためのトリガーですが、これはデフォルトブランチにワークフローファイルが存在しなければ有効になりません。

Events that trigger workflows - GitHub Docs

したがって、on.workflow_dispatchを持つワークフローファイルがある程度複雑な場合には、開発のために一工夫必要です。例えば、いったんon.workflow_dispatchキーだけを設定して、ジョブの中身は空のままデフォルトブランチに(通常はプルリクエスト経由で)コミットし、そのあとでジョブの中身を実装するという方法があります。

ワークフロー名とファイル名

ワークフローを定義するときには、リポジトリ内において一意の名前をつけます。ワークフローの名前は、ワークフローの目的を簡潔に示すものがよいでしょう。CIであればそのまま「CI」、デプロイであれば「Deployment」などです。ワークフロー名には空白や記号も入れられます。

ワークフローのファイル名は、ワークフロー名に対応した名前にするとよいでしょう。システム的にはワークフロー名とファイル名はまったく別のものにもできますが、関係ない名前にすると保守の際に混乱します。ベストプラクティスとしては「ワークフロー名を全て小文字にし、英数字以外の記号をアンダースコアに置き換えたもの」くらいの機械的なルールを作って運用するのがよいと思います。

ワークフローのファイル名は、ワークフローの識別子として使われます。これは例えば次のように、ワークフローのURLに含まれます。

https://github.com/gfx/github-actions-showcase/actions/workflows/hello.yml

パーマリンクの一貫性を保つため、なるべく運用を始めた後では変えない方がよいでしょう。実運用では前述のとおり、ワークフローファイル名をワークフロー名から推測できる名前にすることをおすすめします。

ワークフローを起動する主なイベントトリガー

ワークフローファイルのトップレベルのonキーは、イベントトリガーの設定です。よく使われるトリガーを紹介します。

on.push - リポジトリへのgit pushによって起動するトリガー

on.pushは、リポジトリにコミットがgit pushされたときに起動するトリガーです。

なお、OSSプロジェクトでCIを設定する場合には、フォークからのプルリクエストでCIを実行させるため、次に説明するon.pull_requestも同時に設定することが多いでしょう。フォークしたリポジトリからフォーク元にプルリクエストを作ったときは、on.pushが起動しないからです。

しかし、on.pushon.pull_requestを両方設定すると、プルリクエストへのgit pushごとにワークフローが二重に起動します。これを避けるため、次のようにデフォルトブランチへのpushのみに限定するよう設定します。

on.push.branches: [main]

一方で、プライベートリポジトリでフォークを禁止している場合、CIのためのイベントトリガーはon.pushだけで十分です。

on.pull_request - プルリクエストが作成・更新されると起動するトリガー

on.pull_requestは、プルリクエストの作成・更新などのイベントで起動するトリガーです。

なお、ワークフローのテンプレートだと on.pull_request.branches: [main] が設定されていますが、これはmainブランチに向けたプルリクエストでのみ起動するという意味です。実際はCIをmainブランチに限定する必要はほとんどないので、単にon.pull_requestだけでよく、branchesを設定する必要はありません(CI以外のワークフローでは、対象ブランチを限定した方がいいことはあるかもしれませんが)

on.schedule - 定期実行を管理するトリガー

on.scheduleは、POSIXのcronフォーマットで指定したUTC(協定世界時)の日時に起動するトリガーです。これにより、GitHub Actionsをバッチの定期実行などに使うこともできます。

使い方は次の通りです。cronフォーマットはそのままだとミスしやすいので、Cron Helperなどのツールで確認しながら設定するとよいでしょう。

on:
  schedule:
    - cron: '29 5,17 * * *'

なお、GitHub Actionsに限らずあるタスクを定期実行する場合には、なるべく「毎時0分」など切りのよい時刻で起動させるのを避けるのが、負荷分散のためによいとされています。

実行結果の確認

on.scheduleon.workflow_dispatchなど、リポジトリの更新に紐付かないトリガーを持ったワークフローは、リポジトリのActionsタブを見に行かなければ、結果を知ることができません。このため実行失敗に気づきにくいので、成否(あるいは失敗のみ)をSlackに通知するなどの工夫をした方がよいでしょう。

現状ではSlackのGitHubアプリで検出できないため、自前で通知するしかありません。Slack通知のペイロードを組み立てるのはさほど難しくありませんが、次のようなサードパーティ製のアクションもあります。

ravsamhq/notify-slack-action: 🔔 Send a Slack Notification from Github Actions regarding failure, warnings, or even success.

その他のトリガー

ここまで説明したものも含め、GitHub Actionsで利用できるイベントトリガーは非常に多岐にわたります。

Events that trigger workflows - GitHub Docs

ところで、GitHubにはActionsのほかにWebhookがあり、こちらにも同様のイベントトリガーがあります*1。しかし、WebhookではGUIで設定したものがリポジトリ外のメタデータとして扱われます。また、バージョン管理もされません。特定のAPIエンドポイントにリクエストするだけであれば、なるべくWebhooksよりActionsを使う方がよいでしょう。

ビルドマトリックスで複数のジョブを作成する

ビルドマトリックスは、複数の条件で同じ構造のジョブを走らせたいときに使う機能です。例えば、ランナーをubuntu-latestmacos-latestにしたい、プログラミング言語処理系のバージョンを複数個テストしたい、などです。

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        runner: [ubuntu-latest, macos-latest]
        nodejs: [12, 14, 16]
    name: '${{ matrix.runner}} x nodejs/${{ matrix.node }}'
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.nodejs }}

上記のように設定すると、このmatrixの定義が自動的に生成するパラメータの組み合わせは、runnerが2パターン、Node.jsのバージョンが3パターンで、次の2 × 3 = 6パターンのジョブを実行できます。

  • runner=ubuntu-latest x nodejs=12
  • runner=ubuntu-latest x nodejs=14
  • runner=ubuntu-latest x nodejs=16
  • runner=macos-latest x nodejs=12
  • runner=macos-latest x nodejs=14
  • runner=macos-latest x nodejs=16

includeを使ってパラメータパターンを構築する

パラメータの組み合わせでマトリックスを生成するのではなく、全てのパラメータパターンを手動で構築するには、includeキーを使います。次の例では、「あるパラメータ(runner)と、それに対応するname」というパラメータのリストを定義しています。これはincludeキーを使わずに実現するのは困難です。

jobs:
  build:
    strategy:
      fail-fast: false
      matrix:
        include:
        - name: ubuntu
          runner: ubuntu-latest
        - name: macos
          runner: macos-latest

    name: '${{ matrix.name }}'
    runs-on: '${{ matrix.runner }}'

本来のincludeキーは、パラメータの組み合わせにさらに別のパラメータを付け加える(includeする)機能ですが、その用途では分かりにくいパラメータパターンになりやすいので、筆者は使いません。むしろ上記のように、平坦なパラメータリストをincludeキーだけを使って作るのがよいと思います。

マトリックスのあるジョブが失敗したとき

ここまで紹介したビルドマトリックスの設定例には、次のキーが設定されています。これは、ビルドマトリックスのあるジョブが失敗したときに他のジョブを即座に失敗にしないという意味です。

strategy.fail-fast: false

デフォルトtrueでは、ビルドマトリックスのあるジョブが失敗したときに他のジョブも実行中だった場合、残りの実行中のジョブを即座に失敗させてしまいます。このデフォルトの挙動はたいていの場合不便なので、fail-test: falseは常に指定しておいてよいと思います。

timeout-minutesキーによるタイムアウト

ワークフローに対してはtimeout-minutesキーでタイムアウトを設定できます。このタイムアウトのデフォルト値は360分、つまり6時間です。

このデフォルト値は非常に長いため、ワークフローにうっかり無限ループなどを仕込むと、GitHub Actionsの利用限度(usage limit)を無駄に消費してしまいます。利用限度はGitHubのプランにもよりますが、限度を超えた分は有料なので、ベストプラクティスとしてタイムアウトは常に指定しておくべきです。

timeout-minutesキーは、ジョブまたはステップに対して指定できますが、ほとんどのケースではジョブに設定するだけで十分でしょう。例えば、ジョブのタイムアウトを5分に設定するには、ワークフローファイルに次のように記述します。

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      # ...

ワークフローで認証情報などの秘匿値を使う

認証情報や秘密鍵などの秘匿値をワークフローで扱いたいときは、リポジトリの秘匿情報(Settings > Secrets > Actions secrets)で秘匿値を記録し、それをワークフローで参照するようにします。

例えば、FOOという名前の秘匿値を設定したとき、ワークフローファイルの任意の場所、例えばスクリプトの実行を行うrunキーで、次のようなマクロでFOOを参照できます。

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 5

    steps:
      - run: |
          echo '${{ secrets.FOO }}'

このマクロは、runキーの値がシェルに渡される前に展開されます。さらに、secretsコンテキストの値がGitHub Actionsのログに渡されるときは*でマスクされます。例えばこの例の場合、FOObarという値を設定したとすると、echo '${{ secrets.FOO }}'のログはecho '***'になり、その出力は***になります。

このマスク処理は、ログ中にたまたま秘匿値が現れたときも行われます。この例では、echo 'foobar'というスクリプトを実行したときのログはecho 'foo***'、出力はfoo***になります。

ただし、リポジトリの秘匿情報は確かに認証情報などの管理に必要な機能ですが、後述するGitHub OIDCによる認証の方が安全性が高い方法です。認証したい外部サービスがGitHub OIDCをサポートしているのであれば、GitHub OIDCによる認証を検討することをおすすめします。

ワークフローで使えるアクセストークンと権限管理

GitHub Actionsでワークフローを起動するたびに、そのワークフロー内でのみ有効なアクセストークンを生成します。このGITHUB_TOKENには、${{ secrets.GITHUB_TOKEN }}または${{ github.token }}としてアクセス可能です。このトークンは、actions/checkoutアクションで自動的に使われるほか、ステップでGitHub APIやGitHub CLI(ghコマンド)を使うときの認証に利用できます。

このトークンは、便宜上github-actions[bot]という特殊なユーザーに紐付けられていますが、通常のユーザーが発行するパーソナルアクセストークンとは異なる特徴がいくつかあります。

GITHUB_TOKENの制限

GITHUB_TOKENの第一の特徴は、制限の多さです。まず、GitHub Actionsでactions/checkoutアクションを使うときは、たとえwith.submodule: trueを設定したとしても、サブモジュールに設定した他のプライベートリポジトリをチェックアウトすることはできません。これは、GITHUB_TOKENがGitの認証に使われるからです。パブリックリポジトリであればgit cloneはできます。

また、該当リポジトリにgit pushはできますが、その場合on.pushなどのトリガーを起動しません。これはon.pushで起動したGitHub Actionsの中からgit pushを行うと同じワークフローを無限に呼ぶことになりかねないからと説明されています(下記のリンクを参照*2。残念ながら、2022年4月現在このGITHUB_TOKENに対してこの制限を外すことはできません。

Automatic token authentication - GitHub Docs

この制限を超えた操作を行うときは、パーソナルアクセストークンかGitHub Appsで生成したアクセストークンが必要です。チームメンバーのパーソナルアクセストークンはアクセス範囲が広すぎることが問題ですが、当面の問題をシンプルに解決できます。GitHub Enterpriseであれば、このような用途のためだけに作成したユーザー(サービスアカウント)のパーソナルアクセストークンを使う手もあります。

GitHub Appsでアクセストークンを生成する方法は、GitHub Actionsで生成されたトークンの制限を突破する方法としてよく案内されています。しかし、準備と運用が複雑なうえ、GitHub Organization(組織アカウント)の場合はGitHub Appsを作成するためにOrganizationの管理者権限も必要です。さらにGitHub公式ドキュメントがほとんどないという問題もあります。このような事情により、GitHub Appsを使う方法はこの記事では掘り下げません。筆者としては、いずれGitHubからよりよい方法が提供されるはずだと予想しています。

GITHUB_TOKENの権限管理

GITHUB_TOKENの第二の特徴は、権限管理の方法がパーソナルアクセストークンと全く異なることです。

まず、GITHUB_TOKENにはデフォルトの権限があり、デフォルトでは「Read and write permissions」、つまり「該当リポジトリに対しては全てのことが行える」となっています。公式ドキュメントではpermissiveと呼ばれています。

これは、該当リポジトリのコンテンツだけを読むことができる「Read repository contents permission」に変更できます。こちらはrestrictedと呼ばれます。GITHUB_TOKENのデフォルトの権限は、個人リポジトリであれば、このpermissiverestrictedをリポジトリごとに設定します。

しかし、GitHub Organization下のリポジトリは、Organizationの設定でrestrictedに設定すると、リポジトリの設定に関わらずデフォルトのパーミッションはrestrictedになります。セキュリティの観点からいえば、自動生成されるアクセストークンの権限は最小限であるべきですから、もしあなたがGitHub Organizationの管理者であれば、このrestrictedを検討するべきです。その場合、個々のリポジトリではGITHUB_TOKENのデフォルトの権限がrestrictedであると仮定してワークフローを実装するとよいでしょう。

GITHUB_TOKENの権限は、ワークフローファイルのトップレベルまたはジョブごとにpermissionsキーで制御できます。設定可能な権限は、次のリンク先で確認できます。

Permissions - Workflow syntax for GitHub Actions - GitHub Docs

GitHub OpenID Connectにより外部サービスで認証する

ワークフローからAWS(Amazon Web Services)やGCP(Google Cloud Platform)などの外部サービスを利用したいときには、GitHub OpenID Connect(OIDC)を使ってそれら外部サービスの認証情報を取得できます。

GitHub OIDCによる認証情報の取得は、外部サービスの認証情報をリポジトリの秘匿情報として管理する方法と比較すると、安全性に優れています。リポジトリの秘匿情報は、設定した人物の退職に伴って秘匿値の更新などの対応が必要です。また、その認証情報の有効期限も長いことが多く、漏洩したときの影響が大きくなる可能性があります。

GitHub OIDCは、リポジトリに秘匿値を保存しなくてすみ、ワークフローの実行ごとに有効期限の短い認証情報を取得できます。このため秘匿情報として管理する方法の問題を解消しています。

欠点は、GitHub OIDCという機能、そして背景技術であるOpenID Connectや、JWT(JSON Web Token)およびJOSE(JSON Object Signing and Encryption)、PKI(Public Key Infrastructure)などが複雑で難解なこと。それゆえにたとえ動いているとしても、本当に正しく設定できているかどうか確信を得るのが難しいことです。ただこの欠点を踏まえても、利点の方が大きいと思っています。

GitHub OIDCのアーキテクチャ

GitHub OIDCによってクラウドプロバイダーの認証情報を取得する流れは以下のようになります。

  1. 事前準備として、クラウドプロバイダーはGitHub OIDCからのリクエストに応答する設定を作成し、ここで認証情報の権限なども設定する(具体的な方法はクラウドプロバイダーに依存する)
  2. ワークフローが起動するたびに、GitHub OIDCプロバイダーはJWTを作成し、リポジトリの名前などを持たせる。これはGitHubによって署名され、クラウドプロバイダーによって検証できる
  3. JWTをクラウドプロバイダーに渡す
  4. クラウドプロバイダーはJWTを検証し、正当であればリポジトリ名などを取得して、(1)で設定した権限を持つ認証情報を生成して、GitHub Actionsに渡す

本記事ではこの流れに沿って、GCPをクラウドプロバイダーとして設定してみます。具体的な操作はGCP固有のものですが、基本的な考え方は他のクラウドプロバイダーにも通じるはずです。

GCPにGitHub OIDCへの応答を設定する

GCPでGitHub OIDCへの応答を設定する方法は、次のドキュメントに説明されています。

Setting up Workload Identity Federation - google-github-actions/auth

ここでは、この指示をほとんどそのまま実行します。Google Cloud SDKをセットアップしてgcloudコマンドを使えるようにしておいてください。

(1) 対象となるGCPプロジェクトを決める

テスト用であれば、新規で作ってもかまいません。ここではtest-gcp-github-federationというプロジェクトがあるとします。

$ export PROJECT_ID="test-gcp-github-federation"
(2) サービスアカウントを作成

すでにあるサービスアカウントを使う場合、この操作はしなくてかまいません。

$ gcloud iam service-accounts create "my-service-account" \
  --project "${PROJECT_ID}"
(3) 作成したサービスアカウントにロールで必要な権限を与える

GitHub OIDCで最終的に取得するGCPの認証情報は、このサービスアカウントに与えられた権限を持ちます。

$ gcloud projects add-iam-policy-binding  "$PROJECT_ID" \
  --role="roles/iam.roleViewer" \
  --member "serviceAccount:my-service-account@${PROJECT_ID}.iam.gserviceaccount.com"

ここでは、Role Viewerroles/iam.roleViewerを付与しています。これは、ワークフローのテストとして、gcloud projects get-iam-policyを実行するためです。

なお、これはWeb UIでも操作できます。また、GCPで必要最小限の権限をもつロールを検索するには、GCP Predefined Roles Finderが便利です。

(4) IAM Credentials APIを有効にする

次を実行します。

$ gcloud services enable iamcredentials.googleapis.com \
  --project "${PROJECT_ID}"
(5) Workload Identity Poolを作成する

次を実行します。

$ gcloud iam workload-identity-pools create "my-pool" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --display-name="Demo pool"
(6) Workload Identity Poolの名前を取得

取得した名前をシェル変数に入れておきます。

$ export WORKLOAD_IDENTITY_POOL_ID="$(gcloud iam workload-identity-pools describe "my-pool" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --format="value(name)")"
(7) Workload Identity Providerを作成

(5)で作成したPoolの中に作成します。

$ gcloud iam workload-identity-pools providers create-oidc "my-provider" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="my-pool" \
  --display-name="Demo provider" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
  --issuer-uri="https://token.actions.githubusercontent.com"```
(8) Workload Identity Providerからの認証を許可

特定のリポジトリからの要求に対して、Workload Identity Providerで認証を許可するアトリビュートを設定します。このattribute.repositoryでリポジトリをフルネームで明示的に許可しないかぎり、GCPは認証を通しません。たとえばフォークされたリポジトリはフルネームが違うの認証はされません。

$ export REPO="..." # $org/$name でrepoのフルネームを指定する
$ gcloud iam service-accounts add-iam-policy-binding "my-service-account@${PROJECT_ID}.iam.gserviceaccount.com" \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"

このとき設定した許可は、次のコマンドラインで確認できます。Web UIでは「IAM > Service Accounts > アカウント選択 > Permissions」です。

$ gcloud iam service-accounts get-iam-policy "my-service-account@${PROJECT_ID}.iam.gserviceaccount.com"
(9) Workload Identity Providerの名前を取得

GitHub Actionsのワークフローでは、サービスアカウントのメールアドレスと、次のコマンドラインで取得したWorkload Identity Providerの名前が必要です。

$ gcloud iam workload-identity-pools providers describe "my-provider" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="my-pool" \
  --format="value(name)"

ワークフローでgoogle-github-actions/auth@v0を使って認証する

GitHub Actions側では、ワークフローでgoogle-github-actions/auth@v0を使って、GitHubからJWTの発行およびGCPからの認証情報の取得を行います。

まず、GitHub OIDCを使うための権限として、permissions.id-token: writeが必要です。

なお、permissionsキーを使うとデフォルトの権限が使われなくなり、必要な権限を全て明示しなければならないため、チェックアウトのためにpermissions.contents: readも指定します。

google-github-actions/auth@v0アクションには最低限、Workload Identity Providerの名前とサービスアカウントが必要です。これらは秘匿値ではないので、ワークフローファイルに直接書いてもかまいません。

最後に、確認のためgcloud projects get-iam-policyを実行するステップも追加します。

name: GCP

on:
  push:
    branches: [ main ]
  pull_request:
  workflow_dispatch:

permissions:
  contents: 'read'
  id-token: 'write'

env:
  GCP_PROJECT_ID: 'test-gcp-github-federation'

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v3

    - id: 'auth'
      name: 'Authenticate to Google Cloud'
      uses: 'google-github-actions/auth@v0'
      with:
        workload_identity_provider: 'projects/372019545604/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
        service_account: 'my-service-account@test-gcp-github-federation.iam.gserviceaccount.com'

    - name: 'Get IAM policy'
      run: gcloud projects get-iam-policy "${GCP_PROJECT_ID}"

これをプロジェクトにコミットして、ワークフローが実行されて全てのステップが正常終了すればセットアップは成功です。このワークフローは次のURLから確認できます。

github-actions-showcase/oidc-gcp.yml at main · gfx/github-actions-showcase

なお、gcloudコマンド以外のGoogle Cloud SDKのツール、例えばbqコマンドやgsutilコマンドでは、Workflow Identity Federationによる認証ができません。GoogleCloudPlatform/gsutil#1497の開発者によるコメントによると今後サポートする予定もないようで、gcloud alpha storageなどの次期バージョンの開発版を使う必要があるとのことです。

クラウドプロバイダーがGitHub OIDCのJWTを検証する仕組み

GitHub OIDCにおいて安全性の中核を担うのは、「GitHubが生成したJWTが第三者によって改竄(かいざん)されていないかどうか、クラウドプロバイダーが検証する」という部分です。JWTが改竄されていなければ、その中のリポジトリ名を信頼できます。これが、指定されたサービスアカウントに紐付いたリポジトリ名と一致していれば、認証情報を生成してよいことになります。

この検証プロセスで必要なのは、次の2点です。

  1. GitHubだけがJWTに署名できること
  2. クラウドプロバイダーはGitHubが署名したJWTを検証できること

これはJOSEによって、RS256による署名で実現されます。RS256は、RSAを使った非対称アルゴリズムです。GitHubは秘密鍵でJWTを署名し、クラウドプロバイダーはGitHubが提供する公開鍵によって、JWTの署名を検証します。署名は誰でも検証できますが、署名できるのは秘密鍵を持つGitHubだけというわけです。

GitHubの公開鍵は、OpenID Connect Discoveryに定められた方法で提供されます。確認するには、次のようにJWT発行者(issuer)のエンドポイント/.well-known/openid-configurationから情報を取得します。

$ curl -sSf https://token.actions.githubusercontent.com/.well-known/openid-configuration | jq .jwks_uri | xargs curl -sSf | jq .

GitHub OIDCの参考情報

GitHub OIDCについては、次のWebページが参考になります。

JWTとJOSEについては、書籍『OAuth徹底入門』も参考にしました。

このGitHub OIDCのセクションは、Twitterで@nov氏と@ritou氏に概要を教えていただいたことをきっかけに執筆しました。ありがとうございました。

その他の重要な機能

この記事ではGitHub Actionsを典型的なユースケースに即して解説してきましたが、他にも使用頻度は比較的まれながら重要な機能がいくつかあります。ここで概要だけでも紹介しておきます。

Expressionsは、ワークフローの中で利用できるDSLです。jobs.<job_id>.steps[*].ifでステップを特定の条件でスキップしたりするときに使えます。また、この機能では組み込み関数もいくつか提供されており、たとえばtoJSON()関数を使ってコンテキストの値をJSONとしてログに出力すると、ワークフローの開発に不慣れなときは特に役立つでしょう。

Self-hosted runnersは、ランナーとして任意のマシンを使うための機能です。クラウドサービス上のインスタンスはもちろんのこと、ローカルマシンでさえランナーとして使えます。GitHub Actions提供のランナーよりも協力なインスタンスを使うことでCIを高速化したり、カスタムカーネルを使うなど、自由にカスタマイズしたランナーを利用できます。

終わりに - ワークフローを複雑にし過ぎないこと

この記事では、GitHub Actionsの基本的な機能を説明しました。GitHub Actionsは、CI/CDだけでなくバッチ処理の実行やGitHubリポジトリ自体の操作などに使える、非常に使い勝手のいいサービスです。

ただし、どのような機能を使うにせよ、ワークフローを複雑にし過ぎないことは非常に重要です。誰しもがGitHub Actionsのエキスパートというわけではありません。GitHub Actionsに熟達していなくてもワークフローをメンテナンスできるくらいシンプルにするべきです。

したがって、通常のビルドスクリプト内で行えることを、あえてGitHub Actionsの機能でやる必要はないと考えています。この記事で紹介したことを最低限の共通認識とし、細かいことはなるべくビルドスクリプトで行う、くらいがちょうどいいバランスだと思っています。

藤 吾郎(ふじ・ごろう) twitter @__gfx__ / github gfx

藤 吾郎さん
ソフトウェアエンジニア。株式会社ディー・エヌ・エー、クックパッド株式会社、株式会社ビットジャーニーでのソフトウェアエンジニア経験を経て、2019年よりファストリー株式会社に勤務。RubyKaigi、Shibuya.pm、YAPC::Asia、DroidKaigiなどのエンジニアコミュニティでも活動している。インターネットとプログラミングが好きで、ツールやライブラリをOSSとして多数公開している。
ブログ:Islands in the byte stream

編集:はてな編集部

*1:GitHubの歴史的には、WebhooksにあったイベントトリガーをActionsにも流用したというのが実際のところでしょう。

*2:掲載したリンク先に「When you use the repository's GITHUB_TOKEN to perform tasks, events triggered by the GITHUB_TOKEN will not create a new workflow run. This prevents you from accidentally creating recursive workflow runs.」とあります。

若手ハイキャリアのスカウト転職