はじめに
Google Cloud Platformを使い始めました。
備忘録として、作業の流れを記録していきます。
フロントエンドWebサイト
まずは何でもいいのでフロントエンドのWebサイトを作ります。
Vueで作ります。
$ npm create vue@latest
$ cd <your-project>
$ npm i
$ npm run dev
Firebase Hostingにデプロイ
以下のGitHub Actionsでデプロイします。
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
がうまくいかんかった
Firebase Functionsを作る
Functionsを作ります。
$ firebase init functions
TypeScriptを選びます。
できたら、以下のファイルを編集したり追加したりします。
export * from './hello';
export * from './foo';
/foo/bar
のルートを作ります。{ "message": "foo, bar!"}
を返します。
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アドレスチェックのためのミドルウェアが入っています。あとで説明します。
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を修正
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 }}
デプロイできました。
/foo/bar
/hello/world
も機能していることはわかります。
Cloud Load Balancingを作成
ロードバランサを作成していきます。
ここは全部左側を選択。
フロントエンドは、HTTPSで。
バックエンドサービスを作成する
バックエンドタイプに、サーバーレスネットワークエンドポイントグループを指定。新しいものを作成する。
ここで、Firebase Functionsの関数が選択できる。
同じ要領で、あるやつすべて作る。今回は2つ。
ルーティングはいったんこのままで。firebase-hello
のバックエンドサービスは、後で。
作成します。
DNSの設定
ロードバランサができたら、IPアドレスがわかるので、これを先ほど指定したドメインに設定します。
証明書の状態をみます。
まだなので、待ちます。
完了しました。
アクセスしてみるが、思った通りではない。
/foo/bar
にアクセスしてみます。うまくいきません。
/bar
ならうまくいきます。
今回、すべてのルートをFirebase Functionsのfoo
へ転送するようにしました。foo
からすれば、ルートとして持っているのは/bar
ということなのでしょう。
ルーティングに、/hello/world
のFunctionsを追加
先ほどの学びを生かして、/world
をFunctionsのworld
に転送するようにします。
/world
でいけました。
Firebase Hostingへのルートを追加したいがうまくいかんかった
まずは、ロードバランサの編集から、バックエンドサービスを追加します。バックエンドタイプに、インターネットネットワークエンドポイントグループを指定。そして、新しいインターネットネットワークエンドポイントグループを作成します。
で、完全修飾ドメイン名に、Firebase HostingのURLを入力します。
これで、Firebase Hostingのバックエンドサービスが追加され、3つになりました。
次に、ロードバランサのルーティングを設定します。デフォルトのルートをFirebase Hostingにします。
これで設定は完了です(たぶん)。アクセスしてみます。
うまくいきません。Firebaseにアクセスされているのはわかりましたが。
ちょっとこれはどうすればいいのかはわかりませんでした。
Cloud Storageを使えということか。
Firebase FunctionsにIPアドレス制限を入れたい
さて、ロードバランサの背後にFirebase Functionsを置いたので、Firebase FunctionsのURLへ直接アクセスすることを禁止したいです。
そのような設定はなさそうなので、コードで書いていきます。それを書いたのが、/hello/world
です。再掲します。
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
でした。)
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');
}
});
こうすることで、ロードバランサを経由した場合はアクセスできるようになりました。
おわりに
おわり!