Firebaseプロジェクトのデプロイについて

## 2018/02/15追記

この記事以降、Alpine Linuxイメージだとしばしば`firebase-tools`のインストール中にgrpcのバイナリファイルが存在されずにインストールが失敗することがありました。

現時点（`v3.17.4`）ではこの問題は起きていませんが、安定を考えて`node-slim`イメージを使った方がいいかもしれません。

## 前置き

この記事は「[Firebase Advent Calendar 2017](https://qiita.com/advent-calendar/2017/firebase)」７日目の記事となります。

Firebaseを使ったプロダクトの知見はちらほら見るようになりましたが、デプロイ周りの知見はまだほとんど見ません。ですので私が実際にやっていることをまとめてみました。参考になれば幸いです。

## 前提とする必須条件

 * 開発、ステージング、本番などにデプロイ先を切り替えられる
 * 秘匿情報はリポジトリに組み込まない
 * CIでデプロイ可能にする

まずはこれが達成できていることを前提にします。

## Firebase CLI

まず本記事はFirebase CLIの利用を前提としています。そしてFirebase CLIに対する解説はほぼしないため、こちらについては公式のドキュメントを参照してください。

[Firebase CLI リファレンス  |  Firebase](https://firebase.google.com/docs/cli/?hl=ja)

## デプロイ先を切替可能とする

たとえばHostingやDatabase関連は、ひとつのプロジェクトにつき、ひとつしか用意されていません。そのため本番環境とステージング環境など複数の環境を用意したい場合は、それぞれのFirebaseプロジェクトを新規作成する必要があります。
[これはGoogle公式のドキュメントでも、そんな風に指示しています。](https://firebase.google.com/docs/cli/?hl=ja)


プロジェクトへの追加は `firebase use --add` を使う方法もありますが、`.firebaserc` を直接編集した方が手っ取り早いと思います。

```json:.firebaserc
{
  "projects": {
    "release": "project-id",
    "develop": "develop-project-id"
  }
}
```

`firebase init`でデフォルト連打してプロジェクトを作成すると default 枠が作られているので、これを意図的に削除しておきます。デプロイ先を明示的に指定することで、デプロイ先間違えという事故を防ぐためです。

```shell-session
# develop環境へdeployを実施する
$ firebase use develop
$ firebase deploy
```

## CI用に認証トークンを用意する

通常、Firebase CLIを使用する場合、`firebase login`で必要なGoogleアカウントへログインする必要があります。しかしそれだとデプロイするたびにログインが必要となりますし、なにより一度ブラウザに遷移するためCIでは利用できません。

という問題を回避するために、 `login:ci` というコマンドがあります。実行すると `login` 同様にブラウザで認証処理が呼び出されます。この認証が完了するとトークンを返却します。

```shell-session
$ firebase login:ci

Visit this URL on any device to log in:
https://accounts.google.com/o/oauth2/auth?client_id=......

Waiting for authentication...

✔  Success! Use this token to login on a CI server:

1/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Example: firebase deploy --token "$FIREBASE_TOKEN

# 使用例
$ firebase use develop --token 1/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
$ firebase deploy --token 1/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# 認証トークンを無効化する
$ firebase logout --token 1/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

✔  Logged out token "1/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
```

ここで取得した認証トークンを使うことで、ログインしていない環境からでもFirebase CLIを使うことができます。

```shell-session
# tokenを利用してdevelop環境でdeployを実施
$ firebase use develop --token 1/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
$ firebase deploy --token 1/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

### 認証トークンは「Firebaseのプロジェクトに紐づくトークン」ではない


気を付てほしいことがあります。ここで発行した認証トークンは、Googleアカウントに紐付いたトークンです。認証したGoogleアカウントが他のFirebaseプロジェクトにもアクセス可能な場合、そのプロジェクトにも利用可能です。

たとえば開発環境（ステージング）の認証トークンでは本番環境にデプロイさせたくない、というケースがあるとします。その場合、開発環境のFirebaseプロジェクトだけアクセスできるGoogleアカウントからトークンを発行しないといけません。また、該当するGoogleアカウントが削除された場合、当然発行された認証トークンは使えなくなります。

### これまで生成した認証トークンって確認できる？

わかりません…。少なくとも、Firebase周りの管理画面では確認できる場所がありませんでした。私も知りたい。

## Dockerでデプロイ環境を作る

ということで、CLIだけでデプロイできる準備が整ったのでここからが本番です。私の個人的な趣味と、Circle CI 2.0を利用してデプロイできるようにしたかったので、Dockerでデプロイ環境を構築します。

また、なるべくimageを軽くしたかったので、alpineイメージを利用しています。

```Dockerfile:deploy.dockerfile
FROM node:6.11-alpine

RUN apk update
RUN apk add git
RUN npm install -g firebase-tools

RUN mkdir app
WORKDIR app

ARG FIREBASE_PROJECT="develop"
ARG FIREBASE_TOKEN="x/xxxxxxxxxxxx"

CMD cd functions && npm install && cd ../ \
    && firebase use ${FIREBASE_PROJECT} --token ${FIREBASE_TOKEN} \
    && firebase deploy --token ${FIREBASE_TOKEN}
```

```yaml:deploy-develop-compose.yml
version: '2'
services:
  deploy:
    environment:
      - FIREBASE_PROJECT=develop
      - FIREBASE_TOKEN
    build:
      context: ./
      dockerfile: deploy.dockerfile
    volumes:
      - ./:/app
```

これで、 `docker-compose -f ./deploy-develop-compose.yml up --build` とすれば、Docker上でデプロイされるようになりました。
ステージング環境やリリース環境へデプロイする場合は、そのぶんだけ `FIREBASE_PROJECT` を変更したcompose.ymlを用意します。

### Dockerfileの中身少し解説

ARGで初期値を適当にいれているのは、明示的に指定させないと必ず失敗させるようにしたいためです。

また、compose.ymlでは値を指定しないenvironmentがあると、同名のシステム環境変数から値を取得します。今回のプロジェクトで秘匿したい情報は認証トークンだけですので、これでリポジトリにトークンを書かずに済みます。

DockerfileのCMDで最初にやっていることは、Cloud Functionsのデプロイに必要な設定です。Cloud Functionsを触っていないのであれば、飛ばしても問題ありません。

## Circle CI2.0で実行する

Circle CI2.0では、そのままconfig.ymlでcomposeを実行するだけです。
FIREBASE_TOKENはCircle CIの環境変数へ登録してください。ただしVM上での実行を前提とするため、 `machine:true` を忘れずに設定してください。

```yaml:.circleci/config.yml
version: 2
jobs:
  build:
    machine: true
    steps:
      - checkout
      - run:
          name: deploy
          command: |
            docker-compose -f ./deploy-develop-compose.yml up --build
```

## deployの動作について

`firebase deploy` を実行すると、プロジェクト内でdeploy対象となる各機能（Hosting、Database Rules（＋Firestore Rules）、Storage Rules、Cloud Functions）のデプロイが実行されます。この中でひとつでもdeployに失敗すると、全てのdeployは中断されます。また、既にdeployに成功している機能もロールバックされます。

これはDatabaseやStorageのRulesの更新と同時にHostingやCloud Functionsをデプロイしたい場合には有効ですが、こうした仕様であることは留意する必要があります。

個別で確実にdeployを終わらせたい場合は、 `firebase deploy --only [hosting|functions|database|storage]` で機能ごとにdeployすればいいですが、普通使うことはないと思います。

ただCircle CI2.0だとたまにtimeoutエラーでコケることがあったので、自分は`--only`で別個でアップさせる設定を利用することもあります。

全般的に言えることですが、`firebase deploy`はdeployに失敗してもsuccessが返ってくるため、deployの終了通知がきてもCircleCIのログを見て確認が必要です…。不便。

```Dockerfile:deploy.dockerfile
#（省略）

CMD cd functions && npm install && cd ../ \
    && firebase use ${FIREBASE_PROJECT} --token ${FIREBASE_TOKEN} \
    && firebase deploy --only database --token ${FIREBASE_TOKEN} \
    && firebase deploy --only firestore --token ${FIREBASE_TOKEN} \
    && firebase deploy --only storage --token ${FIREBASE_TOKEN} \
    && firebase deploy --only hosting --token ${FIREBASE_TOKEN} \
    && firebase deploy --only functions --token ${FIREBASE_TOKEN}
```

## 機能ごとにリポジトリを分けるべきか

今回紹介したデプロイでは、ひとつのリポジトリにHosting、DatabaseやStorageのRule、Cloud Functionsがすべて入ったリポジトリの場合、全部まるごとデプロイします。当然ながらその場合、不要な部分のデプロイも走ってしまい、無駄な時間ができてしまいます。

もし変更点をチェックして、必要な部分デプロイの判断をするやり方を知っているのであれば、その対応をするのが一番かもしれません。ちなみに私はやり方を知りません…。

そうでない場合、いきなり複数のリポジトリに分けても管理が大変だと思います。一番デプロイに時間がかかるのはCloud Functionsなので、やるにしてもまずはCloud Functionsとその他、ぐらいで分けるぐらいでも良いかもしれません。

以上です。