🫥

Firebase Hostingのプレビューチャンネル数リミットにヒットした件

2024/04/24に公開

問題

今携わっているプロダクトの開発では、フロントエンド側をfirebase hostingに上げていて、PRごとにプレビュー用のチャンネルをCIでデプロイするようにしています。

name: Firebase Hostingのプレビューチャンネルを作成
on:
  pull_request:
    types: [ opened, reopened ]
jobs:
  build:
    # ここでビルドする
  deploy-pr:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      pull-requests: write
      checks: write
    steps:
      - name: Checkout repository (PR)
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - name: Download web build static files
        uses: actions/download-artifact@v3
        with:
          name: web-dist
          path: web/dist
      - id: auth
        name: Authenticate to Google Cloud
        uses: google-github-actions/auth@v1.1.1
        with:
        # ...
      - name: Get Google Cloud credentials from credentials file
        id: gcloud_credentails
        run: echo "credentials=$(cat ${{ steps.auth.outputs.credentials_file_path }})" >> $GITHUB_OUTPUT
      - name: Deploy to Firebase Hosting
        uses: FirebaseExtended/action-hosting-deploy@v0.7.1
        with:
          repoToken: '${{ secrets.GITHUB_TOKEN }}'
          firebaseServiceAccount: '${{ steps.gcloud_credentails.outputs.credentials }}'
          projectId: ${{ inputs.GOOGLE_PROJECT_ID }}
          expires: 30d # -> ここ指定がないとデフォルトは7日

この前に、デフォルトのプレビューチャンネルの有効期限(7日間)が短すぎたケースが出てきて、それでデプロイ時に有効期間を30日に伸ばしました。

これでハッピーかなと期待していましたが、数日前に、なんとチャンネルの数が多すぎて、channel quota reachedとのエラーとなって、PRのプレビューチャンネルができなくなってしまいました。

この問題を解決するための調査と結果をここでまとめたいと思います。

方針

ドキュメントでははっきりとチャンネル数のquotaは書かれていないが、このエラーを見ると実際存在するよ、とのことがわかります。Google Supportに問い合わせてみると、なんと50件の上限がある、との情報が得られました。

50件は開発プロジェクトによって少ない、と感じられる方もいらっしゃるかもしれません。この数字をみたときに思ったのは、同時に開くPRが多くても20件前後で、しかもdependabotのPR込みとなっていますし、50まで使うことはないではないかと思いました。

また、プレビューがいつ必要なのかというのも考えたいところです。現状はとにかくPRであれば作られているので、変更内容は全くみていません。そもそも、CIファイルとか、BE側のコードとか、テストとか、変更内容次第でプレビューいらないケースが多くあるのではないかと思いました。

それで解決の方針も割と自明となりました。つまり、

  • 変更内容に基づいてプレビューチャンネル作るかを判断する
  • dependabotのPRにプレビューチャンネルを作らない(根本的に変更内容package.jsonとかに基づいて判断することとも言える)
  • PRがクローズまたはマージされたらチャンネルを削除する

解決

dependabotの判定

これは公式にも記載されているが、シンプルにactorで判断できます。

if: ${{ github.actor != 'dependabot[bot]' }}

変更内容の判定

しかし、dependabotはともかく、「変更内容」というのはどうやってわかるのだろうか。

答えは、 labeler でした。要するに、PR作られるときに、ファイル変更のパスのパターンをマッチングしてPRにラベルを貼ってくれます。そのラベルを利用すれば、デプロイすべきかどうかは判断できるようになるでしょう。

# .github/labeler.yml
scope/web: # ラベル名
- changed-files:
  - any-glob-to-any-file: 'web/**' # -> webのフォルダーにファイル変更があれば`scope/web`のラベルをはる

それで他のワークフローでは、次の条件判定を入れれば良い。

      - id: pr_labels
        uses: actions/labeler@v5
      - name: Firebase Hostingへデプロイ
        if: ${{ github.actor != 'dependabot[bot]' && contains(steps.pr_labels.outputs.all-labels, 'scope/web') }}

ただ、一つ注意したいのは、今回のユースケースで、PRをクローズするときにも、該当PRにはプレビュー用のチャンネルがあるかどうかの判定が必要で、それも同じくラベルを利用したいですが、labelerはどうもクローズされたPRを追従することができないようです。クローズ時にlabelerを使うと、Could not find pull request <number>, skippingとのワーニングが出て、ワークフローがそのタイミングで終了になります。

これは公式のドキュメントには対策が書いていなかったので、削除時だけ別途 既存のラベル取得専用のアクション を導入しました。

      - id: pr_labels
        uses: snnaplab/get-labels-action@v1
      - name: チャンネルを削除
        if: ${{ github.actor != 'dependabot[bot]' && contains(fromJSON(env.LABELS), 'scope/web')}}
        # もしくは
        # if: ${{ github.actor != 'dependabot[bot]' && contains(steps.pr_labels.outputs.labels, 'scope/web')}}

PRオープン時にチャンネルデプロイする条件

以上の2つをまとめると、まずはPRデプロイのワークフローを修正します。

      - id: pr_labels
        uses: actions/labeler@v5
      - name: Deploy to Firebase Hosting
        if: ${{ github.actor != 'dependabot[bot]' && contains(steps.pr_labels.outputs.all-labels, 'scope/web') }}
        uses: FirebaseExtended/action-hosting-deploy@v0.7.1
        with:
          repoToken: '${{ secrets.GITHUB_TOKEN }}'
          firebaseServiceAccount: '${{ steps.gcloud_credentails.outputs.credentials }}'
          projectId: ${{ inputs.GOOGLE_PROJECT_ID }}
          channelId: 'pr-${{ github.event.pull_request.number }}' # -> デフォルトのではブランチ名の一部も入るが、処理が面倒なので番号だけで十分
          expires: 30d

channelIdは本来指定しなかったのですが、デフォルトでは、ブランチ名入りで最大20文字かつ、無効な文字を入れ替えるとかの処理が行われていいます( こちら )。ここはもしでデフォルトにすると、削除するときも同じくブランチ名入り+無効な文字列の入れ替えが必要になってしまうため、ここはpr-<number>のとシンプルな形にしました。

PRがクローズ時にチャンネルを削除する

次にチャンネル削除用のワークフローを作成します。チャンネル管理のドキュメントは こちら に参照してください。

name: Firebase Hostingのプレビューチャンネルを削除 (PRクローズ時)
on:
  pull_request:
    types: [ closed ]
jobs:
  delete-channel:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write
      pull-requests: write
    steps:
      - name: チェックアウト
        uses: actions/checkout@v4
        with:
          ref: ${{ github.event.pull_request.head.sha }}
      - id: auth
        name: Google Cloudに認証
        uses: google-github-actions/auth@v1.1.1
        with:
        # ...
      - name: 生成されたクレデンシャルファイルからGoogle Cloudのクレデンシャルをoutputに取得
        id: gcloud_credentails
        run: echo "credentials=$(cat ${{ steps.auth.outputs.credentials_file_path }})" >> $GITHUB_OUTPUT
      - uses: snnaplab/get-labels-action@v1
      - name: チャンネルを削除
        if: ${{ github.actor != 'dependabot[bot]' && contains(fromJSON(env.LABELS), 'scope/web')}}
        uses: w9jds/firebase-action@master
        env:
          PR_NUMBER: '${{ github.event.pull_request.number }}'
          GCP_SA_KEY: '${{ steps.gcloud_credentails.outputs.credentials }}'
          CHANNEL_ID: 'pr-${{ github.event.pull_request.number }}'
        with:
          args: hosting:channel:delete $CHANNEL_ID -f

get-labels-actionには、pull-requests: writeの権限が必要なので、そこがないとResource not accessible by integrationとかのエラーになります。

また、firebase hosting:channel:delete $CHANNEL_ID -fを実行時に、-fをつけておかないと、Y/Nの回答が必要になるので、CI上は必ずつけておきましょう。ちなみにチャンネルがもし存在しい場合は404エラーで失敗するようになります。

終わりに

今回はfirebase hostingのPRのプレビュー用のチャンネルについて、quotaのリミットにヒットしないように、自分たちのユースケースに踏まえてより実用的な改修を行ってみました。

PRプレビューは嬉しい機能ですが、一概に全てのPRにつけるのはやはり避けたいですね。ラベルの利用、ワークフロー実行者の判定、不要なときにプレビューチャンネルを削除とかの施策で一旦問題が解決できました。

同じ問題にあった方のご参考になれると嬉しいです。

また、labelerの運用については、うちの チームリーダーの記事 にもぜひご参考ください。

ではでは。

OPTIMINDテックブログ

Discussion