docker-composeを用いてローカルでfirebase functionsを実行する

## はじめに


firebase functionsを用いて手軽にAPIを公開したいと思い、ローカル開発環境構築を行いましたので、その構築メモを記載しました。


API開発に向けてローカルでの開発環境を構築するためには、firebase CLIをインストールし、設定をしなければいけません。
しかし、以下3点の理由からdockerコンテナ内で開発できる環境を用意することにしました。
* ローカル環境を汚したくない
* 環境依存しない形で書きたい
* (将来的に)別のfirebaseアカウントに簡単に切り替えられるようにしておきたい

## 前提条件

* docker, docker-composeがインストールされていること

## ファイルの準備

#### フォルダ構成

今回は環境変数を.envファイルでまとめて管理できるような構成にしました。

```
/
┝ firebase/
│ └ Dockerfile 
┝ .env
└ docker-compose.yml
```

#### docker-composeファイル

$で始まる変数は、後述の.envファイルから読み取っています

```yml:docker-compose.yml
version: '3'
services:
  firebase:
    build:
      context: ./firebase
      args:
        WORKDIR: $WORKDIR
    volumes:
      - ./firebase:/$WORKDIR
    ports:
      - "$FIREBASE_CLI_PORT:$FIREBASE_CLI_PORT"
      - "$FIREBASE_UI_PORT:$FIREBASE_UI_PORT"
      - "$FIREBASE_FUNCTIONS_PORT:$FIREBASE_FUNCTIONS_PORT"
    tty: true
```

#### Dockerfile

コンテナ内でfirebase CLI(firebase-tools)をインストールしています。

```Dockerfile:Dockerfile
FROM node:14-alpine
ARG WORKDIR
ENV HOME=/${WORKDIR} \
    LANG=C.UTF-8 \
    TZ=Asia/Tokyo \
    HOST=0.0.0.0
WORKDIR ${HOME}

RUN yarn global add firebase-tools

CMD ["/bin/sh"]
```

#### .envファイル

これは好みですので直接docker-composeファイルに書いてもOKです。
ここでは、ポート番号やフォルダ等の設定を変数として.envファイルで管理し、docker-composeで読み込むようにしています。

```bash:.env
WORKDIR=app
FIREBASE_CLI_PORT=9005
FIREBASE_FUNCTIONS_PORT=5001
FIREBASE_UI_PORT=4000
```
9005: firebase CLIの設定時の通信ポート
5001: firebase functions通信ポート
4000: firebase エミュレータをブラウザから見られるようにする
これらのポートを開けておくことで、ゲストOSとfirebaseへの接続、ホストOSからゲストOSへのAPI接続確認が可能になります。

## dockerコンテナ立ち上げ

docker-composeをビルド・立ち上げ

```
docker-compose build
docker-compose up -d
```

コンテナ内に入る
※ベースのdockerイメージにalpineを利用したためashコマンドになっている

```
docker-compose exec firebase ash
```

以降の手順はdockerコンテナ内で行う

## コンテナ内でのfirebase設定

今回はdocker-compose.ymlの設定によりワークスペースのパスを/appにしているため、/app/firebase直下でfirebase CLIコマンドを実行します

```
cd /app/firebase
firebase login
```

```
firebase init functions
```

これで/app/firebase 下にファイルが作成されます。
サーバを立ち上げる前に、firebase.jsonを以下のように変更することで、ホストOSからアクセスできるようにしておきます。
> ※ハマりポイント
ホストOSからlocalhostでアクセスする場合は、
ゲストOS内のfirebase APIが[0.0.0.0でlistenする]("https://qiita.com/amuyikam/items/01a8c16e3ddbcc734a46")ようにしなければならない

```json:firebase.json
{
  "functions": {
    "predeploy": [
      "npm --prefix \"$RESOURCE_DIR\" run lint",
      "npm --prefix \"$RESOURCE_DIR\" run build"
    ]
  },
  "emulators": {
    "functions": {
      "host": "0.0.0.0",
      "port": 5001
    },
    "ui": {
      "enabled": true,
      "host": "0.0.0.0",
      "port": 4000
    }
  }
}

```

## サーバ実行

このままでも実行することはできますが、一つのfunctionsに対し1つしかAPIを定義できません。そのため、expressライブラリを利用し複数APIを作成できるようにしました。今回は例として、indexファイルを以下のように書き換え、/helloへアクセスするとHello Express!と表示するAPIを作成しました。

```js:src/index.ts
import * as functions from "firebase-functions";
const express = require("express");

const app = express();

app.get('/hello', (req:any, res:any) => {
  // レスポンスの設定
  res.send('Hello Express!');
});

const api = functions.https.onRequest(app);
module.exports = { api };
```

以下コマンドを実行して立ち上がるか確認します。

```
npm run serve
```

立ち上がると以下のような文字がコンソールに出力されます。

```
i  ui: Emulator UI logging to ui-debug.log
i  functions: Watching "/app/functions" for Cloud Functions...
✔  functions[us-central1-api]: http function initialized (http://0.0.0.0:5001/xxx).
```

ここで http://0.0.0.0:5001/xxx がapiのベースurlになります。

最後にホストOSのブラウザからhttp://0.0.0.0:5001/xxx/hello  へアクセスして、ブラウザ上で"Hello Express!"と表示されれば完了です。