Firebase Hostingのプレビューチャンネル数リミットにヒットした件
問題
今携わっているプロダクトの開発では、フロントエンド側を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のテックブログです。「どの車両が、どの訪問先を、どの順に、どういうルートで回ると最適か」というラストワンマイルの配車最適化サービス、Loogiaを展開しています。recruit.optimind.tech/
Discussion