Google のグローバルなロードバランサーとサーバーレスサービスの色々な組み合わせ

Seiji Ariga
google-cloud-jp
Published in
33 min readJul 13, 2020
ベルギーにある Google のデータセンターラックの写真

先の記事では Serverless NEG の紹介をし、その中で Cloud Run のサービスに HTTP(S) Load Balancing 経由でアクセスする例をお見せしましたが、本記事ではもう少し色々な構成を試してみたいと思います。

EDIT(2020–10–14): 以下すべて beta でのご提供のタイミングでの内容となっているため、コマンドラインでの説明(かつ “beta” の修飾付き)ですが、2020年10月14日に GA になっており、Cloud Console (ウェブUI)による設定も可能になっています。

目次

  • サンプルアプリの用意
  • ロードバランサーの準備
  • マルチリージョン構成
  • URL ベースルーティング
  • URL マスクを使ってみる
  • マネージド証明書を使ってみる
  • Cloud Armor と一緒に使ってみる
  • コンソールと gcloud の合せ技で作成してみる
  • 落ち穂拾い
  • まとめ

サンプルアプリの用意

まず初めに、いくつかのリージョンにアプリを作っておきます。

初めに Cloud Functions のアプリをベルギー (europe-west1)に作ります。(Python の helloworld サンプルを流用します)

$ git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git
$ cd python-docs-samples/functions/helloworld/
$ sed -i '' 's/Hello World!/europe-west1\\n/' main.py # 出力を変更
$ gcloud functions deploy hello-function --runtime python37 --trigger-http --allow-unauthenticated --region europe-west1 --entry-point=hello_get
$ curl https://europe-west1-project-ongcp.cloudfunctions.net/hello-function
europe-west1

次に Cloud Run のアプリを米国のアイオワ(us-central1)に作ります。(Knative のサンプルコードを流用します)

$ git clone https://github.com/knative/docs.git
$ cd docs/docs/serving/samples/hello-world/helloworld-python/
$ sed -i '' -e 's/Hello {}!/us-central1/' app.py # 出力を変更
$ gcloud builds submit --tag gcr.io/project-ongcp/hello-run
$ gcloud run deploy --image gcr.io/project-ongcp/hello-run --platform managed --region us-central1 --allow-unauthenticated hello-run
$ curl https://hello-run-7525gqawya-uc.a.run.app
us-central1

リージョン名を表示するアプリが、Cloud Run、Cloud Functions で、それぞれアイオワ、ベルギーにできました。

アイオワの Cloud Run と、ベルギーの Cloud Function

ロードバランサーの準備

$ x="medium";
$ gcloud compute backend-services create --global ${x}-bs
$ gcloud compute url-maps create --default-service=${x}-bs ${x}-um
$ gcloud compute target-http-proxies create --url-map=${x}-um ${x}-tp
$ gcloud compute forwarding-rules create --target-http-proxy=${x}-tp --global --ports=80 ${x}-fr

この時点ではバックエンドは空っぽです。

マルチリージョン構成

Cloud Run や Cloud Functions はリージョナルなサービスのため、複数のリージョンのエンドユーザーを適切なリージョンにルーティングするのは面倒でした。HTTP(S) Load Balancing と Serverless NEG を組み合わせると、これが簡単にマルチリージョンのシステムを構成できます。

なお、App Engine は一つのプロジェクトで一つのリージョンにしか展開できないので対象外になります。

そこでこんな構成を作ってみたいと思います。

マルチサービスでマルチリージョン

では、さっそく Serverless NEG を作っていきます。

Cloud Functions 用$ gcloud beta compute network-endpoint-groups create function-neg --region=europe-west1 --network-endpoint-type=SERVERLESS --cloud-function-name=hello-function
Created [https://www.googleapis.com/compute/beta/projects/project-ongcp/regions/europe-west1/networkEndpointGroups/function-neg].
Created network endpoint group [function-neg].
Cloud Run 用$ gcloud beta compute network-endpoint-groups create run-neg --region=us-central1 --network-endpoint-type=SERVERLESS --cloud-run-service=hello-run
Created [https://www.googleapis.com/compute/beta/projects/project-ongcp/regions/us-central1/networkEndpointGroups/run-neg].
Created network endpoint group [run-neg].

作成した Serverless NEG をバックエンドサービスに追加していきます。

$ gcloud beta compute backend-services add-backend medium-bs --global --network-endpoint-group=function-neg --network-endpoint-group-region=europe-west1
Updated [https://www.googleapis.com/compute/beta/projects/project-ongcp/global/backendServices/medium-bs].
$ gcloud beta compute backend-services add-backend medium-bs --global --network-endpoint-group=run-neg --network-endpoint-group-region=us-central1
ERROR: (gcloud.beta.compute.backend-services.add-backend) Could not fetch resource:
- Invalid value for field 'resource.backends[1].group': 'https://compute.googleapis.com/compute/beta/projects/project-ongcp/regions/us-central1/networkEndpointGroups/run-neg'. Serverless NEGs with different serverless target types (e.g. Cloud Run and App Engine) cannot be backends of the same backend service.

おっと、エラーになりました。一つのバックエンドサービスに異なるサーバーレスサービスをターゲットとした Serverless NEG を追加することはできないようです。しようがないので、Cloud Run の代わりに Cloud Functions のアプリを東京にも作ることにします。

Cloud Run の代わりにもう一つ Cloud Functions を作成
$ cd python-docs-samples/functions/helloworld/
$ sed -i '' 's/europe-west1/asia-northeast1/' main.py # 出力を東京に
$ gcloud functions deploy hello-function --runtime python37 --trigger-http --allow-unauthenticated --region asia-northeast1 --entry-point=hello_get
$ gcloud beta compute network-endpoint-groups create function-neg --region=asia-northeast1 --network-endpoint-type=SERVERLESS --cloud-function-name=hello-function # リージョンが違えば名前は重複できる
Created [https://www.googleapis.com/compute/beta/projects/project-ongcp/regions/asia-northeast1/networkEndpointGroups/function-neg].
Created network endpoint group [function-neg].
$ curl https://asia-northeast1-project-ongcp.cloudfunctions.net/hello-function
asia-northeast1

再度、Serverless NEG を作ってバックエンドサービスに追加します。

$ gcloud beta compute network-endpoint-groups create function-neg --region=asia-northeast1 --network-endpoint-type=SERVERLESS --cloud-function-name=hello-function
Created [https://www.googleapis.com/compute/beta/projects/project-ongcp/regions/asia-northeast1/networkEndpointGroups/function-neg].
Created network endpoint group [function-neg].
$ gcloud beta compute backend-services add-backend medium-bs --global --network-endpoint-group=function-neg --network-endpoint-group-region=asia-northeast1
Updated [https://www.googleapis.com/compute/beta/projects/project-ongcp/global/backendServices/medium-bs].
$ gcloud compute forwarding-rules describe medium-fr --global --format="value(IPAddress)"
34.107.176.85

早速、アクセスして確認してみましょう。(設定が反映されるまで5分前後かかる場合があります。すぐにアクセスして HTTP 404 や HTTP 502 が返ってきた場合は少しおまちください。)

ベルギーリージョンに VM を作り、そこからアクセスしてみる$ gcloud compute instances create belgium --zone europe-west1-b
$ gcloud compute ssh belgium --zone europe-west1-b -- curl http://34.107.176.85/
europe-west1
同じく東京リージョンに VM を作り、そこからアクセスしてみる$ gcloud compute instances create tokyo --zone asia-northeast1-b
$ gcloud compute ssh tokyo --zone asia-northeast1-b -- curl http://34.107.176.85/
asia-northeast1

ご覧の通り、クライアントに近いリージョンのファンクションにルーティングされていることが分かるかと思います。これにより、リージョナルなサーバーレスアプリケーションも簡単にグローバルに展開できます。

URL ベースルーティング

上の例ではすべてのサービスが Cloud Functions にルーティングされますが、同じシステム内でも適材適所で使うサービスを変えたいと思うことがあると思います。そこでこんな構成を試してみます。

/status は Cloud Functions へ。それ以外は Cloud Run へ。

まずは Cloud Run 用にバックエンドサービスを作り、Cloud Run の Serverless NEG をバックエンドとして登録します。

$ gcloud compute backend-services create --global medium-run-bs
$ gcloud beta compute backend-services add-backend medium-run-bs --global --network-endpoint-group=run-neg --network-endpoint-group-region=us-central1

次に URL マップを変更し、デフォルトのバックエンドサービスを Cloud Run に変更、/status を Cloud Functions へ向けます。

$ cat url-map
hostRules:
- hosts:
- '*'
pathMatcher: path-matcher-1
kind: compute#urlMap
name: medium-um
pathMatchers:
- defaultService: https://www.googleapis.com/compute/v1/projects/project-ongcp/global/backendServices/medium-run-bs
name: path-matcher-1
pathRules:
- paths:
- /status
service: https://www.googleapis.com/compute/v1/projects/project-ongcp/global/backendServices/medium-bs
$ gcloud compute url-maps import medium-um --global --source=url-map
Url Map [medium-um] will be overwritten.
Do you want to continue (Y/n)? y

ではアクセスして構成を確かめてみます。

$ curl http://34.107.176.85/
us-central1
$ curl http://34.107.176.85/status
asia-northeast1
$ gcloud compute ssh belgium --zone europe-west1-b -- curl http://34.107.176.85/status
europe-west1

期待通りにルーティングされています。(“/status” は前のセクションの通り、クライアントに近い場所にルーティングされています。)

URL マスクを使ってみる

システムを複数のサービスから構成し、URL/パスで適切なサービスへルーティングするというのはよくあるやり方かと思います。HTTP(S) Load Balancing でのやり方の一つは、前項の「URL ベースルーティング」で見たように、パスごとにバックエンドサービスと Serverless NEG を用意し、URL マップでパスに割り当てる方法になります。。

しかし、この方法ではサービスが増えるたびに バックエンドサービスの作成、Serverless NEG の作成と登録、URL マップの更新が発生し煩雑です。

ただし、一つのバックエンドサービスには同一種類のサーバーレスサービスしか登録できないので、複数のサーバーレスサービスでシステムを構成したい場合は必然的に URL マップによるルーティングが必要になります。

そこで Serverless NEG の URL マスクという機能を使うと、もっとシンプルに構成できます。URL マスクではパスに含まれる文字列(+ドメイン名)と、サービス名をマップすることができ、サービス名ベースのルーティングができます。(Cloud Run、Cloud Functions、App Engine のいずれでも使えます)

それではさっそく試してみましょう。まずは、先のサンプルを使って、login サービスと logout サービスを Cloud Run で作っておきます。

$ cat app.py
(省略)
@app.route('/login')
def hello_world():
return 'login\n'
$ gcloud builds submit --tag gcr.io/project-ongcp/login
$ gcloud run deploy --image gcr.io/project-ongcp/login --platform managed --region us-central1 --allow-unauthenticated login
$ curl https://login-7525gqawya-uc.a.run.app/login
login
(logout サービスも同様に作成)

そして、URL マスクを使った Serverless NEG を作成します。(これまでとは違い “— cloud-run-url-mask” というオプションを使います)

$ gcloud beta compute network-endpoint-groups create run-mask-neg --region=us-central1 --network-endpoint-type=SERVERLESS --cloud-run-url-mask="/<service>"
Created [https://www.googleapis.com/compute/beta/projects/project-ongcp/regions/us-central1/networkEndpointGroups/run-mask-neg].
Created network endpoint group [run-mask-neg].

URL マスクを使った Serverless NEG と、前のセクションで使っていた NEG を入れ替え(“run-neg” を削除して、”run-mask-neg” を追加し)ます。

$ gcloud beta compute backend-services remove-backend medium-run-bs --global --network-endpoint-group=run-neg --network-endpoint-group-region=us-central1
Updated [https://www.googleapis.com/compute/beta/projects/project-ongcp/global/backendServices/medium-run-bs].
$ gcloud beta compute backend-services add-backend medium-run-bs --global --network-endpoint-group=run-mask-neg --network-endpoint-group-region=us-central1
Updated [https://www.googleapis.com/compute/beta/projects/project-ongcp/global/backendServices/medium-run-bs].

それではアクセスしてみます。

$ curl http://34.107.176.85/login
login
$ curl http://34.107.176.85/logout
logout

期待通りにルーティングされることを確認できました。

Cloud Run ではサービス名に加えてタグ名、App Engine ではサービス名とバージョンを指定できますので、かなり柔軟なルーティングを実現いただけるかと思います。詳細はドキュメントにある例を参照ください。

最後に、この後のためにバックエンドの Serverless NEG を URL マスクを使ってないものに戻しておきます。

$ gcloud beta compute backend-services remove-backend medium-run-bs --global --network-endpoint-group=run-mask-neg --network-endpoint-group-region=us-central1
$ gcloud beta compute backend-services add-backend medium-run-bs --global --network-endpoint-group=run-neg --network-endpoint-group-region=us-central1

マネージド証明書を使ってみる

Cloud Run ならサービスごとにカスタムドメインを設定してマネージドの証明書が使えますし、App Engine もカスタムドメインの設定とマネージド証明書の利用が可能です。しかし、いずれの場合もサービスやアプリと直接紐付いてしまいます。

HTTP(S) Load Balancing と Serverless NEG を使い、各サーバーレスアプリケーションをロードバランサーのバックエンドとして位置づけることで、HTTP(S)の終端とそれに必要な SSL 証明書の管理をロードバランサーに任せ、各サーバーレスアプリケーションはサービスを提供することに集中できるのです。

というわけで、Serverless NEG とは直接関係ないですが、HTTP(S) Load Balancing でマネージド証明書をさくっと設定してみましょう。クラウドコンソールも使えますが、せっかくなので gcloud コマンドでさっとこんな構成を作っていきます。

HTTP のみだったロードバランサーを HTTPS 対応に

なお、ここではマネージド証明書の紹介だけしてますが、ユーザーが証明書を持ち込んで使うことももちろんできます。

初めにマネージド証明書を作成します。(この後の手順を全部終えると証明書の発行が完了します。)

$ gcloud compute ssl-certificates create medium-demo-certificate --description="demo certificate for medium article" --domains=medium.nikai.date --global
Created [https://www.googleapis.com/compute/v1/projects/project-ongcp/global/sslCertificates/medium-demo-certificate].
NAME TYPE CREATION_TIMESTAMP EXPIRE_TIME MANAGED_STATUS
medium-demo-certificate MANAGED 2020-07-10T02:32:07.529-07:00 PROVISIONING
medium.nikai.date: PROVISIONING

Cloud DNS で IP アドレス に証明書で指定した名前をつけます。

$ gcloud dns record-sets transaction start --zone=nikai-date
Transaction started [transaction.yaml].
$ gcloud dns record-sets transaction add --name=medium.nikai.date "34.107.176.85" --type=A --ttl=1 --zone=nikai-date
Record addition appended to transaction at [transaction.yaml].
$ gcloud dns record-sets transaction execute --zone=nikai-date
Executed transaction [transaction.yaml] for managed-zone [nikai-date].
Created [https://dns.googleapis.com/dns/v1/projects/project-ongcp/managedZones/nikai-date/changes/55].
ID START_TIME STATUS
55 2020-07-10T09:35:27.465Z pending
$ host medium.nikai.date
medium.nikai.date has address 34.107.176.85

作成した証明書を使って、HTTPS Load Balancing に仕立てます。

$ gcloud compute target-https-proxies create medium-ssl-tp --ssl-certificates=medium-demo-certificate --url-map=medium-um
Created [https://www.googleapis.com/compute/v1/projects/project-ongcp/global/targetHttpsProxies/medium-ssl-tp].
NAME SSL_CERTIFICATES URL_MAP
medium-ssl-tp medium-demo-certificate medium-um
ここまで使ってきた IP アドレスを予約アドレスに変換します。$ gcloud compute addresses create medium-ip --global --addresses=34.107.176.85
Created [https://www.googleapis.com/compute/v1/projects/project-ongcp/global/addresses/medium-ip].
$ gcloud compute forwarding-rules create medium-ssl-fr --address=medium-ip --target-https-proxy=medium-ssl-tp --global --ports=443
Created [https://www.googleapis.com/compute/v1/projects/project-ongcp/global/forwardingRules/medium-ssl-fr].

しばらくすると証明書の発行が完了します。ただし、実際に HTTPS でアクセスできるようになるには追加でもう少し時間がかかるので辛抱してください。

$ gcloud compute ssl-certificates describe medium-demo-certificate --format="get(managed.domainStatus)"
medium.nikai.date=ACTIVE
$ curl https://medium.nikai.date/
curl: (35) error:14004410:SSL routines:CONNECT_CR_SRVR_HELLO:sslv3 alert handshake failure
しばらく待つと、無事 Cloud Run アプリからの応答が得られました。$ curl https://medium.nikai.date/
us-central1

Cloud Armor と一緒に使ってみる

Cloud Armor とは Google Cloud の提供するサービスで、HTTP(S) Load Balancing と一緒に使い、IP アドレスや位置情報にもとづくアクセス制御、エンタープライズクラスの DDoS 対策、SQLインジェクションやXSSなどの OWASP 10大リスクを軽減する WAF 機能などを提供します。

これらの機能はこれまで Cloud Run、App Engine、Cloud Functions と一緒に使うことはできませんでしたが、Serverless NEG によって使えるようになりました。

Cloud Armor の利用自体は非常に簡単で、ポリシーを作ってロードバランサー(バックエンドサービス)にアタッチするだけです。

試しに、日本からのアクセスを拒否してみましょう。今は普通にアクセスできています。

$ curl https://medium.nikai.date/
us-central1

Cloud Armor の設定はシンプルで、ルールを記述してロードバランサーにアタッチするだけです。例として「origin.region_code == “JP”」というポリシーを記述、ブロックしてみています。

Cloud Armor の設定画面

では、再度アクセスしてみましょう。(ポリシーの設定が完了してから有効になるまで少し時間がかかります)

$ curl https://medium.nikai.date/
<!doctype html><meta charset="utf-8"><meta name=viewport content="width=device-width, initial-scale=1"><title>403</title>403 Forbidden

無事(?)アクセスがブロックされたました。

なお、GCE インスタンスからのアクセスは、そのインスタンスがあるリージョンの国からのアクセスと認識されない場合があるので気をつけてください。

ところで、こっちのアクセスはブロックされません。

$ curl https://medium.nikai.date/status
asia-northeast1

これは、Cloud Armor がバックエンドサービス単位で有効化されており、先に設定した「/status」を担ってるバックエンドサービス(“medium-bs”)には Cloud Armor を設定してないためです。

コンソールと gcloud の合せ技で作成してみる

先の記事でクラウドコンソールは未対応と書きましたが、Serverless NEG を作るところと、その Serverless NEG をバックエンドサービスに追加するところ以外は普通のロードバランサーの作成と同じです。

そこで最後にオマケとして、コンソールでロードバランサーの本体を作って、そこに gcloud コマンドで作った Serverless NEG を追加してみます。

まず初めにクラウドコンソールで最小限の構成でロードバランサーを作成してみます。「バックエンドタイプ」として「Instance group」を選びながら、「バックエンド」は空の状態でバックエンドサービスを作ります。

最小限の構成のロードバランサー (構成確認画面)

そして、先の記事とは別の名前(“hello-app-serverless-neg2")で Serverless NEG を作ります。ここは gcloud コマンドでしか対応していません。

$ gcloud beta compute network-endpoint-groups create hello-app-serverless-neg2 --region=asia-northeast1 --network-endpoint-type=SERVERLESS --cloud-run-service=hello-app
Created [https://www.googleapis.com/compute/beta/projects/project-ongcp/regions/asia-northeast1/networkEndpointGroups/hello-app-serverless-neg2].
Created network endpoint group [hello-app-serverless-neg2].

クラウドコンソールで作ったバックエンドサービス(“hello-app-bs”)にこの Serverless NEG を追加してみます。

$ gcloud beta compute backend-services add-backend hello-app-bs --global --network-endpoint-group=hello-app-serverless-neg2 --network-endpoint-group-region=asia-northeast1
ERROR: (gcloud.beta.compute.backend-services.add-backend) Could not fetch resource:
- Invalid value for field 'resource.healthChecks': ''. A backend service cannot have a healthcheck with Serverless network endpoint group backends.

おっと、エラーです。ヘルスチェックが設定されてるとダメのようです。(クラウドコンソールではヘルスチェックの設定は必須でした) ヘルスチェックを削除してから再度追加すると、今度は成功しました。

$ gcloud beta compute backend-services add-backend hello-app-bs --global --network-endpoint-group=hello-app-serverless-neg2 --network-endpoint-group-region=asia-northeast1
Updated [https://www.googleapis.com/compute/beta/projects/project-ongcp/global/backendServices/hello-app-bs].

実際にアクセスしてみると、問題なく応答があることが分かります。

$ gcloud compute forwarding-rules describe hello-app-fe --global --format="value(IPAddress)"
34.107.176.85
$ curl http://34.107.176.85/
Hello, world!
Version: 1.0.0
Hostname: localhost

落ち穂拾い

ここまで Serverless NEG で実現できることを色々と紹介してきましたが、残念ながら今のところいくつか惜しいところがあります。

  • Cloud Functions、Cloud Run をバックエンドとした場合、IAP (Identity-Aware Proxy)は今のところ利用できません。(App Engine の場合には利用できますが、App Engine はもともと IAP を利用できます)
  • Cloud Run、App Engine のサービスにはデフォルトで run.appappspot.com といったドメインの URL が付与されますが、これらの URL へのアクセスを一括して拒否することはできません。そのため、Serverless NEG を利用した場合、HTTP(S) Load Balancing 経由とで2つのアクセス経路ができてしまいます。今のところ、アプリ側で URL にもとづいて応答を変えるのがワークアラウンドです。

Cloud Functions では cloudfunctions.net 経由でのアクセスを一括して制限することができます

まとめ

Cloud Run、App Engine、Cloud Functions はサーバーレスアプリケーションの開発プラットフォームとして多くご利用いただいてますが、これまでは独立したサービスとして動かされていました。

それが、Serverless NEG を使うことで有機的に結合することができるようになり、さらには HTTP(S) Load Balancing のスケーラブルでグローバルかつ豊富な機能の恩恵が受けられるようになりました。

ぜひ、一度お試しいただき、今後の開発に役立てていただけると幸いです。💁🏻‍♂️

--

--

Seiji Ariga
google-cloud-jp

Customer Engineer, Network Specialist, Google Cloud (All views and opinions are my own.)