Cloud Load Balancingを使ってCloud Functionsに独自ドメインを割り当てる

## はじめに

GCPの [Cloud Functions](https://cloud.google.com/functions/?hl=ja) はちょこっとした処理を記述するのに便利です。ただ提供されるURLは以下のような形式になっていて少し長いです。

```
https://<region-name>-<project-name>.cloudfunctions.net/<function-name>
```

そのためURLを公開する場合は独自ドメインを適用してわかりやすいURLを使いたくなります。

またCloud FunctionsのURLは少しクセがあって、以下のような特徴があります。  (2020年8月現在）

- Server Name Indication（SNI）が必須
  - SNIは同一のIPアドレスで複数のSSL証明書を扱う方法です。詳しくは[こちら]()を参照ください。
  - 組み込み機器など非SNI端末でアクセスすると、返却されるサーバ証明書が`*.google.com`となっており`cloudfunctions.net`と一致しないためSSL接続でエラー（証明書が無効）になります。

- バーチャルホストで運用されている風の挙動
  - 直IPアドレス指定やCNAMEで別のドメイン指定してアクセスすると`404`が返却されるため、独自ドメインでアクセスさせることができない。



そこで今回は[Cloud Load Balancing](https://cloud.google.com/load-balancing?hl=ja) を使って独自ドメインでのアクセスを試してみました。GCPのコンソールのぽちぽち操作で独自ドメインを利用したい場合向けの記事となっています。公開するCloud FunctionsがAPIっぽかったり、APIマネジメントの機能がほしかったりする場合は[こちらの記事](https://qiita.com/hidesuke/items/f7da86666db80441ee47)が参考になります。

Cloud Load Balancingを経由することで次のようなメリットがあります。

- 独自ドメインを割当られる 
- 専用のIPアドレスでアクセスできる
- 非SNI端末へもサービスを提供できる
- セルフマネッジド証明書、自己署名証明書（オレオレ証明書）が使える
- SSL Policyで厳密にCipherSuiteを限定できる


## 構成

![gcp-cloudfunctions.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695122/879cd2ce-46b4-2db5-a387-9f599f851d50.png)

ポイントは`インターネットネットワークエンドポイントグループ（Internet NEG）`を使うところです。
`Internet NEG`はロードバランサーのバックエンドとして外部サービスを利用できるようにするものです。接続できる外部サービスはFQDNやIPアドレスでアクセスできるインターネット上のHTTP(S)リソースになります。

## 手順

### 1. Cloud Functionsを作る

今回は[クイックスタート](https://cloud.google.com/functions/docs/quickstart-nodejs?hl=ja)のHello Worldを表示する関数を独自ドメインでアクセスできるようにします。
手順に従い作成しますが「未認証の呼び出しを許可する」にチェックをつけておきます。

以下、手順上は作成した関数名を「function-1」としています。



### 2. インターネットネットワークエンドポイントグループ（Internet NEG)を作る

GCPのコンソールのメニューから「ComputeEngine」-「ネットワークサービスグループ」を選び、次の設定でネットワークエンドポイントグループを作成します。完全修飾ドメイン名には作成したCloud Functions（function-1）のFQDNを指定してください。

| 項目 | 設定値 |
| ------ | ------ |
| 名前 | 任意の名前です。ここでは`function-1-neg`とします |
| ネットワーク エンドポイント グループの種類 | `ネットワークエンドポイントグループ(インターネット)` |
|リージョン| Global|
|デフォルトポート|443|
|**新しいネットワーク エンドポイント**||
|追加手段| 完全修飾ドメイン名とポート|
|完全修飾ドメイン名| `<region-name>-<project-name>-cloudfunctions.net`|
|ポートタイプ| デフォルト|

### 3. Cloud Load Balancing　を作る

GCPのコンソールのメニューから「ネットワークサービス」-「負荷分散」を選び、画面上部の「`ロードバランサを作成`」ボタンで作成を開始します。

| 項目                             | 設定値                       |
| -------------------------------- | ---------------------------- |
| タイプ                           | HTTP(S) 負荷分散             |
| インターネット接続または内部専用 | インターネットから自分のVMへ |

#### 3-(1) バックエンドの設定

続いてバックエンドの設定をします。


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695122/b4401677-9e76-e8e6-4362-1728cdc48cfe.png" alt="gcp-createbackendservice.png" style="zoom:50%;" />


「バックエンド サービスとバックエンド バケット」の項目で「バックエンドサービス」ー「バックエンドサービスの作成」を選択して、次の設定でバックエンドサービスを作成します。バックエンドには手順2で作成した`Internet NEG`を指定します。

|項目|設定値|
|---|---|
|名前| 任意の名前です。`function-1-backend`|
|バックエンドタイプ| インターネットネットワークエンドポイントグループ|
|プロトコル| HTTPS |
|タイムアウト| 30秒|
|バックエンド|インターネットエンドポイントグループ：前の手順で作成したインターネットNEGを指定（`function-1-neg`)|

#### 3-(2) ホストとパスのルール

次にホストとパスのルールを設定します。

|項目|設定値|
|---|---|
|モード| 単純なホストとパスのルール|
|デフォルトのルールのバックエンド|直前の手順で作成したバックエンドサービスを選択（`function-1-backend`)|


#### 3-(3) フロントエンドの設定
今回は手順省略のためHTTPで構築してみます。もちろんHTTPSを設定すればHTTPSでアクセスできます。HTTPはデフォルトで作成されているためそのまま。`作成`ボタンで作成します。

### 4. 動作確認する

しばらくすると作成した`ロードバランサの詳細`画面にIPアドレスが表示されます。

:point_up: 構成および反映には少し時間がかかります。コンソール上で完了してから3分程度待ちましょう。

ここでは割り当てられたIPアドレスを`xxx.xxx.xxx.xxx`とします。

ブラウザから`http://xxx.xxx.xxx.xxx/function-1`にアクセスします。

やったね。「Hello World」って表示され…ない。


![404.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695122/393366d2-48b7-2fbd-62a0-b5eaea75e884.png)

404が返ってきます…。 :cry:


## 修正する

さて原因についてですが、文頭のCloud FunctionsのURLの特徴で記載したように、IPアドレスやCNAME等で別名をつけてアクセスできません。

言い換えるとHTTPリクエストのHostヘッダーが`<region-name>-<project-name>-cloudfunctions.net`になっていないとアクセスできないことになります。

ドキュメントの制限事項の部分に以下の記載があります（これに気づかず数時間彷徨いましたが…）

https://cloud.google.com/load-balancing/docs/negs/internet-neg-concepts?hl=ja#limitations

> HTTP リクエストの `Host` ヘッダーに特定の値を必要とするカスタムの送信元を使用する場合、バックエンド サービスを構成して `Host` ヘッダーをその値に設定する必要があります。ユーザー定義のリクエスト ヘッダーを構成しない場合、バックエンド サービスはクライアントが Google Cloud 外部 HTTP(S) ロードバランサへの接続に使用した Host ヘッダーを保持します。

これによるとさきほどはHostヘッダーに`Host: xxx.xxx.xxx.xxx`が設定されてCloud Functionsに送信しているためアクセスできなかったと推察できます。

:point_up: 実際にこの挙動を確認したい場合はInternet NEGのネットワークエンドポイントの指定を [webhook.site](https://webhook.site) 等に差し替えてHostヘッダーを確認してみてください。

Hostヘッダーを書き換える設定は作成したロードバランサーの「ホストとパスのルール」のところで「モード」を「詳細なホストとパスのルール（URLリダイレクト、URLの書き換え）」に変更します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695122/77dbb4fb-d329-1996-b039-0e2a97b4483b.png" alt="mode.png" style="zoom:50%;" />


さらに、デフォルトのルールを選択してホスト名の書き換えを設定します。

|項目|設定値|
|---|---|
|アクション| トラフィックを1つのバックエンドにルーティング|
|ホストの書き換え|自身のCloud Functionsのホスト名`<region-name>-<project-name>-cloudfunctions.net`|
|パス接頭辞の書き換え|<未指定>|
|バックエンド|直前の手順で作成したバックエンドサービスを選択（`function-1-backend`)|

デフォルトだと「ホストの書き換え」は非表示になっているので「アドオン アクション（URLの書き換え）」リンクを押して表示してください。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695122/a8738667-4260-7dcc-4e06-fff8d220c03b.png" alt="gcp-addonaction.png" style="zoom:50%;" />



### 動作確認する（再）

さて修正が反映されるのに3分程度かかりますので少し待ってから、再度ブラウザから`http://xxx.xxx.xxx.xxx/function-1`にアクセスしてみます。

画面に `Hello World!` が表示されれば成功です。:congratulations:


## 最後に

固定のIPアドレスでアクセスできるようになったので、あとはDNSにAレコードを設定して独自ドメインでのアクセス完了です。
Cloud Load Balancingへ証明書設定すればHTTPSアクセスにしたりできる状態ですね。

最後に数点補足です。
- Hostヘッダの書き換えをしているのでリダイレクト伴うような挙動は影響をうける可能性があります。
- 今回の用途により適しているServerless NEGもあります。ただし、2020年8月時点でベータ版で、GCPのコンソールからぽちぽち操作できません。

## 参考文献
この記事は以下の情報を参考にして執筆しました。

- [Cloud Functions Node.jsクイックスタート](https://cloud.google.com/functions/docs/quickstart-nodejs?hl=ja)
- [Cloud Load Balancing ドキュメント](https://cloud.google.com/load-balancing/docs?hl=ja) 
- [インターネットNEG概要](https://cloud.google.com/load-balancing/docs/negs/internet-neg-concepts?hl=ja)

そして、

・Google、Google Cloud、Google Cloud PlatformおよびGCPは、Google LLCの商標または登録商標です。
・その他記載されている会社名、製品名、サービス名、ロゴ等は各社の商標または登録商標です。