🏢

dockerでcloud functions for firebase環境構築 + Cloud BuildでCI/CD

2020/12/21に公開

はじめに

Cloud Functions for firebaseは2020/12/21現在Node12までしかサポートしていません。npmを使う場合はpackage.jsonのenginesをnode:12に指定することでローカルでも使えたと思いますが、yarnを使う場合はNode12を実際に入れなければなりません。
自分はローカルに古いNode.jsは入れたくなかったので、今回はcloud functions for firebaseの開発環境をDockerで構築し、ついでにCI/CDパイプラインをCloud Buildで構築します。

サンプル

サンプルはここに置いてあります。

プロジェクト構成

firebaseのプロジェクト構成は実に多様な選択肢があると思いますが、一般的な開発、検証、本番環境のような構成ではだいたい以下のプロジェクトが存在すると思います。

環境 管理者
開発 各エンジニア
オンラインテスト[1] 各エンジニア
検証 組織
本番 組織
CI 組織

つまり各開発者は自分専用の開発プロジェクトとオンラインテスト用のプロジェクトを管理し、そこでユニットテストや開発を行っていくことになります。

一方でstagingやproduction、CIの環境は組織によって管理されていて、権限を持った人でないとアクセスできないという前提とします。(CI/CDの部分はこの権限がないとできないです)

上の構成を踏まえるとローカルの方では開発プロジェクトにデプロイができることと、オンラインユニットテストを走らせた際にオンラインテスト用のプロジェクトに自動でつなぎに行く必要があります。

イメージとしては

  • firebase deployをすると開発プロジェクトに行く
  • yarn testをするとオンラインテストプロジェクトを使用する

ローカル開発環境の構築

Dockerfileはfirebase initで作成されたプロジェクトコードのルートに置きます。(firebasercとかある階層)

ベースイメージにはNode12:alpineを使います。

FROM node:12-alpine

build argとして以下のものを用意します

  • firebase_token -> 開発プロジェクトにデプロイする際に使用するトークン
  • ci_project_id -> オンラインテストプロジェクトのproject id
  • dev_project_id -> 開発プロジェクトのproject id

firebase_tokenとci_project_idはそのままそれぞれ環境変数FIREBASE_TOKENGCLOUD_PROJECTとしてセットします。
GCLOUD_PROJECTはユニットテスト内部のメソッドから参照される変数なのでここでセットしておかないとテストが失敗します。
dev_project_idも後ほどfirebase useコマンドに引数として与えるので指定しておきます。
最後にGOOGLE_APPLICATION_CREDENTIALSですが、これはユニットテスト内部から参照されるサービスアカウントキーファイルへのパスです。後で説明しますが、オンラインテストプロジェクトのサービスアカウントキーを発行してこのパスに置いておくとユニットテストの接続先を自動でオンラインテストプロジェクトにしてくれます

ARG firebase_token
ARG ci_project_id
ARG dev_project_id

ENV FIREBASE_TOKEN=${firebase_token}
ENV GCLOUD_PROJECT=${ci_project_id}
ENV GOOGLE_APPLICATION_CREDENTIALS=/app/functions/service-account-key.json

次にfirebase-toolsのグローバルインストールとfirebase.jsonファイルのコピーをします。/appをプロジェクトルートとして、そこにfirebase.jsonをコピーし、プリインストールされているyarnを使ってfirebase-toolsをインストールします。
その後firebase useコマンドでfirebaseコマンドの対象を開発プロジェクトに指定します。

WORKDIR /app

COPY ["firebase.json", "./"]

RUN yarn global add firebase-tools

RUN firebase use ${dev_project_id}

最後にyarn installとソースコードのコピーをします。初回はyarn.lockが生成されていないのでここをスキップした上で一度コンテナをバインドして起動するところまでやってからinstallをしてファイルを生成してください

WORKDIR /app/functions

COPY ["functions/package.json", "functions/yarn.lock", "./"]

RUN yarn install

COPY functions .

CMD ["/bin/ash"]

最終的なファイルはこんな感じです。

FROM node:12-alpine

ARG firebase_token
ARG ci_project_id
ARG dev_project_id

ENV FIREBASE_TOKEN=${firebase_token}
ENV GCLOUD_PROJECT=${ci_project_id}
ENV GOOGLE_APPLICATION_CREDENTIALS=/app/functions/service-account-key.json

WORKDIR /app

COPY ["firebase.json", "./"]

RUN yarn global add firebase-tools

RUN firebase use ${dev_project_id}

WORKDIR /app/functions

COPY ["functions/package.json", "functions/yarn.lock", "./"]

RUN yarn install

COPY functions .

CMD ["/bin/ash"]

サービスアカウントキーの発行

GCPコンソールでオンラインテストプロジェクトを選択した上で、[IAMと管理]->[サービスアカウント]に行くと、デフォルトで作成されたApp Engineのサービスアカウントがあると思います。こちらのキーを発行してください(このキーは一度しかダウンロードできないので注意してください

これをfunctionsディレクトリ内にservice-account-key.jsonとして置いておけばテスト時にオンラインテストプロジェクトに接続してくれます

使い方

各エンジニアには自分専用の開発、オンラインテストのプロジェクトを作成してもらった上で以下のコマンドでイメージをビルドできます

docker build --build-arg firebase_token=FIREBASE_TOKEN --build-arg ci_project_id=CI_PROJECT_ID --build-arg dev_project_id=DEV_PROJECT_ID .

ビルドできたらコンテナを起動して中に入りましょう。上のDockerfileをそのまま使っていればコンテナ起動時の階層でyarn testやfirebase deployを使用できます。(-vは自分のソースディレクトリとコンテナ内の/appディレクトリをバインドしています)

docker run -it -v ${PWD}:/app IMAGE_NAME:TAG

これでローカル開発環境は構築できました。

Cloud Build概要

Cloud BuildはGCPのフルマネージドCI/CDプラットフォームです。このサービスを使うことの利点は一日120分というかなり太っ腹な無料枠が利用できることと、Cloud IAMによって与えられた権限を使って各種オペレーションを実行できることです。GCP内のサービスであればCloud BuildのサービスアカウントのIAMロールを変更するだけで認証をスキップできます。

ただ先程も述べたようにcloud functionsのオンラインテストにはサービスアカウントキーが必要なので今回は少し面倒ですが、他のfirestore security rulesやhostingのデプロイはかなり単純化できます。

ドキュメント

Cloud Buildのドキュメントを見ると、Firebase community builder imageなるリポジトリが紹介されています。軽く見た感じだとこれはどうやらfirebaseコマンドをあらかじめ使えるようにしておいたdockerイメージのようです。

ただソースコードを読むとパッケージマネージャにnpmを使っているようです。自分はyarnを使用しているので今回は自分でイメージを組みます。firebaseのCIコンテナイメージはそんなに複雑ではないので自作できる範囲です。

この作業の流れ

以下の手順でパイプラインの構築を進めていきます。

  1. API有効化やIAMの設定などの下準備
  2. ベースイメージの作成とGCRへのpush
  3. サービスアカウントキーの設置
  4. ワークフローの定義
  5. Github連携

下準備

APIを有効にする

この作業にはCloud BuildFirebase ManagementCloud Resource ManagerのAPIを有効にしておく必要があります。

各プロジェクトのAPIライブラリから有効にしておきましょう。

IAMの設定

IAMはデフォルトで発行されているCloud Buildのサービスアカウントに対して適用します。
ドキュメントではFirebase管理者権限とAPI Key管理者権限が必要とされていますが、今回はcloud functionsのみなのでfirebaseの方はdevelopの管理者権限を取得します。

またドキュメントに記載されていないですが、サービスアカウントユーザーという権限も必要になります。これがないとオペレーションに対してサービスアカウントとして振る舞うことができず、認証を要する各種firebaseオペレーションが失敗します

最終的なロールは以下のとおりです。

  • [Cloud Build サービス アカウント]
  • [Firebase Develop管理者]
  • [サービスアカウントユーザー]

ベースイメージの作成とGCRへのpush

Node12、yarn、firebase-toolsが入ったシンプルなイメージを作成し、GCRに置いておきます。

アプリのソースコード内に適当なフォルダを作成し、Dockerfileを以下のように作成します。

FROM node:12-alpine

RUN yarn global add firebase-tools

ENTRYPOINT ["/bin/ash", "-c"]

pushはタグを付けてローカルからコマンドで直接行う方法もありますが、今回はCloud Buildを通してイメージのビルドをします。同じ階層にcloudbuild.yamlファイルを作成します
はじめにGCRからdockerコマンドを実行可能なコンテナをpullし、同じ階層にあるDockerfileを使ってGCR用のタグを付けてイメージをビルドしています。imagesに指定することによってGCRにpushしています。

steps:
  - name: "gcr.io/cloud-builders/docker"
    args: ["build", "-t", "gcr.io/$PROJECT_ID/firebase:node12", "."]
images:
  - "gcr.io/$PROJECT_ID/firebase:node12"
tags:
  - firebase
  - node12

これらのファイルを作成したらこのファイルがある階層に移動して以下のコマンドを実行してみてください

gcloud builds submit

完了後Container Registryに作成したコンテナが入っていたら成功です。これでベースイメージは作成できました。

サービスアカウントキーの設置

上で述べたようにオンラインテストのためにはサービスアカウントキーが必要になってしまうのでCI用のプロジェクトのサービスアカウントキーを発行し、コンテナ内にファイルを設置する設定を行います。

キーの発行

キーの発行はCI用のプロジェクトを選択した上で上と同じように行います。

Secret Managerに保存

キーをGCPのSecret Managerに保存します。

まずプロジェクト内でSecret Managerを有効にする必要があります。APIライブラリから有効にしておきます。

次にIAMのコンソールに行って、Cloud Buildのサービスアカウントに[シークレットマネジメントのシークレットアクセサー]権限を付与します。

最後にシークレットマネージャーのコンソールに行き、[シークレットの作成]を押します。シークレットマネージャーはファイルを直接アップロードできるのでbase64エンコードなどの手間が必要なく便利です。先程取得したkeyファイルをここでアップロードしておきます。

ワークフローの定義

Cloud BuildはCircle CIやGithub Actionsと少し違い、一つのコンテナ内で必要なものをインストールしてソースコードをコピーして何かをする、というようなワークフローを定義しません。
基本的には各ステップごとに別々のコンテナが用意され、OSや環境変数を共有しないステップの集合としてワークフローを定義していきます。各ステップで使用するイメージは多くがコマンド実行のためにカスタマイズされたイメージです。
ただし、Cloud Buildはプロセス開始時に/workspaceにソースディレクトリをコピーしますが、この作業領域にあるファイルは各ステップ間で共有されます。つまり、yarn installなどで入れたnode_modulesは他のステップでも参照可能です。

これを踏まえてワークフローを定義していきます。

CIワークフロー

まずはdevelopなどの開発安定ブランチにpull requestが来たときに走るワークフローを定義していきます。ここでの目的は静的解析と自動テストを実行することです。

firebase.jsonなどと同じ階層にcloudbuild.ci.yamlを作成します。

ローカルでのDockerイメージビルドと違ってソースコードはすでに作業空間にコピーされている状態でプロセスがスタートします。

まずはgcloud secrets versions access latestでSecret Managerから最新のサービスアカウントキーファイルを取得します。SECRETの部分を自分がつけた名前に置き換えるのを忘れないでください。取得したJSONファイルはそのままservice-account-key.jsonとして書き込みます。
この作業はfunctionsディレクトリ内で行うのでdir:functionsとしておきます。
entrypointを指定している理由はgcr.io/google.com/cloudsdktool/cloud-sdkgcloudコマンド実行用のイメージであり、Linuxコマンドを普通には実行できないからです。

- name: gcr.io/google.com/cloudsdktool/cloud-sdk
  dir: functions
  entrypoint: /bin/bash
  args:
    [
      "-c",
      "gcloud secrets versions access latest --secret=SECRET_NAME > service-account-key.json"
    ]

次にnode modulesをインストールしていきます。このステップはnode12が入ったイメージであればなんでも実行できるのですが、面倒なので先ほど作成したfirebase-toolsが入ったnode12のイメージを自分のContainer Registryからpullしてきてyarn installを実行します。
このイメージはentrypointを/bin/bash -cとしてあるので実行したいコマンドをそのまま引数として渡します

- name: gcr.io/$PROJECT_ID/firebase:node12
  dir: functions
  args: ["yarn install"]

最後に静的解析とユニットテストを実行します。
自動テストには例によってproject idとサービスアカウントキーまでのパスが必要なので環境変数に設定しておきます。PROJECT_IDを置き換えるのを忘れないでください

- name: gcr.io/$PROJECT_ID/firebase:node12
  dir: functions
  args: ["yarn lint"]

- name: gcr.io/$PROJECT_ID/firebase:node12
  dir: functions
  args: ["yarn test"]
  env:
    - "GOOGLE_APPLICATION_CREDENTIALS=/workspace/functions/service-account-key.json"
    - "GCLOUD_PROJECT=PROJECT_ID"

最終的な設定ファイルは以下のようになります。

steps:
  - name: gcr.io/google.com/cloudsdktool/cloud-sdk
    dir: functions
    entrypoint: /bin/bash
    args:
      [
        "-c",
        "gcloud secrets versions access latest --secret=SECRET_NAME > service-account-key.json"
      ]

  - name: gcr.io/$PROJECT_ID/firebase:node12
    dir: functions
    args: ["yarn install"]

  - name: gcr.io/$PROJECT_ID/firebase:node12
    dir: functions
    args: ["yarn lint"]

  - name: gcr.io/$PROJECT_ID/firebase:node12
    dir: functions
    args: ["yarn test"]
    env:
      - "GOOGLE_APPLICATION_CREDENTIALS=/workspace/functions/service-account-key.json"
      - "GCLOUD_PROJECT=PROJECT_ID"

ココまで来たら設定ファイルのある階層に行き、以下のコマンドを実行し、成功するかテストします。

gcloud builds submit --config=cloudbuild.ci.yaml

CDワークフロー

CDワークフローは上のにdeployのステップが加わるだけです

具体的な追加ステップは以下のとおりです。firebase useでデプロイ先のプロジェクトを指定します。一応指定しないとだめっぽいです。最後にfunctionsをデプロイします。これらの操作に認証などが必要ないのがcloud buildのいい点ですね。

- name: gcr.io/$PROJECT_ID/firebase:node12
  args: ["firebase use PROJECT_ID"]

- name: gcr.io/$PROJECT_ID/firebase:node12
  args: ["firebase deploy --only functions"]

最終的な設定ファイルはこちら

steps:
  - name: gcr.io/google.com/cloudsdktool/cloud-sdk
    dir: functions
    entrypoint: /bin/bash
    args:
      [
        "-c",
        "gcloud secrets versions access latest --secret=SECRET_NAME > service-account-key.json"
      ]

  - name: gcr.io/$PROJECT_ID/firebase:node12
    dir: functions
    args: ["yarn install"]

  - name: gcr.io/$PROJECT_ID/firebase:node12
    dir: functions
    args: ["yarn lint"]

  - name: gcr.io/$PROJECT_ID/firebase:node12
    dir: functions
    args: ["yarn test"]
    env:
      - "GOOGLE_APPLICATION_CREDENTIALS=/workspace/functions/service-account-key.json"
      - "GCLOUD_PROJECT=PROJECT_ID"

  - name: gcr.io/$PROJECT_ID/firebase:node12
    args: ["firebase use PROJECT_ID"]

  - name: gcr.io/$PROJECT_ID/firebase:node12
    args: ["firebase deploy --only functions"]

Githubの連携

リポジトリの接続も非常に簡単にできます。GCPコンソールから[Cloud Build]->[トリガー]に行き、[リポジトリの接続]を押します。
ステップどおりに進めていけばGithubの認証やリポジトリの接続ができます。(説明不要だと思います)

最後に作成したワークフローをトリガーに割り当てます。[トリガーの作成]に行き、フックとなるリポジトリイベント、名前、ブランチなどの必須項目を埋めた上でCloud Build構成ファイルの場所を変更するのを忘れないようにしてください(デフォルトのファイル名から変更している場合のみ)

これでGithubイベントを検知してワークフローが走るようになります。

終わりに

今回は初めてCloud Buildを使用してみました。ドキュメントがちょっと不親切な気もしますが、使いやすくて気に入っています。

なにかミス、指摘などありましたらコメントにお願いします。

脚注
  1. Cloud Functionsにはオンラインテストという実際にプロジェクトに接続して行うユニットテストが存在します。このオンラインテストは他のfunctionsやfirestoreの状態によってテスト結果が変わってしまうのでクリーンなテスト用のプロジェクトを作成することが多いです。 ↩︎

Discussion