PSCを用いたプロジェクト・VPCをまたいだCloudRun同士の内部通信

# 概要
PSC(Private Service Connect)を利用することで、GCP上でプロジェクト・VPCをまたいだ内部通信をすることができます。今回は、プロジェクトをまたいだCloudRunからCloudRunへの通信を可能とする構成とそのTerraform定義を紹介します。

# 構成
今回は以下のような構成を作りたいと思います。ProjectAにAPIを提供するCloudRunを配置し、ProjectBにそれを参照するClientアプリのCloudRunを配置する構成となっています。

![qiita-psc.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/74514/f8a7a829-1378-5b6b-0da5-82fe262efe74.png)

CloudRunからVPCへ通信する場合はVPC Connectorを利用するか、Direct VPCを利用する方法があります。今回はVPC Connectorを利用する構成とします。また、PSCのクライアント側の構成としてはPSC Backendと PSC Endpointがありますが、今回はPSC Endpointを利用した構成とします。

CloudRunからVPCへの接続については以下をご参照ください。

https://cloud.google.com/run/docs/configuring/connecting-vpc

またPSCについては以下をご参照ください。

https://cloud.google.com/vpc/docs/private-service-connect

## 構成についての補足
### ProjectAにおけるサブネット
今回ProjectAにおいて、3つのSubnetを用意しています。１つはLoadBalancerを配置しているSubnetです。このSubnetは普通のSubnetなのですが、その他２つは特殊なサブネットとなります。Subnetを作成するときにpurposeを指定することで特殊なSubnetを作ることができます。どんな種類があるかは以下のTerraformプロバイダのページが参考になります。

https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_subnetwork.html#purpose-1

今回はProjectBにおいて、2つの特殊なサブネットを用意しています。１つはPSC Attachment専用のSubnetです。これはAttachmentにつき１つ作る必要があります。purposeは `PRIVATE_SERVICE_CONNECT`を指定します。

もう１つはregional managed proxy用のサブネットになります。これは、EnvoyベースのLBを利用する場合に用意しておく必要があります。purposeは `REGIONAL_MANAGED_PROXY` を指定します。詳しくは以下をご参照ください。

https://cloud.google.com/load-balancing/docs/proxy-only-subnets

### プロジェクトBにおけるサブネット
また、ProjectBにおいても2つのSubnetを用意しています。１つはPSC Endpointを配置するためのサブネットです。このSubnetは普通のSubnetです。もう１つはVPC Connector専用のサブネットです。purposeは `PRIVATE_SERVICE_CONNECT` を指定する必要があり、CIDRレンジは/28に固定する必要があります。

# 実装
それでは、具体的な実装を紹介します。基本的にはすべてTerraformでリソースを作成しますが、Cloud Runはgcloudコマンドでリリースする構成としました。これは、Cloud Runはインフラとしての管理ではなくアプリケーション側での管理としたいためです。Terraformで定義をした場合、アプリケーション側でデプロイを行うとTerraformで定義した構成と差分が出てしまうため、Terraformでの定義自体をしないようにしています。
## プロジェクトA
### Terraform

```tf
# Networ
resource "google_compute_network" "project_a_network" {
  name                    = "project-a-network"
  project                 = "project-a"
  auto_create_subnetworks = false
}

# Subnet
resource "google_compute_subnetwork" "project_a_subnet" {
  name          = "project-a-subnet"
  project       = "project-a"
  region        = "asia-northeast1"
  ip_cidr_range = "x.x.x.x/xx"
  network       = google_compute_network.project_a_network.name
}

resource "google_compute_subnetwork" "project_a_subnet_for_psc" {
  name          = "project-a-subnet-for-psc"
  project       = "project-a"
  region        = "asia-northeast1"
  ip_cidr_range = "x.x.x.x/xx"
  network       = google_compute_network.project_a_network.name
  purpose       = "PRIVATE_SERVICE_CONNECT"
}

resource "google_compute_subnetwork" "project_a_subnet_for_regional_proxy" {
  name          = "project-a-subnet-for-regional-proxy'
  project       = "project-a"
  region        = "asia-northeast1"
  ip_cidr_range = "x.x.x.x/xx"
  network       = google_compute_network.project_a_network.name
  purpose       = "REGIONAL_MANAGED_PROXY"
  role          = "ACTIVE"
}

# load balancer
resource "google_compute_region_network_endpoint_group" "neg_cloud_run_sample_api" {
  name                  = "neg-cloudrun-sample-api"
  network_endpoint_type = "SERVERLESS"
  region                = "asia-northeast1"

  cloud_run {
    service = "sample-api"
  }
}

resource "google_compute_region_backend_service" "backend_to_service_sample_api_internal" {
  name                  = "backend-service-to-sample-api-internal"
  region                = "asia-northeast1"
  load_balancing_scheme = "INTERNAL_MANAGED"
  protocol              = "HTTP"

  backend {
    balancing_mode = "UTILIZATION"
    group          = google_compute_region_network_endpoint_group.neg_cloud_run_sample_api.self_link
  }
}

resource "google_compute_region_url_map" "url_map_to_sample_api" {
  name            = "url-map-to-sample-api-internal"
  region          = "asia-northeast1"
  default_service = google_compute_region_backend_service.backend_to_service_sample_api.self_link
}

resource "google_compute_region_target_http_proxy" "target_http_proxy_to_sample_app_internal" {
  name        = "target-http-proxy-to-sample-app-internal"
  description = "target-http-proxy-to-sample-app-internal"
  region      = "asia-northeast1"
  url_map     = google_compute_region_url_map.url_map_to_sample_api.self_link
}

resource "google_compute_address" "sample_app_internal_load_balancer_address" {
  name         = "sample-app-internal-balancer-address"
  subnetwork   = google_compute_subnetwork.project_a_subnet.id
  address_type = "INTERNAL"
  address      = "x.x.x.x"
  region       = "asia-northeast1"
}

resource "google_compute_forwarding_rule" "forwarding_rule_to_sample_app_internal" {
  provider = google-beta
  name     = "forwarding-rule-to-sample-app-internal"
  region   = "asia-northeast1"

  ip_protocol           = "TCP"
  load_balancing_scheme = "INTERNAL_MANAGED"
  port_range            = "80"
  network               = google_compute_network.project_a_network.id
  subnetwork            = google_compute_subnetwork.project_a_subnet.id
  target                = google_compute_region_target_http_proxy.target_http_proxy_to_sample_app_internal.self_link
  ip_address            = google_compute_address.sample_app_internal_load_balancer_address.address
  network_tier          = "PREMIUM"
}

# PSC Attachment
resource "google_compute_service_attachment" "service_attachment_to_ma_manager" {
  name   = "service-attachment-to-sample-api"
  region = "asia-northeast1"

  enable_proxy_protocol = false
  connection_preference = "ACCEPT_MANUAL"
  nat_subnets           = [google_compute_subnetwork.project_a_subnet_for_psc.id]
  target_service        = google_compute_forwarding_rule.forwarding_rule_to_sample_app_internal.id

  consumer_accept_lists {
    project_id_or_num = "project-b"
    connection_limit  = 1
  }
}
```

### CloudRun
```sh
gcloud run deploy sample-api \
  --region asia-northeast1 \
  --image "image" \
  --service-account "sa@xxx.iam.gserviceaccount.com" \
  --ingress internal-and-cloud-load-balancing \
  --concurrency 10 \
  --min-instances 1 \
  --max-instances 3
```

### 補足
#### CloudRunのdeployについて
Cloud Runにおいて、`--ingress` オプションで `internal-and-cloud-load-balancing` を指定することで、内部またはLBからの通信のみを許可することができます。`gcloud run deploy` コマンドの詳細は以下のドキュメントをご参照ください。

https://cloud.google.com/sdk/gcloud/reference/run/deploy

## プロジェクトB
### Terraform
```tf
# Network
resource "google_compute_network" "project_b_network" {
  name                    = local.project
  project                 = local.project
  auto_create_subnetworks = false
}

# Subnet
resource "google_compute_subnetwork" "project_b_subnet" {
  name          = "project-b-subnet"
  project       = "project-b"
  region        = "asia-northeast1"
  ip_cidr_range = "x.x.x.x/xx"
  network       = google_compute_network.project_b_network.name
}

resource "google_compute_subnetwork" "project_b_subnet_for_sample_app_vpc_connector" {
  name          = "${local.project}-for-vpc-connector-subnet-asia-northeast1"
  project       = local.project
  region        = "asia-northeast1"
  ip_cidr_range = "x.x.x.x/28"
  network       = google_compute_network.project_b_network.id

  private_ip_google_access = true
}

# VPCConnector
resource "google_vpc_access_connector" "sample_client_app_vpc_connector" {
  name   = "sample-client-app-vpc-connector"
  region = "asia-northeast1"

  subnet {
    name       = google_compute_subnetwork.project_b_subnet_for_sample_app_vpc_connector.name
    project_id = "project-b"
  }
  machine_type  = "e2-micro"
  min_instances = 2
  max_instances = 3
}

# PSC Endpoint
resource "google_compute_address" "sample_api_psc_endpoint_address" {
  address_type = "INTERNAL"
  ip_version   = "IPV4"
  name         = "sample-api-psc-endpoint-address"
  project      = "project-b"
  purpose      = "GCE_ENDPOINT"
  region       = "asia-northeast1"
  subnetwork   = google_compute_subnetwork.project_b_network.id
  address      = "x.x.x.x" # project_b_subnetのレンジから指定
}

resource "google_compute_forwarding_rule" "sample_api_psc_endpoint" {
  load_balancing_scheme = ""
  ip_address            = google_compute_address.sample_api_psc_endpoint_address.id
  name                  = "sample-api-psc-endpoint"
  subnetwork            = google_compute_subnetwork.project_b_network.id
  project               = "project-b"
  region                = "asia-northeast1"
  target                = "https://www.googleapis.com/compute/v1/projects/project-b/regions/asia-northeast1/serviceAttachments/service-attachment-to-sample-api"
"

  service_directory_registrations {
    namespace = "goog-psc-default"
  }
}
```
### CloudRun
```sh
gcloud run deploy sample-client-app \
  --region asia-northeast1 \
  --image "image" \
  --service-account "sa@xxx.iam.gserviceaccount.com" \
  --vpc-connector sample-client-app-vpc-connector \
  --vpc-egress private-ranges-only \
  --concurrency 10 \
  --min-instances 1 \
  --max-instances 1
```
### 補足
####  PSC Endpointのアドレスに付いて
PSC Endpointでは、指定したサブネット内のアドレスを紐づけることができます。そして、VPC内から紐づけたアドレスに対して通信をすることでPSC Endpointに紐づいたPSC Attachmentを経由し、Attachmentに紐づいたリソースへアクセスすることができます。つまり、自分たちで管理しているVPC内のアドレスにアクセスすることで、他のプロジェクト・VPC内のリソースのアクセスが可能となります。これにより、他のVPCのCIDRレンジがどうなっているかなどを気にする必要がありません。

#### PSC Endpointのtargetについて
PSC Endpointでは、ターゲットとなるPSC Attachmentをlink形式で指定する必要があります。基本的には以下の形式のリンクとなります。

```
https://www.googleapis.com/compute/v1/projects/アタッチメントのプロジェクト名/regions/アタッチメントのリージョン/serviceAttachments/アタッチメント名
```

#### CloudRunのdeployについて
Cloud Runにおいて、VPC Connectorを経由した通信をしたい場合は、deployコマンドでどのVPC Connectorを利用するかを `--vpc-connector` オプションで指定する必要があります。また、すべての通信をVPC Connector経由にするか、Private IPのみをVPC Connectorを経由させるようにするかを選択ができます。今回は `--vpc-egress private-ranges-only` とすることで、Private IPのみをVPC Connector経由で通信するように設定しています。こちらも以下のドキュメントをご参照ください。

https://cloud.google.com/sdk/gcloud/reference/run/deploy

# まとめ
今回は、PSCを利用してプロジェクトをまたいだCloudRunからCloudRunへの通信を内部の経路で行う構成について紹介しました。PSCを利用することで、VPCピアリングを利用せずにVPCをまたいだ通信ができるため、CIDRかぶりの問題を考えたりといった頭が痛い問題から開放されてかなり便利だなと感じています。今回の構成を参考にしていただけたら幸いです。