GitHub Actionsのcomposite actionを使ってinternalリポジトリのファイルを配布する

クラウド基盤本部Cloud Platform部の pddg です。この前までチームだったんですが部になったらしいです。
引き続き精力的に cybozu.com のインフラ基盤の移行に取り組んでいます。

今回はKubernetesマニフェストのバリデーションのための仕組みを検討していたときに発見した、GitHub Actionsのちょっとハックっぽい、もしかしたら便利かもしれない手法について紹介したいと思います。

TL; DR

  • Internalリポジトリにcomposite actionを実装してorg/enterprise内に対して利用を許可すると、利用する側のワークフロー内ではそのInternalリポジトリがクローンされる
  • composite actionにおいて指定箇所へファイルをコピーするようなジョブを実装することで、そのorg/enterprise内であればInternalリポジトリのファイルを特別な認証なしで配布できる

背景

cybozu.com では新しいインフラ基盤としてKubernetesを採用し、Necoプロジェクトと称して精力的に整備を進めてきました。Necoプロジェクト自体は完了ということになりましたが、今もNecoチームによる開発・運用が精力的に行われています。現在は実際に複数のテナントチームが様々なサービスを展開し、運用するようになってきました。

テナントごとに分散しているマニフェスト

NecoではArgoCDを採用しており、テナントが自由にアプリケーションを展開できる仕組みが整えられています。これによりマニフェストの中央集権的なリポジトリを導入する必要が無く、各テナントチームは自分たちのアプリケーションを同期するためのマニフェストをそれぞれがそれぞれの方法で記述・運用しています。各テナントは自分たちのニーズに合ったマニフェスト管理手法を採用でき、他チームの作業に影響されることがないため、開発・運用の効率化に寄与していると考えています。

マニフェストリポジトリの関係性

このように、テナントチームのマニフェストはクラスタを管理するNecoチームのマニフェストリポジトリ(以降neco-apps*1)とは異なる独立したリポジトリで管理されています。よって、テナントチームから見たとき実際のクラスタのKubernetesのバージョンや適用されているカスタムコントローラのバージョンなどはNecoチームのリリースノートを追うことでしか把握できない状態になっていました。クラスタに実際に適用される前に実施したいCI上でのマニフェストのバリデーションにおいて、これらの情報を適切に把握しなければ正しいバリデーションが実施できないという問題が生じていました。

kubeconformによるバリデーション

Kubernetesマニフェストのバリデーションができるツールの一つにkubeconformというツールがあります。適切なスキーマ定義ファイルを用意することで、Kubernetesのカスタムリソースも含めてスキーマに沿ったパラメータが設定されているかを検証できます*2。このスキーマ定義ファイルは以下のような方法で用意することが出来るようになっています。

  • CRDs-catalog から取得する
  • CRDからスキーマ定義ファイルを生成する
    • kubeconformが提供するopenapi2jsonschema.pyを使って、CRDのyaml形式のマニフェストから、kubeconform用のスキーマ定義ファイルを生成できる

neco-appsには実際のk8sクラスタに適用されているCRDを後者の方法で変換したスキーマ定義ファイルがコミットされており、kubeconformを使ったマニフェストのバリデーションが出来るようになっています。

テナントもNecoが使っているスキーマ定義ファイルを使いたい!

各テナントチームがkubeconformによるバリデーションをCI上で行いたいと考えたとき、カスタムリソースのマニフェストが含まれていると以下のいずれかの方法をとることが考えられます。

  • CRDs-catalog からスキーマ定義ファイルを取得して利用する
    • カスタムリソースごとに詳細にバージョンを指定できないので、実際のクラスタに適用されているものと異なるかもしれない
    • Necoチームが独自のパッチを適用していたりすると異なるものになっているかもしれない
    • ここに登録されていない独自のCRDが存在するかもしれない
  • バリデーションせずスキップする
    • -ignore-missing-schemasというフラグを付けることでスキーマ定義ファイルがないリソースのバリデーションをスキップできる
    • 適用時にエラーになる可能性が残る
  • 実際のクラスタからCRDを取得して変換する
    • Necoはmeowsというコントローラを使ってGitHub Actionsのself-hosted runnerを運用している。
    • このrunnerからならCRDを取得し、スキーマ定義ファイルを生成すればクラスタの状態と一致したスキーマ定義ファイルを生成できる
    • 本番環境のクラスタ上では動作していないため、開発環境のクラスタのスキーマ定義ファイルしか生成できない
  • neco-appsをクローンして使う
    • 今適用中の定義が確実に手に入る
    • ブランチを選ぶことで、本番環境のものや開発環境のものを自由に選べる

neco-appsにコミットされている定義ファイルを参照するのが最も確実であり、この方法を取れないか検討したのですが、GitHub Actionsの仕様により一つの課題が浮かび上がってきたのでした。

internalリポジトリのclone

GitHub Enterprise Cloudのリポジトリには3種類の公開範囲があります。

  • public:orgの外にも公開される
  • internal:enterprise内のユーザであれば誰でも読み取り権限を持つ
  • private:enterprise内のユーザのうち明示的に権限が設定されたユーザのみが読み書きできる

neco-appsはこのうちinternalなリポジトリとして運用されており、同enterprise内のユーザであれば、つまりcybozuのエンジニアであれば誰でも閲覧することができます。一方で、GitHub Actions内からinternalなリポジトリをcloneする場合、secrets.GITHUB_TOKENでは権限が足りません。これはこのトークンがそのCIを実行しているリポジトリに対する権限のみに限定されているためで、internalなリポジトリをcloneするためには権限を持つユーザがPersonal Access Token(PAT)を発行する必要があります。

このように、enterprise内のユーザから見ると誰でもcloneできるのに、GitHub Actionsの中から実行するとデフォルトではcloneできないということになっています*3

トークンの作り方

GitHub Actionsでもデフォルトの secrets.GITHUB_TOKEN とはまた別のトークンを設定すればクローンできることは分かったので、その生成・管理方法について検討してみました。

  • ユーザのPATを使う
    • pros
      • すぐ実現できる
    • cons
      • ユーザの異動・休職・退職の際に作業が発生する
      • PAT(classic)は権限が広すぎる
  • ロボットアカウントのPATを使う
    • pros
      • すぐ実現できる
    • cons
      • 各テナントチームのロボットアカウントをneco-appsに招待してもらう必要がある
      • PAT(classic)は権限が広すぎる
  • GitHub Appを作る
    • Contents: Readの権限が付いたGitHub Appを作って各リポジトリにインストールする
    • pros
      • ユーザに依存しない短命なトークンを利用できる
    • cons
      • このappをインストールしたリポジトリ間は相互に参照できるようになってしまう
      • 権限範囲が別のリポジトリごとに異なるGitHub Appを作らねばならず、手間が大きい

どれも一長一短で、そもそもinternalなリポジトリとして公開しているのだからもう少し簡単に参照できるようになってほしい……
という気持ちがありしばらく見なかったことにして寝かせていました。

GitHub Actionsのカスタムアクション

リポジトリに所定の形式で記述した action.yaml を配置することで、GitHub Actions上でそのリポジトリを uses して使うことが出来るようになります。

docs.github.com

登場当初はこれはpublicなリポジトリでしか使えず、privateないしinternalなリポジトリにあるactionを配布する方法にはいくつかの工夫が必要でした*4。2022/12/14にprivateリポジトリからのactionやreusable workflowの共有に関してアップデートがあり、共有するactionを配置しているリポジトリに設定を行うことで、同一org内もしくはenterprise内のリポジトリのCIからは自由に uses できるようになりました。

github.blog

composite action(日本語では複合アクション)として作成したカスタムアクション内ではGITHUB_ACTION_PATHという環境変数*5が用意されています。この環境変数の指すパスにアクセスする事で、作成したcomposite action内からアクションの共有元のリポジトリの中身にアクセス出来ます。

おや?もしかしてcompositeアクションにしてしまうとinternalなリポジトリの中身を簡単にorg/enterprise内に配布することができるのでは?

GITHUB_ACTION_PATH には何が入っている?

GITHUB_ACTION_PATH というものがあることは分かりましたが、その実際の中身は自明ではありませんでした。実際に実験してみるのが早いと言うことで適当なリポジトリを用意し、testcomposite/action.yamlを作って以下の様に設定してみました。

name: Test composite action

runs:
  using: "composite"
  steps:
    - shell: bash
      run: |
        set -x
        ls -la "${GITHUB_ACTION_PATH}"
        ls -la "${GITHUB_ACTION_PATH}/../"
        cd "${GITHUB_ACTION_PATH}" && git log --oneline || true

使う側では以下の様に書くだけで使えます。

name: Test Composite Action
on: [push]
jobs:
  run:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: pddg/actions-playground/testcomposite@main

実行結果は以下の様になりました。

Test Composite Actionの実行ログ

  • GITHUB_ACTION_PATHaction.yaml が存在するディレクトリへのパスである
  • GITHUB_ACTION_PATH から相対パスを使って辿ることで、composite actionを提供するリポジトリ内の他のファイルにアクセス出来る
  • .git が存在しないため過去の履歴を辿ったりすることはできない

ということが言えそうです。これを踏まえて、internalなリポジトリ内のファイルを配布するアクションを作成してみます。

ファイルを配布するアクションを作る

注意点は以下の二つです。

  • GITHUB_ACTION_PATHaction.yaml を含むディレクトリへのパスであり、リポジトリのルートディレクトリではない
  • composite actionが実行される際のカレントディレクトリは、 uses している側のリポジトリのルートディレクトリである
name: Install CRD schemas
inputs:
  dest:
    description: "Destination path to install. Creates the directory if it does not exist."
    required: true
runs:
  using: "composite"
  steps:
    - shell: bash
      env:
        DEST: ${{ inputs.dest }}
      run: |
        # 相対パスでリポジトリのルートパスを指定
        # ここでは.github/actions/install-crd-schemas/action.yamlとして作ったので以下の様にする
        root_dir="${GITHUB_ACTION_PATH}/../../../"
        # 指定されたインストール先のディレクトリがなければ作る
        mkdir -p "${DEST}"
        # ファイルをコピーする
        cp -f ${root_dir}/test/crd-schemas/json-schemas/*.json "${DEST}"

書いてみると非常にシンプルですね。そしてリポジトリのSettings -> Actions -> GeneralからAccessに関する設定を開き、Accessible from repositories in the '{{ your org name }}' organizationまたはAccessible from repositories in the '{{ your company name }}' enterpriseにチェックを入れて保存すれば完了です*6

使う側では以下の様に指定します。

jobs:
  kubeconform:
    steps:
      - uses: example.com/your_repo/.github/actions/install-crd-schemas@main
        with:
          dest: /tmp/crd-schemas

パラメータでバージョンを選べるようにする

指定したブランチ、タグ、コミットの時点のファイルを指定したディレクトリへインストールできるようにはなりましたが、不便な点が一つあります。
本番環境向け、開発環境向けなどで使い分ける際に明示的にブランチの指定が必要で、ワークフローの再利用が難しいということです。uses句でのブランチの指定には変数などが使えない*7ため、どれを uses するか静的に決める必要があります。if などで分岐することはできますが、使う側での設定が必要なので各チームごとに同じような分岐処理を書くことになります。これを少し楽にするラッパーのようなactionを作ります。

name: Setup CRD schemas
inputs:
  k8s-env:
    description: 'Which environment to use for validation'
    required: false
    type: string
    # Available values are "latest", "stage", and "prod".
    default: stage
outputs:
  crd-dir:
    description: "Path to the dir that contains CRD schemas"
    value: /tmp/crd-schemas-${{ inputs.k8s-env }}
runs:
  using: composite
  steps:
    - uses: example.com/your_repo/.github/actions/install-crd-schemas@main
      if: ${{ inputs.k8s-env == 'latest' }}
      id: latest
      with:
        dest: /tmp/crd-schemas-${{ inputs.k8s-env }}
    - uses: example.com/your_repo/.github/actions/install-crd-schemas@stage
      if: ${{ inputs.k8s-env == 'stage' }}
      id: stage
      with:
        dest: /tmp/crd-schemas-${{ inputs.k8s-env }}
    - uses: example.com/your_repo/.github/actions/install-crd-schemas@prod
      if: ${{ inputs.k8s-env == 'prod' }}
      id: prod
      with:
        dest: /tmp/crd-schemas-${{ inputs.k8s-env }}

これで利用側は以下の様なreusable workflowを記述し、triggerの設定に応じてパラメータを切り替えるだけで環境を選択できます。

name: Check manifests
on:
  workflow_call:
    inputs:
      k8s-env:
        description: 'Which environment to use for validation'
        required: false
        type: string
        default: stage
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: example.com/your_repo/.github/actions/setup-crd-schemas@main
        id: setup-crd-schemas
        with:
          k8s-env: ${{ inputs.k8s-env }}
      - env:
          CRD_DIR: ${{ steps.setup-crd-schemas.outputs.crd-dir }}
        run: |
          # install kubeconform here
          wget ~~
          # run kubeconform
          kubeconform \
            -strict \
            -schema-location "${CRD_DIR}/{{.ResourceKind}}-{{.Group}}-{{.ResourceAPIVersion}}.json" \
            -schema-location default \
            ./manifests
name: Check manifests (stage)
on:
  pull_request:
    branches:
      - main
jobs:
  run:
    uses: example.com/your_repo/.github/workflows/check_manifests.yaml@main
    secrets: inherit
    with:
      neco-env: stage

prod用のブランチに対しては異なる設定を用意して切り替えます。

name: Check manifests (prod)
on:
  pull_request:
    branches:
      - prod
jobs:
  run:
    uses: example.com/your_repo/.github/workflows/check_manifests.yaml@main
    secrets: inherit
    with:
      neco-env: prod

workflow_dispatchinputstype: choice を使うことで、任意のブランチに対して任意の環境におけるバリデーションを、ユーザはプルダウンから選択するだけで実行できるようになります。

name: Check manifests (manual)
on:
  workflow_dispatch:
    inputs:
      k8s-env:
        description: 'which neco environment to use'
        required: true
        type: choice
        default: 'prod'
        options:
          - 'stage'
          - 'prod'
          - 'latest'
jobs:
  run:
    uses: example.com/your_repo/.github/workflows/check_manifests.yaml@main
    secrets: inherit
    with:
      neco-env: ${{ inputs.k8s-env }}

workflow_dispatchによってGitHubのUIから手動でジョブを実行できる

副次的に得られた効果

この方法は単純にリポジトリのcloneを許可する場合と比較して、認証周り以外にも一つ利点があると考えています。共有したいファイルをホストしている側が、リポジトリの構造について考慮しなければならない点が減っていることです。

単純にリポジトリのcloneを許可してテナントが自由にリポジトリ内のファイルを見るようにしてしまうと、そのリポジトリの管理側はディレクトリ構造を容易には変更できなくなってしまいます。利用しているチームに周知し、タイミングを合わせて変更したり、移行期間を設けて変更してもらうなどの手段が必要となります。

GitHub Actionsのインターフェースを挟むことで、各テナントはcomposite actionを通してしかリポジトリのファイルへのアクセスができません*8。リポジトリの構造の変更時にcomposite actionの操作も対応させることで、ユーザに影響を与えることなくリポジトリ構造の変更が実現できます。

もし下記のようなセキュリティに関する考慮の結果、トークンを使って個別に認証する必要があるケースでも、cloneした後に uses: ./your_repo/.github/actions/install-crd-schemas のように実行してもらってcomposite action経由でファイルを配布することで同様の効果を得られます。

セキュリティに関する考慮

Accessible from repositories in the ~~の設定を有効にすると、GitHub Actionsではorg内やenterprise内の他のinternal/privateリポジトリから実質参照し放題になってしまいます*9。internalなリポジトリの場合、そのenterprise内のinternalもしくはprivateのリポジトリから、privateなリポジトリの場合、そのenterprise内のprivateなリポジトリからアクセス可能になっています。特定のリポジトリには限定できないため、意図しないリポジトリからのアクセスも許可してしまいます。 docs.github.com

internalリポジトリはともかく、privateリポジトリでこの設定を有効化することは難しいでしょう。そのorgに自由にリポジトリを作成できる権限があれば、勝手にprivateリポジトリを作って uses すれば中身が見えてしまいます*10。また、internalなリポジトリであっても、指定した特定のファイルだけではなくリポジトリ内のファイル全体へのアクセスが可能になっていることから、ほかのリポジトリにおいてCI環境が侵害された場合などについて考慮が必要となります。重要な情報がリポジトリに含まれている場合は同様に許可できないでしょう。その場合は前述したトークンを使って配布する方法やdeploy keyにread onlyの権限を付与するなどで閲覧範囲を限定して緩和するなどの対応が考えられます。

まとめ

composite actionを作ることで、internalリポジトリにあるファイルを同一org/enterprise内の他のリポジトリへトークンなしで配布できることがわかりました。セキュリティ面の考慮が必要であり無条件で採用できるものではありませんが、便利に使える側面もあります。

Cybozuの新インフラ基盤では各テナントチームが裁量を持ち、独立して開発・運用する体制が徐々に整えられています。Cloud Platform部ではその新インフラ基盤への移行を支える仕事に興味のある方を募集中です。ぜひご応募ください。

cybozu.co.jp


*1:以前neco-appsリポジトリはpublicリポジトリでしたが、現在は管理上の都合によりprivateなorgのinternalなリポジトリになっています。

*2:https://github.com/yannh/kubeconform?tab=readme-ov-file#CustomResourceDefinition-CRD-Support

*3:これは少し不便な気がしますが、CI環境が侵害された際の影響範囲を限定しやすいという意味では良い仕様なのかも知れません。Go ModulesのようなGitの仕組みに依存したパッケージ管理の仕組みと相性が悪いのはなんとかしたいところです。

*4:caddi.tech

*5:https://docs.github.com/ja/actions/learn-github-actions/variables#default-environment-variables

*6:ここで公開範囲を適切に選択しないと意図しないリポジトリなどから参照される可能性があることに注意してください。Enterprise全体で利用可能にして良いかどうかはセキュリティに関して十分な検討が必要であると考えます。

*7:github.com

*8:厳密には GITHUB_ACTION_PATH が示すであろうパスにアクセスすれば自由に触れますが、それは自己責任でしょう。

*9:設定の説明にある"Access is allowed only from private or internal repositories"という記載が示すとおり、これらの設定を有効にしてもpublicリポジトリから参照されることはありません。

*10:privateリポジトリの存在を知らなければそういうこともできない、というのはそうかもしれませんが、それに依存したセキュリティにすべきではないでしょう。