GitHub Actions上からFirebaseに接続したい
GitHub Actions上でテストを回している時など、GitHub Actions上からFirebaseに接続したい時があります。
このような場合、サービスアカウント用の秘密鍵が入ったJSONファイルを生成して使用するのが一般的だと思います。JSONファイルは以下のような形式です。
{
"type": "service_account",
"project_id": "...",
"private_key_id": "...",
"private_key": "-----BEGIN PRIVATE KEY-----\n<private key>\n-----END PRIVATE KEY-----\n",
"client_email": "...",
"client_id": "...",
...
}
このJSONファイルですが、秘密鍵が含まれているためgitで管理することはできません。そのため、GitHub Actionsのランナー上でFirebaseの秘密鍵を使うには一工夫必要になります。例えば、以下のような方法があります。
- GitHub Actionsの
secrets
に認証情報を設定する - gcloud CLIからFirebaseの認証情報を生成する
方法1:GitHub Actionsのsecrets
に認証情報を設定する
主に以下のサイトで解説されているような方法を使って、認証情報をGitHub Actionsのsecretsに設定します。
まずFirebaseコンソールの[プロジェクトの設定]からサービスアカウントの秘密鍵をダウンロードします。
次にfirebase-admin-sdkのinitializeApp
を呼んでいる所を以下のように書き換えます。
import { cert, initializeApp } from "firebase-admin/app";
initializeApp({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"),
}),
});
最後に、GitHub Actions上で環境変数を設定します。
[Settings] > [Secrets and variables] > [Actions]にある[New repository secret]から設定できます。
設定するのは環境変数として必要なFIREBASE_PROJECT_ID
, FIREBASE_CLIENT_EMAIL
, FIREBASE_PRIVATE_KEY
です。これらのsecretに指定する値は、先ほどダウンロードしたJSONファイルの中で指定されているものと同じ値です。
(ローカル実行時にも.env
ファイルなどを用いて同じように環境変数を指定します。)
GitHub Actionsのworkflow内で、secretを環境変数として渡してあげれば動きます。
# テストを実行
- name: Test
run: npm run test
env:
FIREBASE_PROJECT_ID: ${{ secrets.FIREBASE_PROJECT_ID }}
FIREBASE_CLIENT_EMAIL: ${{ secrets.FIREBASE_CLIENT_EMAIL }}
FIREBASE_PRIVATE_KEY: ${{ secrets.FIREBASE_PRIVATE_KEY }}
以上が環境変数を使用してFirebaseに認証情報を渡す方法です。
上記ではFIREBASE_PROJECT_ID
やFIREBASE_CLIENT_EMAIL
などを別々に環境変数に入れましたが、上で紹介したサイトの中には、JSONファイルを丸ごとbase64エンコードしてsecretに格納している方もおられました。
方法2:gcloud CLIからFirebaseの認証情報を生成する
通常は方法1で説明したやり方でよさそうですが、秘密鍵を自分でダウンロードして管理するのは若干面倒です。筆者はそういうのをどこに設定したかすぐ忘れるタイプです。
既にGitHub Actions内でgcloud cliを使用してGoogle Cloudにログインしている場合、その認証情報を使ってGitHub ActionsからFirebaseに接続することができます。
# gcloudにログイン -> ここで取得した認証情報を使ってFirebaseに接続したい
- name: Login with gcloud
uses: google-github-actions/auth@v1
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
流れとしては、
- gcloud CLIで一時的な秘密鍵を生成
- 秘密鍵を使用してFirebaseに接続し、テストを実行
- 最後に秘密鍵を無効化(削除)
という形になります。
まずgcloud cliでFirebaseの接続に必要な秘密鍵を生成します。gcloud iam service-accounts keys create
コマンドを使用します。
指定したパス(ここでは./firebase-secret.json
)にFirebaseの認証で使うJSONファイルが出力されます。
- name: Create Firebase Credeitial JSON
run: |
gcloud iam service-accounts keys create ./firebase-secret.json --iam-account=<iam account>
cat ./firebase-secret.json | jq --raw-output '.private_key' | sed 's/^ */::add-mask::/'
ここで、取得する秘密鍵がGitHub Actionsの出力に載らないようにするため、GitHub Actionsのコマンドである::add-mask::
を使用してマスクしています。(run
の2行目)
秘密鍵は複数行なので、こちらのstackoverflowで紹介されている方法を参考にsedで置換してマスクしています。
この設定を行うことで、秘密鍵をecho
などで出力しても*****
のようにマスクした状態で出力してくれます。
テスト実行ステップでは、GOOGLE_APPLICATION_CREDENTIALS
という環境変数に、先ほど作成したJSONのパスを設定します。
# テストを実行
- name: Test
run: npm run test
env:
GOOGLE_APPLICATION_CREDENTIALS: ./firebase-secret.json
次にソース側の設定です。
admin-sdkは、GOOGLE_APPLICATION_CREDENTIALS
環境変数を設定しておくと、initializeApp()
が勝手にそのパスを見に行って認証情報を読み込んでくれます。そのため引数には何も指定する必要がありません。
import { initializeApp } from "firebase-admin/app";
initializeApp(); // GOOGLE_APPLICATION_CREDENTIALSを自動で読み込むため、引数には何も指定する必要が無い!
ローカル開発時も同様に、.env
ファイルなどを使ってGOOGLE_APPLICATION_CREDENTIALS
に秘密鍵入りのJSONファイルへのパスを設定しておけばよいです。
最後に、作成した秘密鍵を無効化します。if: ${{ always() }}
で、テストが成功したか失敗したかにかかわらず秘密鍵の削除を実行するのがポイントです。
# firebase接続用の秘密鍵を削除する
- name: Delete Firebase Credeitial JSON
if: ${{ always() }}
run: gcloud iam service-accounts keys delete $(cat ./firebase-secret.json | jq --raw-output '.private_key_id') --iam-account=<iam account>
最終的なGitHub Actionsのworkflowファイルは以下のようになります。
# gcloudにログイン -> ここで取得した認証情報を使ってFirebaseに接続したい
- name: Login with gcloud
uses: google-github-actions/auth@v1
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
# Firebaseログインに使用する秘密鍵入りのJSONを生成
- name: Create Firebase Credeitial JSON
run: |
gcloud iam service-accounts keys create ./firebase-secret.json --iam-account=<iam account>
cat ./firebase-secret.json | jq --raw-output '.private_key' | sed 's/^ */::add-mask::/'
# テストを実行
- name: Test
run: npm run test
env:
GOOGLE_APPLICATION_CREDENTIALS: ./firebase-secret.json
# firebase接続用の秘密鍵を削除する
- name: Delete Firebase Credeitial JSON
if: ${{ always() }}
run: gcloud iam service-accounts keys delete $(cat ./firebase-secret.json | jq --raw-output '.private_key_id') --iam-account=<iam account>
以上がgcloud CLIからFirebaseの秘密鍵を生成する方法となります。
「テストが実行されている間のみ有効な、一時的な秘密鍵」を使用する事で、万が一秘密鍵が流出しても安心ですし、管理も簡単になります。
Note: デフォルトで生成されるGOOGLE_APPLICATION_CREDENTIALS
は(まだ)使えない
実は、最初にgcloudへのログインで使用したgoogle-github-actions/auth@v1
のドキュメントを見ると、デフォルトでGOOGLE_APPLICATION_CREDENTIALS
が出力されると書かれています。これをそのままFirebaseで使えば良さそうに見えます。
しかし、google-github-actions/auth@v1
がら出力されるGOOGLE_APPLICATION_CREDENTIALS
は、そのままでは使うことができません。詳細は以下のissueに記載されています。
- https://github.com/firebase/firebase-admin-node/issues/1703
- https://github.com/firebase/firebase-admin-node/issues/1861
- https://github.com/firebase/firebase-admin-node/issues/1377
どうやら、GOOGLE_APPLICATION_CREDENTIALS
で指定されるJSONファイルには、typeが「service_account
」になっているものと「external_account
」になっているものの2種類あるようです。
google-github-actions/auth@v1
が出力するものはtypeが「external_account
」になっているのに対して、firebase-admin-sdkは「service_account
」にしか現時点では対応していないため、エラーが発生するようです。
そのため、上で紹介した方法ではgcloud iam service-accounts keys create
コマンドで秘密鍵を新たに発行することでエラーを回避しています。
まとめ
- GitHub Actions上で
firebase-admin-sdk
を使用してinitializeApp
するには以下のような方法がある- 方法1:GitHub Actionsの
secret
と環境変数を使用して認証情報を渡す- メリット:環境変数を設定するだけで済む
- デメリット:環境変数を設定する必要がある
- 方法2:gcloud CLIを使用して認証情報を設定する
- メリット:既にGitHub Actions上でgcloud cliを使用している場合はこちらの方が設定項目が少なく楽
- デメリット:Google CloudのIAMで対象のサービスアカウントに適切な権限を付与しないとエラーが出ることがある
- 方法1:GitHub Actionsの
どちらを使うかは、本番環境にデプロイした時にどのように秘密鍵を読み込むかで使い分けるとよいと思います。
例えばvercelにデプロイする時は秘密鍵などの情報は環境変数に設定すると思います。その場合前者のやり方を使うことで、本番環境とテスト時に同じ方法でinitializeApp()
できます。
import { cert, initializeApp } from "firebase-admin/app";
// 本番環境のデプロイ先にもGitHub Actionsにも環境変数を設定しておく。
initializeApp({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"),
}),
});
逆にCloud Runにデプロイしている場合、本番環境では環境変数などを設定しなくてもデフォルトのFirebaseに接続してくれるようになっています。
そのため、initializeApp()
の引数を空にできる後者の方法を取ることで、本番環境とテスト時において共通の方法でinitializeApp()
できます。
import { initializeApp } from "firebase-admin/app";
// Cloud Runでは何も設定しなくても動く。GitHub Actions上ではGOOGLE_APPLICATION_CREDENTIALSを設定すると動く。
initializeApp();