【勉強会用資料】Cloud Functions for FirebaseとFirebase Hostingを使い、ベーシック認証付きステージング環境と本番環境を用意する

勉強会用に作成した、Cloud Functions for FirebaseとFirebase Hostingを使い、ベーシック認証付きステージング環境と本番環境を用意するまでのフローです。

- [ベーシック認証付きステージング環境](https://basic-auth-demo-b2d37.web.app/)
    - ユーザー名：`user`
    - パスワード：`pass`
- [本番環境](https://no-basic-auth-demo.web.app/)

以下の内容を学ぶことができます。

- Git（GitHub）入門
- Firebase Hosting と Cloud Functions for Firebase 入門
    - ほんのりTypeScript

始めるまえに、以下の設定が必要です。

- GitHubアカウント
- Googleアカウント
- ローカル環境にNode.jsのインストール（version 8 以上）

## GitHubのリポジトリを作成

1. 「New repository」でリポジトリを新規作成
2. 「Clone or download」でURLを取得しクローン
3. 任意の場所でプロジェクト用ディレクトリを作成
4. クローンしたディレクトリにターミナルで移動

```console
$ mkdir demo-project
$ git clone <github-url>
$ cd demo-projecy
```

### developブランチの作成

ターミナルでブランチを作成

```console
$ git checkout -b develop
```

## ベーシック認証の環境

### Firebaseのプロジェクトを作成

[コンソール](https://console.firebase.google.com)で新規プロジェクト作成

### firebaseの設定

```console
$ firebase login
$ firebase init
```

1. `Functions: Configure and deploy Cloud Functions`を選択
2. Firebase Consoleで作成したプロジェクトを選択
3. せっかくなのでTypeScriptを選択
4. Do you want to use TSLint to catch probable bugs and enforce style? -> n（TSLintよりも、ESLintが良いかと思うのでここはNoで）
5. Do you want to install dependencies with npm now? -> [enter]（npm installしてくれます）

### 開発環境の構築

リダイレクト用ホスティングディレクトリ作成

```
public/
  └ placeholder.html
```

Webページの本体になるディレクトリを作成
（`nuxt generate`等で出力する場合は、そのディレクトリ）

表示確認ようにダミーの`index.html`を作成しておく

```
functions/
  └ dist/
    └ index.html
```

`package.json`と`package-lock.json`を移動

```
functions/
  ├ package.json
  └ package-lock.json
```

↓

```
functions/
package.json
package-lock.json
```

`npm script`を書き換え

```json
  "scripts": {
    "build": "tsc --project functions",
    "preserve": "npm run build && npm run copy-deps && npm run install-deps",
    "serve": "npm run build && firebase serve",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "init-deploy": "firebase deploy",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log",
    "copy-deps": "cpx \"*{package.json,package-lock.json,yarn.lock}\" \"functions\" -C",
    "install-deps": "cd functions && npm install && cd ../"
  }
```

パッケージの追加

```console
$ npm i express basic-auth-connect && npm i -D @types/express cpx
```

`firebase.json`の書き換え

```json
{
  "functions": {
    "source": "functions",
    "predeploy": "npm run preserve"
  },
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
        "function": "stagingApp"
      }
    ]
  }
}
```

`function/src/index.ts`に以下を記述

```typescript
import * as functions from 'firebase-functions';
import * as express from 'express';

// 型定義ファイルを作るのが面倒なので妥協。。。
const basicAuth = require('basic-auth-connect');

const user = 'user';
const pass = 'pass';

const server = express();
server.use(basicAuth(user, pass));
server.use(express.static('./dist'));

export const stagingApp = functions.https.onRequest(server);
```

`function/src/index.ts`をビルドしてローカルサーバー立ち上げ

```console
$ npm run serve
```

#### 機密情報を環境変数に変更

このままだとベーシック認証のアイパスが見えてしまうので環境変数に格納

```console
$ cd functions
$ firebase functions:config:set basicauth.user="<yourUserName>" basicauth.pass="<yourPassword>"
```

設定した環境変数は以下で確認できる

```console
$ firebase functions:config:get
```

ローカルサーバーでも使えるように、`functions/.runtimeconfig.json`を作成

```console
$ firebase functions:config:get > .runtimeconfig.json
$ cd ../
```

`.gitignore`に`.runtimeconfig.json`を追加してgit管理から外す（リモートにプッシュ禁止）

また、`functions/`配下にも`node_modules`や`package.json`が必要なため、以下のコマンドで複製しインストールしておく

（ローカルサーバー立ち上げ、デプロイ時にも実行されます）

```console
$ npm run copy-deps && npm run install-deps
```

`function/src/index.ts`を修正

```typescript
import * as functions from 'firebase-functions';
import * as express from 'express';

// 型定義ファイルを作るのが面倒なので妥協。。。
const basicAuth = require('basic-auth-connect');

// 環境変数
const user = functions.config().basicauth.user;
const pass = functions.config().basicauth.pass;

const server = express();
server.use(basicAuth(user, pass));
server.use(express.static('./dist'));

export const stagingApp = functions.https.onRequest(server);
```

ローカルサーバーで確認

```console
$ npm run serve
```

`public`ディレクトリをアップするため、以下を実行

```console
$ npm run init-deploy
```

次回以降は、`public`ディレクトリをアップしないので、以下でOK

```console
$ npm run deploy
```

コミットしてGitHubにプッシュ

```console
$ git add .
$ git commit -m "コミットメッセージ"
$ git push origin HEAD
```

## 本番環境

firebaseで本番環境も対応する場合は切り替えが必要なので対応

[コンソール](https://console.firebase.google.com)で新規プロジェクト作成

今回は2種類の本番環境を用意

- 静的ホスティング
  - 静的なサイトであれば十分
  - Cloud Functions for Firebase を経由しないのでパフォーマンス的には少し有利？
- Expressをそのまま使用
  - Node.jsが使える
     - よって、ExpressじゃなくてもOK

### 環境構築（静的ホスティング）

`develop`ブランチから新たにブランチを作成

```console
$ git checkout -b static_hosting
```

作成した本番用プロジェクトのホスティング情報を`.firebaserc`に追加

```console
$ firebase target:apply hosting production <production-project-id>
$ firebase target:apply hosting staging <staging-project-id>
```

`firebase.json`に環境別のホスティング情報を追記

```diff
  {
    "functions": {
      "source": "functions",
      "predeploy": "npm run preserve"
    },
+   "hosting": [{
+     "target": "staging",
+     "public": "public",
+     "ignore": [
+       "firebase.json",
+       "**/.*",
+       "**/node_modules/**"
+     ],
+     "rewrites": [
+       {
+         "source": "**",
+         "function": "stagingApp"
+       }
+     ]
+   },
+   {
+     "target": "production",
+     "public": "functions/dist",
+     "ignore": [
+     "firebase.json",
+       "**/.*",
+       "**/node_modules/**"
+     ]
+   }]
  }
```

`package.json`の書き換え

```diff
  "scripts": {
    "build": "tsc --project functions",
    "preserve": "npm run build && npm run copy-deps && npm run install-deps",
    "serve": "npm run build && firebase serve",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "deploy-init": "firebase deploy",
-   "deploy": "firebase deploy --only functions",
+   "deploy-staging": "firebase deploy --only functions",
+   "deploy-production": "firebase deploy --only hosting:production",
    "logs": "firebase functions:log",
    "copy-deps": "cpx \"*{package.json,package-lock.json,yarn.lock}\" \"functions\" -C",
    "install-deps": "cd functions && npm install && cd ../"
  },
```

ステージングにデプロイ

```console
$ npm run deploy-staging
```

本番にデプロイ

```console
$ npm run deploy-production
```

### 環境構築（Express環境）

一度、`develop`ブランチに戻り、`express_hosting`ブランチを新たに作成

```console
$ git checkout develop
$ git checkout -b express_hosting
```

作成した本番用プロジェクトを指定し、エイリアスを設定

```console
$ firebase use --add
$ What alias do you want to use for this project? (e.g. staging)
```

`.firebaserc`にプロジェクトが追加される（今回は`production`というエイリアスで登録）

もともとの`default`エイリアスはわかりやすい名前に変えてOK（以下は`staging`に変更）

```diff
{
  "projects": {
-   "default": "<staging-project-id>",
+   "staging": "<staging-project-id>",
+   "production": "<production-project-id>"
  }
}
```

`firebase.json`も変更

```diff
{
  "functions": {
    "source": "functions",
    "predeploy": "npm run preserve"
  },
  "hosting": {
    "public": "public",
    "ignore": [
      "firebase.json",
      "**/.*",
      "**/node_modules/**"
    ],
    "rewrites": [
      {
        "source": "**",
-       "function": "stagingApp"
+       "function": "app"
      }
    ]
  }
}
```

`npm script`にプロジェクトの切り替えタスクを追加

```diff
  "scripts": {
    "build": "tsc --project functions",
    "preserve": "npm run build && npm run copy-deps && npm run install-deps",
    "serve": "npm run build && firebase serve",
    "shell": "npm run build && firebase functions:shell",
    "start": "npm run shell",
    "init-deploy": "firebase deploy",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log",
    "copy-deps": "cpx \"*{package.json,package-lock.json,yarn.lock}\" \"functions\" -C",
    "install-deps": "cd functions && npm install && cd ../",
+   "env-staging": "firebase use staging",
+   "env-production": "firebase use production"
  }
```

プロジェクト切り替えが可能になる

```console
$ npm run env-xxx
```

### 本番環境でベーシック認証解除

環境変数をクローンし、問題がないか確認

```console
$ firebase functions:config:clone --from <project-id>
$ firebase functions:config:get
```

以下のディレクトリとファイルを作成

```
functions/src
  └ app
    ├ app.ts
    ├ index.ts
    ├ productionApp.ts
    └ stagingApp.ts
```

`functions/src/app/stagingApp.ts`

```typescript
import * as functions from 'firebase-functions';
import * as express from 'express';

// 型定義ファイルを作るのが面倒なので妥協。。。
const basicAuth = require('basic-auth-connect');

const user = functions.config().basicauth.user;
const pass = functions.config().basicauth.pass;

const server = express();
server.use(basicAuth(user, pass));
server.use(express.static('./dist'));

export const stagingApp = functions.https.onRequest(server);
```

`functions/src/app/productionApp.ts`

```typescript
import * as functions from 'firebase-functions';
import * as express from 'express';

const server = express();
server.use(express.static('./dist'));

export const productionApp = functions.https.onRequest(server);
```

`functions/src/app/app.ts`

```typescript
import { productionApp } from './productionApp';
import { stagingApp } from './stagingApp';

const isStaging = process.env.GCLOUD_PROJECT === '<staging-project-id>';
export const app = isStaging ? stagingApp : productionApp;
```

`functions/src/app/index.ts`

```typescript
export { app } from './app';
```

`functions/src/index.ts`を書き換え

```typescript
export { app } from './app';
```

本番環境用ローカルサーバーを立ち上げ

```console
$ npm run env-production
$ npm run serve
```

テスト環境用ローカルサーバーを立ち上げ

```console
$ npm run env-staging
$ npm run serve
```

本番環境にデプロイ

```console
$ npm run env-production
$ npm run init-deploy
```

次回以降は、以下でOK

```console
$ npm run env-production
$ npm run deploy
```

テスト環境にデプロイ

```console
$ npm run env-production
$ npm run deploy
```

コミットしてGitHubにプッシュ

```console
$ git add .
$ git commit -m "コミットメッセージ"
$ git push origin HEAD
```

最後にGitHubでプルリクエストを作り、`develop`ブランチにマージしてみましょう！