Firebase FunctionsなどをCloud Load Balancingの配下に設定する

# はじめに
Google Cloud Platformを使い始めました。

備忘録として、作業の流れを記録していきます。

# フロントエンドWebサイト

まずは何でもいいのでフロントエンドのWebサイトを作ります。

Vueで作ります。

```
$ npm create vue@latest
$ cd <your-project>
$ npm i
$ npm run dev
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/48c82734-a50d-28ef-8efa-85aede72e807.png)

# Firebase Hostingにデプロイ

以下のGitHub Actionsでデプロイします。

```yml:.github/workflows/deploy-firebase.yml
name: Deploy to Firebase

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm install

      - name: Build project
        run: npm run build

      - name: Install Firebase CLI
        run: npm install -g firebase-tools

      - name: Deploy to Firebase Hosting
        run: firebase deploy --only hosting --token ${{ secrets.FIREBASE_TOKEN }}
```

…Node.js v22だと `npm install -g firebase-tools`がうまくいかんかった

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/5f5f200a-2c37-5837-5460-5c80ec57d8fa.png)

# Firebase Functionsを作る

Functionsを作ります。

```
$ firebase init functions
```

TypeScriptを選びます。

できたら、以下のファイルを編集したり追加したりします。

```typescript:functions/src/index.ts
export * from './hello';
export * from './foo';
```

`/foo/bar`のルートを作ります。`{ "message": "foo, bar!"}`を返します。

```typescript:functions/src/foo/index.ts
const functions = require('firebase-functions');
const express = require('express');

const app = express();
const router = express.Router();

// @ts-ignore
router.get('/bar', async (req, res, next) => {
    console.log(req.headers);
    res.send({
        message: 'foo, bar!'
    });
});

app.use('/', router);

export const foo = functions
    .region('asia-northeast1')
    .runWith({ memory: '128MB',  timeoutSeconds: 2 })
    .https
    .onRequest(app);
```

こちらは`/hello/world`のルートを作ります。

IPアドレスチェックのためのミドルウェアが入っています。あとで説明します。

```typescript:functions/src/hello/index.ts
const functions = require('firebase-functions');
const express = require('express');

const app = express();
const router = express.Router();

// @ts-ignore
app.use((req, res, next) => {
    const forwardedFor = req.headers['x-forwarded-for'];
    if (!forwardedFor) {
        res.status(403).send('Forbidden');
        return;
    }

    const forwardedIps = forwardedFor.split(',');

    // x-forwarded-forは偽装が可能
    // 偽装しない場合、<your-ip>,<load-balancer-ip>の順になる
    // <your-ip>,<load-balancer-ip>と偽装した場合、<your-ip>,<load-balancer-ip>,<your-ip>の順になった
    // LoadBalancerは追記するが、Firebase Functionsは追記しない？
    // <load-balancer-ip>と偽装した場合、<load-balancer-ip>,<your-ip>の順になる
    // なので、最後の要素が<load-balancer-ip>であるかどうかで判定する
    if (forwardedIps.length >= 2 && forwardedIps[forwardedIps.length - 1].trim() === '34.54.110.179') {
        next();
    } else {
        res.status(403).send('Forbidden');
    }
});

// @ts-ignore
router.get('/world', async (req, res, next) => {
    console.log(req.headers);
    res.send({
        message: 'Hello, World!'
    });
});

app.use('/', router);

export const hello = functions
    .region('asia-northeast1')
    .runWith({ memory: '128MB',  timeoutSeconds: 2 })
    .https
    .onRequest(app);
```

# Firebase Functionsをデプロイする

GitHub Actionsを修正

```diff_yaml:.github/workflows/deploy-firebase.yml
name: Deploy to Firebase

on:
  push:
    branches:
      - main

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: npm install

+     - name: Install dependencies ./functions
+       run: npm install
+       working-directory: functions
+
      - name: Build project
        run: npm run build

      - name: Install Firebase CLI
        run: npm install -g firebase-tools

      - name: Deploy to Firebase Hosting
-       run: firebase deploy --only hosting --token ${{ secrets.FIREBASE_TOKEN }}
+       run: firebase deploy --only hosting,functions --token ${{ secrets.FIREBASE_TOKEN }}
```

デプロイできました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/e877de49-0c11-9917-0f3b-06df58b98074.png)

`/foo/bar`

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/080553fb-e0e7-acd8-469a-22dae8d905cc.png)

`/hello/world`も機能していることはわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/be1e91a0-8cfb-79a0-c240-2bb7b68ac374.png)

# Cloud Load Balancingを作成

ロードバランサを作成していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/1ec6e4c1-4a27-e22b-6db1-25c593fb0067.png)

ここは全部左側を選択。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/2757e139-ed59-934a-ec8a-9e849211fcf6.png)

フロントエンドは、HTTPSで。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/275cd255-dd61-000d-f624-a13ba33dc60d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/a897f6a2-2c2f-e650-715f-f22e99f80f2f.png)

バックエンドサービスを作成する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/0850f034-27e9-1d9f-fc69-30dc6e73bae2.png)

バックエンドタイプに、サーバーレスネットワークエンドポイントグループを指定。新しいものを作成する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/525433f2-c8de-a71c-9e94-76d9d48348cc.png)

ここで、Firebase Functionsの関数が選択できる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/30003123-eef5-e6ba-2cfb-983e3e94fbfd.png)

同じ要領で、あるやつすべて作る。今回は２つ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/3cc5f51f-31e5-22e3-46a7-505ec714087d.png)

ルーティングはいったんこのままで。`firebase-hello`のバックエンドサービスは、後で。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/0a9639ad-3864-c156-db4c-38cc6a323ffa.png)


作成します。

# DNSの設定

ロードバランサができたら、IPアドレスがわかるので、これを先ほど指定したドメインに設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/db251271-2edb-f6e5-8e01-677680224caa.png)

証明書の状態をみます。

まだなので、待ちます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/6ec78d75-32d7-94b5-8e6f-6802fb2616e2.png)

完了しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/27d5f6ca-3fc9-2ee2-069c-29cf8e8a1abf.png)


# アクセスしてみるが、思った通りではない。

`/foo/bar`にアクセスしてみます。うまくいきません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/4ec7b295-12d0-fc93-e5fb-c644784ce85e.png)

`/bar`ならうまくいきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/c105a99f-5525-768d-6282-8ce75747cdeb.png)

今回、すべてのルートをFirebase Functionsの`foo`へ転送するようにしました。`foo`からすれば、ルートとして持っているのは`/bar`ということなのでしょう。

# ルーティングに、`/hello/world`のFunctionsを追加

先ほどの学びを生かして、`/world`をFunctionsの`world`に転送するようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/31319d61-ebb3-c7e5-ffa6-9affadc81690.png)

`/world`でいけました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/45f483db-221c-17ad-ea5f-2d4fa4536ee9.png)

# Firebase Hostingへのルートを追加したいがうまくいかんかった

まずは、ロードバランサの編集から、バックエンドサービスを追加します。バックエンドタイプに、インターネットネットワークエンドポイントグループを指定。そして、新しいインターネットネットワークエンドポイントグループを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/50d89c77-fa7d-f7c0-55a0-6ebae5420178.png)

で、完全修飾ドメイン名に、Firebase HostingのURLを入力します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/85c89ddc-c6d6-caee-ad3e-5d4040b8b126.png)

これで、Firebase Hostingのバックエンドサービスが追加され、3つになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/aa34986f-5a3a-6da2-b869-26955b6178d2.png)

次に、ロードバランサのルーティングを設定します。デフォルトのルートをFirebase Hostingにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/1a78ed68-1d2f-b4f5-b920-154686ef49c0.png)

これで設定は完了です（たぶん）。アクセスしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/6627b368-9523-2810-87d4-9b5a483553a9.png)

うまくいきません。Firebaseにアクセスされているのはわかりましたが。

ちょっとこれはどうすればいいのかはわかりませんでした。

Cloud Storageを使えということか。

# Firebase FunctionsにIPアドレス制限を入れたい

さて、ロードバランサの背後にFirebase Functionsを置いたので、Firebase FunctionsのURLへ直接アクセスすることを禁止したいです。

そのような設定はなさそうなので、コードで書いていきます。それを書いたのが、`/hello/world`です。再掲します。

```typescript:functions/src/hello/index.ts
const functions = require('firebase-functions');
const express = require('express');

const app = express();
const router = express.Router();

// @ts-ignore
app.use((req, res, next) => {
    const forwardedFor = req.headers['x-forwarded-for'];
    if (!forwardedFor) {
        res.status(403).send('Forbidden');
        return;
    }

    const forwardedIps = forwardedFor.split(',');

    // x-forwarded-forは偽装が可能
    // 偽装しない場合、<your-ip>,<load-balancer-ip>の順になる
    // <your-ip>,<load-balancer-ip>と偽装した場合、<your-ip>,<load-balancer-ip>,<your-ip>の順になった
    // LoadBalancerは追記するが、Firebase Functionsは追記しない？
    // <load-balancer-ip>と偽装した場合、<load-balancer-ip>,<your-ip>の順になる
    // なので、最後の要素が<load-balancer-ip>であるかどうかで判定する
    if (forwardedIps.length >= 2 && forwardedIps[forwardedIps.length - 1].trim() === '34.54.110.179') {
        next();
    } else {
        res.status(403).send('Forbidden');
    }
});

// @ts-ignore
router.get('/world', async (req, res, next) => {
    console.log(req.headers);
    res.send({
        message: 'Hello, World!'
    });
});

app.use('/', router);

export const hello = functions
    .region('asia-northeast1')
    .runWith({ memory: '128MB',  timeoutSeconds: 2 })
    .https
    .onRequest(app);
```

基本的には、`X-Forwarded-For`ヘッダを見ることになるかと思います。しかし、このヘッダーはアクセスする人が自由に付加することができます。いわゆる偽装が可能です。

軽く偽装（アクセス元で付与）をしてみる実験をしてみた結果、Firebase Functions側では、`X-Forwarded-For`ヘッダは以下のような規則で取得できました。

- 何も付与しない場合、`<your-ip>,<load-balancer-ip>`の順になる
- `<your-ip>,<load-balancer-ip>`と偽装した場合、`<your-ip>,<load-balancer-ip>,<your-ip>`の順になった
- `<load-balancer-ip>`と偽装した場合、`<load-balancer-ip>,<your-ip>`の順になる

というわけで、これだけを見た感じですが、最後の要素が`<load-balancer-ip>`であれば、処理を続行する、というルールにすればいいことがわかります。

それをコードであらわしたのが、以下になります。（今回はロードバランサのIPが`34.54.110.179`でした。）

```typescript
app.use((req, res, next) => {
    const forwardedFor = req.headers['x-forwarded-for'];
    if (!forwardedFor) {
        res.status(403).send('Forbidden');
        return;
    }

    const forwardedIps = forwardedFor.split(',');

    // 最後の要素が<load-balancer-ip>であるかどうかで判定する
    if (forwardedIps.length >= 2 && forwardedIps[forwardedIps.length - 1].trim() === '34.54.110.179') {
        next();
    } else {
        res.status(403).send('Forbidden');
    }
});
```

こうすることで、ロードバランサを経由した場合はアクセスできるようになりました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/7e1d51e4-7beb-8248-e222-429d3cc45359.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/172833/be1e91a0-8cfb-79a0-c240-2bb7b68ac374.png)

# おわりに

おわり！