株式会社ブレインパッド プロダクトユニットの岡です。
弊社は「データ活用の促進を通じて持続可能な未来をつくる」をミッションに、
データ分析支援やSaaSプロダクトの提供を通じて、企業の「データ活用の日常化」を推進しております。
私は、SaaSプロダクトを開発・提供する「プロダクト・サービス」部門に所属し、
企業のマーケティング活動をデータで支援する「Rtoaster GenAI」の開発を担当しております。
はじめに
WebサービスやAPIサーバー、マイクロサービスなど様々な場面で利用されているCloud Runにおいて、同じプライベートネットワーク内のサービス間通信を内部通信のみで実現する方法について書きます。具体的に、Cloud Load Balancing(Google Cloudが提供するサービス)のリージョン内部ロードバランサを用いてCloud Run同士のサービス間通信を実現します。
対象読者
- 内部通信でサービス間通信を実現したい
- Cloud Runの利用を検討している
- ロードバランサの使用例の実装例を探している
VPCを作成
はじめに内部通信用のVPCを作成します。VPCを作成したらサブネットも作成します。IPアドレス範囲はRFC1918から適当な範囲を選択して作ります。(Google CloudではRFC1918以外に他のアドレス空間も使えるそう)

2つのサービス用意
1つ目のサービスをclient-service、2つ目のサービスをserver-serviceとします。client-serviceからserver-serviceを呼び出します。中身のアプリケーションはWebフレームワークを使って実装したものなど適当に準備します。
Cloud Runにデプロイすると、そのサービスには https://{service_name}-{project_number}.{region}.run.appというURLが付与されます。

2つのサービス間通信の挙動を見てみる
client-serviceから自動で付与されたserver-serviceのURLを用いてアクセスしてみます。
以下がコード例の一部です。
server_url = request.server_url or DEFAULT_SERVER_URL
try:
logger.info(f"Sending message to server at {server_url}: {request.message}")
async with httpx.AsyncClient(timeout=30.0) as client:
response = await client.post(
server_url,
json={
"message": request.message,
"sender": client_id
}
)
...
そしてあらかじめ仕込んでおいたclient-serviceのログを見てみると、server-serviceにリクエストしていることが確認できます。

しかしserver-serviceのネットワーク設定を見てみると、公開設定になっていることがわかります。つまり、インターネットを経由してアクセスしていることになります。

この設定を「内部」に変更して再度リクエストを送信すると404エラーになります。server-serviceのIngress設定を内部に変更することで、client-serviceからserver-serviceへの通信ができなくなりました。つまり、元々の通信経路は外部を通っていたことがわかります。

この後内部通信できるように内部向けロードバランサを導入します。
ロードバランサの実装
client-serviceとserver-serviceの間にロードバランサを配置し、client-serviceのリクエストをロードバランサに向けることで、server-serviceへのリクエストをできるようにします。
ロードバランサの追加は具体的にフォワーディングルールやバックエンドサービスなどいくつかの設定が必要になります。細かい設定はGoogle Cloudのドキュメントに記載されている図1がわかりやすいのでそちらを参考にしてください。
Terraformの実装例は以下です。
# Internal Load Balancer
resource "google_compute_address" "internal_lb_ip" {
name = "${var.internal_lb_name_prefix}-ip"
region = var.region
address = var.internal_lb_ip_address
address_type = "INTERNAL"
subnetwork = google_compute_subnetwork.test_subnet.id
}
resource "google_compute_region_target_http_proxy" "internal_lb_target" {
name = "${var.internal_lb_name_prefix}-target-proxy"
url_map = google_compute_region_url_map.internal_lb_url_map.id
region = var.region
}
resource "google_compute_region_url_map" "internal_lb_url_map" {
name = "${var.internal_lb_name_prefix}-url-map"
default_service = google_compute_region_backend_service.internal_lb_backend.id
region = var.region
}
# Forwarding Rule
resource "google_compute_forwarding_rule" "internal_lb_frontend" {
name = "${var.internal_lb_name_prefix}-frontend"
target = google_compute_region_target_http_proxy.internal_lb_target.id
region = var.region
ip_protocol = "TCP"
port_range = "80"
ip_address = google_compute_address.internal_lb_ip.address
load_balancing_scheme = "INTERNAL_MANAGED"
subnetwork = google_compute_subnetwork.test_subnet.id
}
# Backend Service
resource "google_compute_region_backend_service" "internal_lb_backend" {
name = "${var.internal_lb_name_prefix}-backend-svc"
protocol = "HTTP"
port_name = "http"
load_balancing_scheme = "INTERNAL_MANAGED"
timeout_sec = 30
enable_cdn = false
backend {
group = google_compute_region_network_endpoint_group.internal_lb_neg.id
balancing_mode = "UTILIZATION"
capacity_scaler = 1.0
}
log_config {
enable = true
sample_rate = 1.0
}
locality_lb_policy = "ROUND_ROBIN"
session_affinity = "NONE"
connection_draining_timeout_sec = 0
}
# Network Endpoint Group (Cloud Run)
resource "google_compute_region_network_endpoint_group" "internal_lb_neg" {
name = "${var.internal_lb_name_prefix}-neg"
network_endpoint_type = "SERVERLESS"
region = var.region
cloud_run {
service = var.internal_lb_service_name
}
}
# Security Policy
resource "google_compute_security_policy" "internal_lb_security_policy" {
description = "Backend security policy for Internal Load Balancer"
name = "${var.internal_lb_name_prefix}-security-policy"
adaptive_protection_config {
layer_7_ddos_defense_config {
enable = false
}
}
rule {
action = "allow"
priority = "2147483647"
description = "Default allow rule"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
}
lifecycle {
ignore_changes = [
rule
]
}
}
ロードバランサ経由で通信する
最後に、client-serviceのリクエスト先をロードバランサにして通信が成功することを確認します。
以下のスクリーンショットのように、追加したロードバランサのIPアドレスに向けてリクエストすることで通信できることがわかります。

まとめ
Cloud Run のサービス間通信はデフォルトで外部経由となるため、Ingress を「内部」にすると相互通信ができなくなります。
これを解決するには、Cloud Run(Serverless NEG)をバックエンドにした L7 リージョン内部ロードバランサを経由させる構成が有効です。
ポイントは以下になります。
- Cloud Run はそのままで内部通信できない
- Serverless NEG を内部ロードバランサに紐付けることで、同一 VPC 内で閉じた通信が可能になる
- client-service → 内部ロードバランサ → server-service の経路でリクエストが流れる
- Terraform でロードバランサ構成を定義できる