Google Cloud Service Extensionを試してみた

この記事は[Optimind Advent Calendar 2024](https://qiita.com/advent-calendar/2024/optimind) 5日目の記事です。

# Google Cloud Service Extensionを試してみた

先日[GAされたGoogle Cloud Service Extension](https://cloud.google.com/service-extensions/docs/lb-extensions-overview)を試してみました。
将来的にGoogle Cloudにて扱いやすいドキュメントが提供されると思うので、この資料はすぐに陳腐化するためあまり参考にしないでください。
また、この記事ではCloud Load Balancing(以降、CLB)のService Extensionについてしか触れないです。※1
想定読者としては、現在Lambda@Edgeを用いてAWS上でアプリケーションを構築しているけれどService Extensionsが出たことでGCPに移行できるのか検討をしたい方です。

## Service Extension

Media CDNやCLBのデータ処理に拡張ポイント(拡張できる箇所)が定められており、そこにプログラムを配置して処理を実行することができる機能です。
要はAWSでのLambda@Edgeのようなものになると思います。

### 種類

以下の2種類が扱えるようです。

* Callout
  * gRPCのインターフェースに従ったバックエンドサービスを紐づけて拡張する形式
* Plugin
  * GCP上で管理しているリソースで動作させて拡張する形式

#### Callout

gRPC APIを配置して特定の拡張ポイントで定義されたAPIを呼び出すような形式でした。
こちらは現時点でもサンプルとしてGoとPythonがありました。
また、gRPCのインターフェースに従ったバックエンドサービスを紐づけることで実現できます。
そのため、Cloud RunやCompute Engineなどで動かすことができます。
サポートされているCLBの種類はCalloutの方が多いです。

#### Plugin(Pre-GA)

Wasmモジュールを生成してそれを拡張ポイントで定義された関数が呼び出されるような形式でした。
こちらは現時点ではRustとC++がサポートされていました。
サポートされているCLBの種類はCalloutと比べて少ないです。
個人的に実装の際に踏みそうだと感じた注意点として以下の点があります。

* タイマーが使えないです。waitするような処理を書くとタイムアウトするようです
* 送信ネットワーク呼び出しができません(JWTの公開鍵を取ってきて認証するや外部サーバによる認可の実装などはできないです)

### 拡張ポイント

拡張ポイントとして拡張できる箇所が定義されています。
CLBでは以下の3箇所があります。

* Route
  * ユーザのリクエストを受け付けたタイミング
* Authorization
  * 認証用の処理タイミング(?)
* Traffic
  * Lambda@EdgeのOrigin RequestやOrigin Responseのようなタイミング

また、それぞれの拡張ポイントではリクエストに対して条件を指定できます。
その条件に対して複数(最大3つまで)のCalloutやPluginを設定することができます。

![全体像](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130732/7900a405-5bb3-0832-4d7d-cbaa35ffcbf6.png)

#### Route

* 動作タイミング
  * ユーザのリクエストを受け付けた最初
* 注意点
  * グローバル外部Application Load Balancingでは設定できません
  * Request Headerにはアクセスできますが、Request Bodyにはアクセスできないです
  * Route 拡張ポイントからレスポンスを返すことはできないです

![Route](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130732/16a8fee2-a588-c039-debb-57778abea289.png)


#### Authorization(Pre-GA)

* 動作タイミング
  * CDNやIdP動作後
  * 認証が通るか通らないかの判定前に動作します
* 注意点
  * Request HeaderにはアクセスできるがRequest Bodyにはアクセスできないようです
  * Authorization 拡張ポイントからレスポンスを返すことはできないようです
  * CDNの後段で動くのでCDNでキャッシュしている内容に対して認証をかけるみたいな用途には利用できなさそうです
  * 現時点では、Google Cloud Console上からはこの拡張ポイントは利用できなさそうでした※3

![Authorization](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130732/a6c9a2cb-9389-d019-0cb3-3dbcade86230.png)


#### Traffic

* 動作タイミング
  * Lambda@EdgeのOrigin RequestやOrigin Responseのようなタイミング
  * バックエンドサービスの接続のheader書き込み前に動きます
* できること
  * Responseを返すことができます
  * HeaderやBodyも書き換えることができます
* 注意点
  * バックエンドサービスでのRequest/Response Header書き込み前に実行されるので、バックエンドサービスでのRequest/Response Headerの書き込みが優先になります(たとえば、ExtensionでHogeヘッダーをFugaにしてサーバレスバックエンドサービスでHogeをPiyoにしている場合は、HogeヘッダーはPiyoになります)

![アプリケーション系の処理](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/130732/701b4987-9abd-8b3c-4fef-c1f4877d45af.png)

### ざっくりとした設定方法

#### Callout

Calloutでの設定方法は以下の手順でした。※4

1. インフラリソースを用意する
1. バックエンドサービスを作る
1. Service Extensionに拡張機能としてバックエンドサービスを紐づける

また、Calloutとして設定しているバックエンドサービスに対してRequest/Response Headerの書き換え設定を行なっても無視されます。
あくまでもCalloutの処理内でのみ書き換えることができるようでした。

#### Plugin

Pluginでの設定方法は以下の手順でした。

1. PluginをArtifact Registoryに登録する
1. Service Extensionにプラグインを追加する
1. Service Extensionに拡張機能として追加したプラグインを設定する

### Lambda@Edgeとの比較

#### Service Extensionではできないこと

Amazon CloudFrontのViewer系トリガー相当の処理はできないです。

Route拡張ポイントを使うとできそうに見えますが、Route拡張ポイントはグローバル外部アプリケーションロード バランサーでは使えないです。
今後追加されるであろうMedia CDNのService ExtensionがこのViewer系のような処理を行う箇所を提供するように見えますが、どこまでのことができるのかはまだわからないです。
現時点の資料を読む限り、Media CDNではPluginのみサポートしているようなので外部から公開鍵を取ってくるということがやりたくなりそうな認証とは相性が悪いかもしれないです。

#### Service Extensionだとやりやすそうなこと

Service Extensionを実行する制限が複雑なケース(リクエストヘッダーにAuthorizationが含まれているかつ、パスが/user/*の場合のみ認証するなど)では、ルールとして記述できるため、要件によっては呼び出しを減らすということがやりやすいかもしれないです。
Calloutであれば、実質好きな言語で動かすことができるのでプログラミング言語は幅広く扱えます。
組織の管理ポリシーを設定して、Service Extensionでリクエストbodyの書き換えやリクエストheaderの書き換えをできないようにするなどの制限ができるので管理面で役立つことがあるのかもしれないです。
Service Extensionを小さく作って複数を組み合わせて作るということはやりやすいかもしれないです。1つの条件に対して複数のCalloutやPluginを紐づけることができるためです。

## まとめ

* Amazon CloudFrontのViewer系トリガーで処理をしている場合は、移行しない方が良さそうです(Lambda@Edge内で外部に依存している場合は特に)
* Lambda@Edgeの実行回数でお困りかつ、headerとパスの組み合わせなどで実行回数を減らせることが魅力的な場合は移行を検討してみることに価値があるかもしれないです

## 注釈

※1 Media CDNもあるようですがMedia CDNの利用に関しては、記事作成時点では「[Media CDN へのアクセスをご希望の場合は、Google Cloud の営業担当者またはアカウント担当者にお問い合わせください。](https://cloud.google.com/media-cdn/docs/overview?hl=ja#request-access)」と記載されていたため希望した場合のみ扱える状態なのかもしれないです
※2 Armor適用前に動くけれど、DDoSを食らったらお金がいっぱいかかるみたいな事態には陥るのだろうか…？(小声
※3 [exampleがありますが、exampleのコードでは現時点で見る限りAuthenticationではなくTraffic(Request)の方でJWTの認証をしていました](https://github.com/GoogleCloudPlatform/service-extensions/blob/main/callouts/python/extproc/example/jwt_auth/service_callout_example.py)
<details>
<summary>※4 忙しいけれど試してみたい人のためのこの記事を書くに当たって実施した手順メモ</summary>

##### インフラリソースを用意する

検証時はGCEを利用しました。
GCEを以下の設定をしてアプリケーションを作成します。

* イメージ
  * Debian GNU/Linux 12
* ファイアウォール
  * HTTP トラフィック: オン
  * HTTPS トラフィック: オン

SSHログインして以下のコマンドを実行します。

```shell
sudo su -
apt-get update
apt install git python-full python3-pip
git clone https://github.com/GoogleCloudPlatform/service-extensions.git
BIN="/usr/local/bin" && \
VERSION="1.47.2" && \
curl -sSL \
"https://github.com/bufbuild/buf/releases/download/v${VERSION}/buf-$(uname -s)-$(uname -m)" \
-o "${BIN}/buf" && \
chmod +x "${BIN}/buf"
```

作成したGCEのstartup-scriptに以下を設定する。

```shell
cd ~/service-extensions/callouts/python
python -m venv env
source env/bin/activate
pip install -r requirements.txt
buf -v generate \
  https://github.com/envoyproxy/envoy.git#subdir=api \
  --path envoy/service/ext_proc/v3/external_processor.proto \
  --include-imports
python3 -m pip install ./protodef
python3 -m extproc.example.redirect.service_callout_example
```

GCEインスタンスグループに以下の設定をして作成する。

* Select VMs
  * 上記で作成したGCE
* ポート マッピング
  * http: 80
  * grpc: 443

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

CLBのバックエンドサービスとして以下の設定をして作成する。

* バックエンドサービス: Global backend service
* エンドポイント プロトコル: HTTP/2
* 名前付きポート: grpc

##### Service Extensionに拡張機能としてバックエンドサービスを紐づける

CLBの拡張機能として以下を設定する。

* ロードバランサ タイプ: グローバル外部アプリケーション ロードバランサ
* 一致条件: request.host == "example.com"
* 権限: 適当(xxxxxとかの文字列)
* バックエンド サービス: 上記で追加したバックエンドサービスを選択する

##### 検証用リクエスト

Service Extensionあり

```bash
curl -H 'Host:example.com' ${CLB_IP}
```

Service Extensionなし

```bash
curl ${CLB_IP}
```

</details> 

## 参考資料

* [サービス拡張機能のドキュメント - https://cloud.google.com/service-extensions/docs](https://cloud.google.com/service-extensions/docs)
* [Media CDN のドキュメント - https://cloud.google.com/media-cdn/docs?hl=ja](https://cloud.google.com/media-cdn/docs?hl=ja)
* [Lambda@Edge を利用した外部サーバによる認可の実装 - https://aws.amazon.com/jp/blogs/news/external-server-authorization-with-lambdaedge/](https://aws.amazon.com/jp/blogs/news/external-server-authorization-with-lambdaedge/)
* [Amazon CloudFront ドキュメント - https://docs.aws.amazon.com/cloudfront/](https://docs.aws.amazon.com/cloudfront/)

# 宣伝

株式会社オプティマインドでは、一緒に働く仲間を大募集中です。
カジュアル面談も大歓迎ですので、気軽にお声がけください。

https://recruit.optimind.tech/#join-us