grasys blog

Cloud Run で Direct VPC Egress を試してみた

初めまして、2024年1月に入社した yoshida です。

Google Cloud Next ’24 で Cloud Run に関する最新情報がいくつか紹介されました。

その中でも Direct VPC Egress が GA( General Availability:一般公開の意) したとのことで、様々な方法で Cloud Run からリソースにアクセスする方法を試してみたいと思います。

Cloud Run とは

Google Cloud が提供する、サーバーレスでコンテナを実行することができるマネージドサービスです。

サーバーの構成やスケーリング管理が不要で、デプロイコマンドを実行するだけで簡単に外部に公開されたアプリケーションを作成することができます。

詳細は 公式ドキュメント を参照しましょう。

事前準備

  • デプロイに必要な権限を用意します
  • Docker で Flask アプリケーションを作成します
  • コンテナイメージを Artifact Registry で管理します
  • 検証に必要なリソースを Terraform で作成します

必要な権限を付与する

公式ドキュメント を参考にデプロイに必要な権限を用意します。

MY_PROJECTMY_USER_ACCOUNT は環境に合わせて設定しましょう。

$ gcloud projects add-iam-policy-binding MY_PROJECT \
  --member=user:MY_USER_ACCOUNT \
  --role=roles/run.developer
$ gcloud projects add-iam-policy-binding MY_PROJECT \
  --member=user:MY_USER_ACCOUNT \
  --role=roles/iam.serviceAccountUser

Flask アプリケーションを作成する

  • ディレクトリ構成
.
├── Dockerfile
└── app
      ├── __init__.py
      ├── main.py
      └── requirements.txt
  • Dockerfile
FROM --platform=linux/amd64 python:3.12-alpine

WORKDIR /code/app
ENV FLASK_APP=app

COPY /app .

RUN apk add --update

RUN pip install --upgrade pip
RUN pip install -r requirements.txt

ENV PORT 8080
EXPOSE 8080

CMD ["python", "main.py"]
  • requirements.txt
flask
redis
  • main.py
import os
import redis
from flask import Flask

app = Flask(__name__)

redis_host = os.environ.get("VPC_REDISHOST", "localhost")   # 後々変更します
redis_port = int(os.environ.get("VPC_REDISPORT", 6379))     # 後々変更します

redis_client = redis.StrictRedis(host=redis_host, port=redis_port)

@app.route("/")
def index():
    return {"Hello": "World"}

@app.route("/redis")
def redis_count():
    value = redis_client.incr("counter", 1)
    return f"Visitor number: {value}"

if __name__ == "__main__":
    app.run(debug=True, host="0.0.0.0", port=int(os.environ.get("PORT", 8080)))

コンテナイメージをビルドします

$ docker build . --no-cache

Artifact Registry でコンテナイメージを管理する

リポジトリを作成し、ビルドしたコンテナイメージを push します。

Artifact Registry に push する際に必要なタグの形式は以下です。

タグの形式:<REGION>-docker.pkg.dev/<PROJECT_NAME>/<REPOSITORY_NAME>/<IMAGE_NAME>:<TAG_NAME>

$ gcloud artifacts repositories create run-repo \
  --repository-format=docker \
  --location=asia-northeast1

$ docker tag <IMAGE_ID> asia-northeast1-docker.pkg.dev/<PROJECT_NAME>/run-repo/flask:test
$ docker push asia-northeast1-docker.pkg.dev/<PROJECT_NAME>/run-repo/flask:test

Terraform で検証に必要なリソースを作成する

今回の検証では VPC から Redis に接続を試していきます。

必要なリソースを Terraform で作成します。

resource "google_compute_network" "vpc" {
  name                    = "run-vpc"
  auto_create_subnetworks = false
}
resource "google_compute_subnetwork" "subnet" {
  name          = "run-subnet"
  ip_cidr_range = "10.5.0.0/24"
  region        = "asia-northeast1"
  network       = google_compute_network.vpc.self_link

  log_config {
    aggregation_interval = "INTERVAL_10_MIN"
    flow_sampling        = 0.5
    metadata             = "INCLUDE_ALL_METADATA"
  }
}
resource "google_compute_subnetwork" "connector" {
  name          = "run-connector-subnet"
  ip_cidr_range = "10.15.0.0/28"
  region        = "asia-northeast1"
  network       = google_compute_network.vpc.name

  log_config {
    aggregation_interval = "INTERVAL_10_MIN"
    flow_sampling        = 0.5
    metadata             = "INCLUDE_ALL_METADATA"
  }
}
resource "google_compute_subnetwork" "direct-vpc" {
  name          = "run-direct-vpc-egress"
  ip_cidr_range = "10.8.0.0/24"
  region        = "asia-northeast1"
  network       = google_compute_network.vpc.name

  log_config {
    aggregation_interval = "INTERVAL_10_MIN"
    flow_sampling        = 0.5
    metadata             = "INCLUDE_ALL_METADATA"
  }
}

resource "google_redis_instance" "cache" {
  name               = "run-memory-cache"
  memory_size_gb     = 1
  authorized_network = google_compute_network.vpc.name
  lifecycle {
    prevent_destroy = true
  }
}

Cloud Run にデプロイ

  • Cloud Run にデプロイします
$ gcloud run deploy run-service \
  --image=asia-northeast1-docker.pkg.dev/<PROJECT_NAME>/run-repo/flask:test \
  --no-allow-unauthenticated \
  --port=8080

Deploying container to Cloud Run service [run-service] in project [<PROJECT_NAME>] region [asia-northeast1]
✓ Deploying new service... Done.
  ✓ Creating Revision...
  ✓ Routing traffic...
Done.
Service [run-service] revision [run-service-00001-fbr] has been deployed and is serving 100 percent of traffic.
Service URL: https://<RUN_SERVICE_URL>

これだけでアプリケーションが公開されます。

公開アクセスを許可する場合は --no-allow-unauthenticated ではなく --allow-unauthenticated を指定します。

最後の行に、アクセスするための URL が表示されています。

Service URL: https://<RUN_SERVICE_URL>

正常にデプロイされ、アクセスできることを確認しましょう。

$ curl --header "Authorization: Bearer $(gcloud auth print-identity-token)" https://<RUN_SERVICE_URL>
{"Hello":"World"}%

VPC にトラフィックを送信して他のリソース(Redis)に接続する

Cloud Run は VPC ネットワークの外に作成されます。

デフォルトの設定では VPC 内部のリソースに内部 IP でアクセスすることができません。

VPC 内に Redis を用意して、様々な方法で接続できることを試していきます。

Serverless VPC access connector で Cloud Run から Redis に接続する

まず最初に Serverless VPC Access という機能が追加され、Cloud Run、App Engine、Cloud Functions などのサーバーレス環境から VPC ネットワークに直接接続できる様になりました。

主な利点は以下の通りです。

  • リクエストがインターネットに公開されないこと
  • インターネット経由に比べてレイテンシが低くなること

Serverless VPC access connector と呼ばれるインスタンスを作成し、このインスタンスを経由することで接続が可能になります。

$ gcloud compute networks vpc-access connectors create run-connector \
  --subnet run-connector-subnet \
  --min-instances 2 \ #設定可能な値は2~9
  --max-instances 3 \ #設定可能な値は3~10
  --machine-type f1-micro

コネクタにスケーリングの最小値、最大値を設定することができ、トラフィック量に応じて自動でスケールアウトします。

Serverless VPC access connector を使用するように設定を更新します。

--set-env-vars で設定している VPC_REDISHOSTVPC_REDISPORT は作成した Redis インスタンスのエンドポイントを参照します。

$ gcloud run services update run-service \
  --vpc-connector=run-connector \
  --set-env-vars VPC_REDISHOST=<PRIVATE_IP>,VPC_REDISPORT=<PORT>

Redis にアクセスできることを確認します。

$ curl --header "Authorization: Bearer $(gcloud auth print-identity-token)" https://<RUN_SERVICE_URL>/redis
Visitor number: 1

問題点

  • コネクタに最小インスタンス、最大インスタンスを設定しましたが、一度スケールアウトしてしまうと自動でスケールインされない
  • スケールインする場合はコネクタの削除、再作成が必要
  • インスタンスの起動時間に応じて料金が課金される

せっかくスケーリング管理が不要な Cloud Run を使うのに、コネクタを手動管理するのは手間ですね。

Serverless VPC access connector を使う場合は Cloud Monitoring でインスタンスを監視するなどして、気づいたら最大インスタンスになっていた、といったことがないように注意しましょう。

Direct VPC egress で Cloud Run から Redis に接続する

2024/4/24に Direct VPC egress が GA されました。

Direct VPC egress を有効にすると、コネクタを経由せずに直接 VPC にトラフィックを送信できるようになります。

オプション:network,subnet,vpc-egress で Direct VPC egress に必要な項目を設定します。

--clear-vpc-connector は Serverless VPC access connector をデタッチするためのオプションです。

もし設定していなければこちらのオプションは不要です。

$ gcloud run services update run-service \
  --clear-vpc-connector \
  --network=yoshida-vpc \
  --subnet=yoshida-direct-vpc-egress \
  --vpc-egress=private-ranges-only

Direct VPC egress でも Redis にアクセスできることを確認します。

$ curl --header "Authorization: Bearer $(gcloud auth print-identity-token)" https://<RUN_SERVICE_URL>/redis
Visitor number: 2

ちなみにプレビュー版の時点では、Direct VPC egress は Cloud NAT をサポートしておらず、アップデートが期待されていました。

2024/3/14のリリース で正式にサポートされ、Direct VPC egress でも外部 IP アドレスを固定して外部インターネットへ接続できるようになりました。

問題点

  • サブネットの CIDR は/24か、それ以上である必要があり、Cloud Runのインスタンス数の4倍を予約しておく必要がある
  • リビジョン更新時、最大20分間は IP アドレスを保持するため、Cloud Run のインスタンス数が増減する更新をした場合、大量の IP アドレスを用意しておく必要がある
  • サービスごとに100インスタンスまでのサポートとなっているのでそれ以上にスケールアウトした場合、動作が保証されない
  • 一部指標やログで動かないものがある

IP アドレスが枯渇しているとスケールアウトができなくなりますので余裕を持って確保しておきましょう。

100以上にスケールする必要がある場合は Quotas で割り当てを調整しましょう。

詳細は 公式ドキュメント を確認しましょう。

(寄り道)Cloud Run integrations で Cloud Run から Redis に接続する

現状(2024/4時点)はプレビューですが、統合( Integrations )機能が用意されており、Memorystore、Firebase Hosting など一部のリソースであればもっと簡単に接続が可能です。

試しに Redis を作成して接続まで確認していきます。

前項の Direct VPC egress の設定は解除します。

Integrations 機能で Redis を作成すると、接続情報として環境変数 REDISHOST と REDISPORT が自動で追加されます。

コード内でこちらの環境変数を参照できるようにアプリケーションを更新します。

Python アプリケーションの main.py を修正します。

redis_host = os.environ.get("REDISHOST", "localhost")
redis_port = int(os.environ.get("REDISPORT", 6379))

コンテナを再ビルドし、Cloud Run にデプロイします。

$ docker build . --no-cache
$ docker tag <IMAGE_ID> asia-northeast1-docker.pkg.dev/<PROJECT_NAME>/run-repo/flask:test
$ docker push asia-northeast1-docker.pkg.dev/<PROJECT_NAME>/run-repo/flask:test
$ gcloud run deploy run-service \
  --image=asia-northeast1-docker.pkg.dev/<PROJECT_NAME>/run-repo/flask:test \
  --no-allow-unauthenticated \
  --port=8080 \
  --clear-network

CLI を最新版にしましょう

gcloud components update

Integrations 機能で Redis を作成します

gcloud beta run integrations create \
  --type=redis \
  --service=run-service \
  --parameters=memory-size-gb=1 \
  --service-account=<SERVICE_ACCOUNT>

--parameters を指定できますが、現状(2024/4時点)はメモリサイズしか設定できません。
設定可能な項目は下記で確認できます。

$ gcloud beta run integrations types describe redis
Configure a Redis instance (Cloud Memorystore) and connect it to a Cloud Run Service.

Parameters:
  memory-size-gb [optional]:
    Memory capacity of the Redis instance.

Example Usage:
  $ gcloud beta run integrations create --service=[SERVICE] --type=redis --parameters=memory-size-gb=2

接続できることを確認しましょう。

Serverless VPC access connector、Direct VPC egress の時とは違う Redis を参照しているのでインクリメントがリセットされています。

$ curl --header "Authorization: Bearer $(gcloud auth print-identity-token)" https://<RUN_SERVICE_URL>/redis
Visitor number: 1

注意事項

  • 作成された Redis は認証ネットワークは default、接続モードは Direct peering で作成されます
  • 認証ネットワーク、接続モードを指定してインスタンスの作成はできません
  • 作成された Redis には Direct VPC egress で default ネットワーク経由で接続を行います
  • Memorystore for Redis は既存のインスタンスの認証ネットワークや接続モードを切り替えることができません 

公式ドキュメント を参照

そのため

  • インテグレーションで作成したインスタンスに Private service access で接続することができません
  • 任意の VPC ネットワークを指定した接続ができません

GA 時に解消されていると良いですね。

まとめ

VPC ネットワーク経由でリソースにアクセスする方法を試してみました。

では Serverless VPC access と Direct VPC egress どちらを選択すれば良いのでしょうか。

  • ネットワークを経由せず、直接接続を行うので Direct VPC egress の方がパフォーマンスが向上します
  • インスタンスを作成する必要がないので Direct VPC egress の方が費用を抑えることが期待できます

プレビュー時点では

  1. Cloud Nat が使えない
  2. 一部リージョンが非対応
  3. 一部指標やログが動かない

といった問題があり、Serverless VPC access を選択する必要もありましたが、1と2は解消しているため、3の要件でどちらを選択するかを検討すれば良いのではないでしょうか。

各サービスの特徴を把握して、最適なサービスを選択できるようにしましょう。


採用情報
お問い合わせ