GitHub Actions で private リポジトリの action を共有する仕組み

Platformチームの前多( @kencharos )です。
私たちのプロダクトはGCP上に構築されていて、アプリケーションのテストやビルド、デプロイに加えて、 GCPのインフラ構築もほとんどがIaC化されています。
Pull Requestによるコードレビューを経て、CI/CD パイプラインによりアプリケーションのデプロイやGCPリソースの構築が自動化されています。

CI/CDパイプラインは、以前はCircleCIに注力していましたが、現在はGitHub Actionsに注力しています。
新規サービスの開発ではGitHub Actions を最初から使用し、既存サービスでも徐々にCircleCIからGitHub Actionsへの移行を進めています。

GitHub Actionsへの移行理由としては以下の2点があります。

  1. GitHubを使用しているため、CI/CDが統合されていると使い勝手が良い
  2. OIDCに対応しているため、GCPのWorkload Identityを使うことによってGCP Service AccountのKey fileをCI/CD側に保存しなくてもGCPリソースを扱うことができる

特に 2 のService Account Key fileをCI/CD側に保存する必要がなくなるのは重要でした。
CADDiではGCPプロジェクトの数が多く、複数のKey fileを扱うのはメンテナンスコストや漏洩リスクが高かったためです。
なお、現在ではCircleCIもOIDCに対応していますが、検討開始当初ではGitHub Actionsのみが対応していました。

actionを共通化するための課題

GitHub Actionsでは 再利用可能な処理をactionとして定義して、workflowから呼び出すことができます。

例えば、次のような .github/actions/sample/action.yaml があるとします。

#.github/actions/sample/action.yaml
name: sample
description: sample
runs:
  using: "composite"
  steps:
    - name: echo
      id: echo
      shell: bash
      run: |
        echo "sample"

同一リポジトリ内のactionは次のように uses を使ってworkflowから呼び出すことができます。

#.github/workflows/sample_workflow.yaml
name: workflow sample
on:
  pull_request:
jobs:
  sample_job:
    runs-on: ubuntu-latest
    steps:
        # 同一リポジトリの場合 ./ を先頭につける
      - uses: ./.github/actions/sample

このようにaction 共通処理をまとめることができると便利です。
CircleCIでも私たちはOrbという仕組みを使って共通処理をまとめていました。

しかし、検討を進めていくと、別のprivate actionにある共通actionを利用できないという問題に気づきました。

Github Actionsのusesの説明 を見ると、 uses に指定できるのは次のものだけでした。

  • publicなGitHubリポジトリにあるaction
  • 同一リポジトリ内のaction
  • publicなdockerイメージ

私たちはサービスごとにGitHubのリポジトリを分けていてその数は20を超えています。
それぞれのリポジトリの中にはGCP SDK の初期化、terraformの実行などの共通処理が多数ありますので、これらの処理を共通actionとして別リポジトリに管理できないと、コピペの嵐となってしまいます。
しかし、これらの処理をpublicリポジトリに置くこともできません。

private リポジトリを checkout するアイデア

同様の悩みを抱えている人は多数いるようで、その中で出てきていたのは別privateリポジトリをcheckoutして使うというものでした。

#.github/workflows/sample_workflow.yaml
name: PAT checkout sample
on:
  pull_request:
jobs:
  sample_job:
    runs-on: ubuntu-latest
    steps:
    # PAT で sampleorg/private-actions の内容を .github/actions/common にチェックアウト
    - id: checkout-private-repo-by-PAT
      uses: actions/checkout@v3
      with:
        repository: sampleorg/private-actions
        path: ./.github/actions/common
        ref: "main"
        token: ${{ secrets.CHECKOUT_PAT }}
    # チェックアウトした action を使う
    - uses: ./.github/actions/common/sample

actions/checkout actionは別リポジトリの内容をパスを変えてチェックアウトできます。
チェックアウトした後であれば同一リポジトリ内のファイルと見做せるので、
uses: ./.github/actions/common/sample のようにして使うことができます。

ですが、別のリポジトリをチェックアウトするにはデフォルトの GITHUB_TOKEN では権限が足りないため、
PAT(Personal Access Token)を使う必要があります。

PAT は個人に紐づくトークンですし期限もあるため、退職リスクや期限切れによって突然CI/CDが動かなるリスクもあるので、
組織活動としてなるべく使いたくありません。

そこで考えたのがGitHub Appsを使うというものです。

GitHub Apps

GitHub Apps は、 アプリケーションが適切な権限でGitHub APIにアクセスする仕組みを提供するものです。
GitHub Apps であれば、Organization 内のリポジトリ単位でアクセスを制御できますし、チェックアウトに利用するアクセストークンを安全に発行できます。

一方で、GitHub Appsを使ってアクセストークンを発行するのはそこそこ複雑です。

詳しくは公式ドキュメントなどを見ていただきたいですが、簡単に説明すると以下の作業が必要です。

事前準備として、GitHub Appsを作成して秘密鍵を取得した後、GitHub Appsを organization にインストールしておきます。

アクセストークンを取得するには次の手順を実施します。

  • GitHub Appsの秘密鍵からJWTを生成する
  • installations APIをJWTを使用して呼び出し、GitHub AppsがインストールされているOrganization一覧を取得し、アクセス対象のOrganization の ID (installation ID) を特定する
  • installation IDを使用して installations/:installation_id/access_tokens エンドポイントを呼び出してアクセストークンを取得する
  • アクセストークンを使用してリポジトリをチェックアウトする

このようにPATを使用する場合と比べてGitHub Appsを使う手順は複雑です。
そこで、上記のGithub Appsを使ってprivateリポジトリをチェックアウトするpublic actionを作成しました。

checkout-private-action

https://github.com/caddijp/checkout-private-action
は、前述のGitHub Appsを使って アクセストークンを発行して、privateリポジトリをチェックアウトするactionです。
このactionだけはpublic actionとして定義します。

次のように使用します。

name: sample workflow
on:
  pull_request
jobs:
  sample-job:
    name: Sample job
    runs-on: ubuntu-latest
    steps:
      # リポジトリのチェックアウト
      - uses: actions/checkout@v3
      # Github appsを使用して caddijp/common-github-actions private リポジトリを .github/actions/common にチェックアウト
      - uses: caddijp/checkout-private-action@v0.1.0
        with:
          app_id: {{secrets.CHECKOUT_PRIVATE_ACTION_APPS_ID}}
          secret_key:{{secrets.CHECKOUT_PRIVATE_ACTION_APPS_SECRET}}
          org: "caddijp"
          repo: "common-github-actions"
          ref: "main" 
          dist: "./.github/actions/common"
      # チェックアウトした action を呼び出す
      - uses: ./.github/actions/common/common_action

事前にGitHub Appsを作成して、GitHub AppsのIDと秘密鍵をsecretに入れておく必要があります。
これで、別リポジトリにある共通actionを各repositoryで使うことができるようになりました。

具体的な処理内容を知りたい方は https://github.com/caddijp/checkout-private-action を見てみてください。
これまでに説明した内容の処理が書かれています。

共通actionを使用する際の注意点

action.yaml の内部にも uses を使用して、別のactionを呼び出すことができます。
そのため、action の中からさらに別の共通actionを呼び出したいと思うかもしれませんが、
これには注意が必要です。

Github Actionsのusesの説明 で説明したとおり、同じリポジトリ内のaction指定は、uses: ./.github/actions/sample のようにリポジトリのルートからの指定しかできないためです。
相対パスや、自分のactionのパスを示す変数である ${{ github.action_path }} などを uses 節で使うことはできません。

そのため、actionから別の共通actionを呼び出すには、必ずチェックアウトするパスを固定しておく必要があります。
この制約を満たすことが難しい場合は、そもそもactionから共通actionを呼ぶような構成を見直して、workflow側で制御すると良いでしょう。

まとめ

CircleCIからの移行を検討したときに Orb 相当の機能がなくて困りましたが、GitHub Appsを使う工夫をすることで対応できました。
将来 GitHub Actionsの機能追加で、privateリポジトリのactionも利用可能になると嬉しいですが、それが待てないという方は参考にしてください。

https://github.com/caddijp/checkout-private-action は図らずともCADDi初のOSSライブラリとなりました。
今後、違う形で OSS のライブラリが出せると個人的には嬉しいなと思っています。

私たちは他にも CI/CDパイプラインの品質を高く保つための工夫を取り入れています。
Workload Identityによるシークレットの排除やRenovateによる依存ライブラリの定期更新など様々な取り組みをおこなっています。
これらの取り組みもまた別の記事で紹介できればと思っています。
興味がある方はぜひ、採用ページをご覧ください。