見出し画像

Cloud RunとPrivate VPCのリソースの連携方法

※「株式会社YOJO Technologies」から「PharmaX株式会社」へ社名変更いたしました。この記事は社名変更前にリリースしたものになります。

はじめに

開発の竹内(@kenta_714)です。
弊社では2022/6/12に漢方・サプリのサブスクリプションサービスであるYOJOのシステム基盤をHerokuからGCPへとリプレースしました。
その際にIaC化としてTerraformを導入してみましたので、GCPをTerraformで実装する際のノウハウを複数回に分けて記事にて共有していきます。
今回はプライベートなVPCネットワークを使った際のCloud Runとの連携方法についてです。

まずは今回作りたいインフラ構成図を図示します。

Cloud RunとVPCを使ってDBに接続するためのインフラ構成図

上図の構成であればCloud Runだけではなく、VPC上にGCEを作成した場合も同様にCloud SQLに割り当てられたIP(10.20.0.0/16の範囲になるはずです)を指定すれば接続可能です。ただし今回はCloud RunがメインのためGCEの話はしません。

次章からはTerraformのコード記述に入る前に、図に記載のあるGoogle Cloud専用の単語についてそれぞれ簡単に解説します。

各サービスの解説

VPC

VPCとはVirtual Private Cloud の略で、GCPの場合はプロジェクト単位で分割され、複数のリージョンを横断したグローバルな仮想ネットワークを構築することのできるサービスです。
VPCを利用することでクラウド環境化でも仮想的に内部ネットワークを構築することができます。ローカルネットワークではなじみ深い10.0.0.0あたりのプライベートIPアドレスがデフォルトで設定されるので、感覚的にも分かりやすいようになっています。

Cloud Run

ここではCloud Runそのものの説明は割愛し、ネットワークの部分のみ説明します。

Cloud RunはGoogle Cloudの仕様上、VPC配下に作成されません。そのため「サーバーレスVPCアクセス」を使って他のサービスと連携させる必要があります。

サーバーレスVPCアクセス

Cloud RunやCloud Functionsなどのサービスは、サーバーレスVPCアクセスを使うことで内部 DNS と内部 IP アドレスを使用してをVPC配下のサービスと疎通させることができます。
参考: https://cloud.google.com/vpc/docs/serverless-vpc-access

なお、サーバーレスVPCアクセスは疎通するための環境を作ってくれるだけなので、実際はサーバーレスVPCアクセスコネクタを使います。実態はコンテナベースのサービスのため、GCEやCloud Runのようにインスタンスタイプを選んだり最小・最大コンテナ数を設定しなければいけません。さらに自動スケールアウトはするもののスケールイン(稼働コンテナ数を減らす)はしてくれないという点に注意して使う必要があります。

プライベートサービスアクセス

Cloud Runと同様、Cloud SQLやMemorystoreも、Google Cloudの仕様上VPC配下に作成されません。
これらのサービスは「プライベートサービスアクセス」を利用することでプライベートなVPCを通じて他のサービスと疎通させることができます。
参考: https://cloud.google.com/vpc/docs/private-services-access

利用に際してIP範囲を指定する必要があります。(10.30.0.1/24など)
Cloud SQLやMemorystoreの作成時にIP範囲を自動作成することもできますが、自分たちの定義したVPCのネットワークセグメントや利用不可能なIP範囲が割り当てられることもたまにあるため、なるべく指定したほうが良いと思います。
実際の事例: https://tech.zeals.co.jp/entry/2020/03/05/140627

また、GUIで作業する場合Cloud SQLやMemmorystoreを作る前に定義しておかないと以下のように表示されてしまいますので、構築の順序には気をつけましょう。

プライベートサービスアクセス接続の定義がない場合、Cloud SQLの作成時に警告文が表示される

作成はVPCネットワークの詳細から [プライベートサービス接続] > [IP範囲の割当] からできます。

プライベートサービスアクセスが作成できる画面

VPCネットワーク ピアリング

VPC ネットワークピアリングは異なるVPCセグメント同士を接続するためのサービスです。
参考: https://cloud.google.com/vpc/docs/vpc-peering

前述の通りプライベートサービスアクセスはIP範囲を指定する必要があるため、VPCとは異なるセグメントに作成されます。プライベートサービスアクセスを利用したCloud SQLやMemorysoreはプライベートサービスアクセスに指定したIP範囲に属するIPが付与されます。
このサブネットとVPCを接続するためにVPCネットワークピアリングが必要になります。
ルーティング対象のサブネットについては [VPCネットワーク] > [VPCネットワーク ピアリング] > [作成したピアリングをクリック] で閲覧可能です。(ここに具体的なサービスへのリンクもあれば最高なんですが……)

VPC ネットワーク ピアリングを使って疎通可能なIP範囲一覧


TerraformによるIaC

いよいよTerraformです。
Cloud RunとCloud SQLをVPCで接続するためのIaCをTerraformで書いてみましょう。
前提条件は以下のとおりです。

  • VPCを作成(IP範囲は10.10.0.0/16)

    • プライベートサービスアクセスを作成(IP範囲は10.20.0.0/16)

    • VPCネットワーク ピアリングの作成

    • サーバーレスVPCアクセス/サーバーレスVPCアクセスコネクタを作成(IP範囲は10.30.0.0/28)

  • Cloud SQLを作成

  • Memorystoreを作成

  • Cloud Runを作成

ファイル構成は以下の通りです。stgディレクトリを作っている理由としては他の環境とコードが混合しないようにするためです。なお、provider.tfやterraform.tfvarsは企業やサービスによって全然違うと思いますので割愛します。

└── stg
    ├── cloud_run.tf
    ├── cloud_sql.tf
    ├── memorystore_for_redis.tf
    ├── provider.tf
    ├── terraform.tfvars
    ├── variables.tf
    └── vpc.tf

vpc.tf

vpc.tfではVPCそのもののとプライベートサービスアクセス、VPC ネットワーク ピアリングの定義を作成します。

# VPC作成
resource "google_compute_network" "this" {
  name                    = var.project
  auto_create_subnetworks = false
}

# VPCオリジナルのIP範囲の定義
resource "google_compute_subnetwork" "this" {
  name    = var.project
  region  = var.region
  network = google_compute_network.this.self_link

  ip_cidr_range = "10.10.0.0/16"
  log_config {
    metadata = "INCLUDE_ALL_METADATA"
  }
}

# プライベートサービスアクセスの定義(IP範囲は 10.20.0.0/16)
resource "google_compute_global_address" "private_ip_address" {
  name          = "private-ip-address"
  purpose       = "VPC_PEERING"
  address_type  = "INTERNAL"
  address       = "10.20.0.0"
  prefix_length = 16
  network       = google_compute_network.this.self_link
}

# VPCネットワーク ピアリングの作成
resource "google_service_networking_connection" "private_vpc_connection" {
  network                 = google_compute_network.this.self_link
  service                 = "servicenetworking.googleapis.com"
  reserved_peering_ranges = [google_compute_global_address.private_ip_address.name]
}

# サーバーレスVPCアクセスコネクタの作成
resource "google_vpc_access_connector" "this" {
  provider      = google-beta
  name          = var.project
  region        = var.region
  ip_cidr_range = "10.30.0.0/28"
  network       = google_compute_network.this.name
  machine_type  = "e2-micro"
  min_instances = 2
  max_instances = 3
}

注意点としてはサーバーレスVPCアクセスコネクタのIP範囲です。
サーバーレスVPCアクセスコネクタは必ず `/28` のサブネットでなければなりません。また、他のVPCのIP範囲と被らないようにする必要があります。
これらを考慮した上で、今回は 10.30.0.0/28 をIP範囲に設定しています。
参考: https://cloud.google.com/appengine/docs/standard/php-gen2/connecting-vpc


cloud_sql.tf

Cloud SQLはMySQLを作ります。工夫点としてはプレフィックスを付与するようにしている点。7日間は同じ名前のCloud SQLサービスを作れないというGoogle Cloudの仕様を回避するためです。

# 7日の間を空けないと同名のCloud SQLを作成できないのでプレフィックスを付与するように
resource "random_id" "db_name_suffix" {
  byte_length = 4
}

# Cloud SQLの定義
resource "google_sql_database_instance" "master" {
  name                = "test-db-${random_id.db_name_suffix.hex}"
  database_version    = "MYSQL_8_0"
  region              = "asia-northeast1"
  deletion_protection = "false"
  # VPCネットワーク ピアリング
  depends_on          = [google_service_networking_connection.private_vpc_connection]

  settings {
    tier              = var.cloud_sql_instance_type
    availability_type = "ZONAL"
    backup_configuration {
      enabled            = true
      binary_log_enabled = "true"
    }
    ip_configuration {
      ipv4_enabled    = false # 外部IPの付与を禁止
      # ↓作ったVPCを指定
      private_network = google_compute_network.this.self_link
      require_ssl = false # SSLは任意に指定
    }
    location_preference {
      zone = "asia-northeast1-a"
    }
  }
}

# DBインスタンス作成
resource "google_sql_database" "this" {
  name     = "test_database"
  instance = google_sql_database_instance.master.name
}

# DBユーザー作成
resource "google_sql_user" "this" {
  name     = "testuser"
  instance = google_sql_database_instance.master.name
  password = "password"
}

memorystore_for_redis.tf

MemorystoreはCloud SQLより簡単です。今回はRedis6を作ります。

resource "google_redis_instance" "this" {
  name           = var.project
  tier           = "BASIC"
  memory_size_gb = 1

  location_id = var.zone

  authorized_network = google_compute_network.this.id # 作ったVPCを指定
  connect_mode       = "PRIVATE_SERVICE_ACCESS"

  redis_version = "REDIS_6_X"

  # VPCネットワーク ピアリング
  depends_on = [google_service_networking_connection.private_vpc_connection]
}

cloud_run.tf

Cloud Runの定義をします。コンテナイメージがContainer Registryかにないとデプロイできないので注意が必要です。
今回はとりあえず未認証でも公開URLにアクセスできれば接続できるCloud Runアプリをデプロイします。

resource "google_cloud_run_service" "this" {
  name     = "cloudrun-test"
  location = "asia-northeast1"
  autogenerate_revision_name = true
  metadata {
    # サーバーレスVPCアクセスコネクタを利用
    # 内部IP範囲の通信だけに適用したいため「private-ranges-only」
    annotations = {
      "run.googleapis.com/vpc-access-connector" = google_vpc_access_connector.this.name
      "run.googleapis.com/vpc-access-egress"    = "private-ranges-only"
    }
  }
  template {
    spec {
      containers {
        # 実際はちゃんとしたコンテナイメージを指定する
        image = "gcr.io/cloudrun/testcontainer"
      }
    }
  }
}

# Cloud Runを未認証で外部公開するための設定↓
data "google_iam_policy" "this" {
  binding {
    role = "roles/run.invoker"
    members = [
      "allUsers",
    ]
  }
}
resource "google_cloud_run_service_iam_policy" "noauth" {
  location    = "asia-northeast1"
  project     = var.project
  service     = google_cloud_run_service.this.name
  policy_data = data.google_iam_policy.this.policy_data
}

サーバーレスVPCアクセスコネクタの設定は以下の部分になります。run.googleapis.com/vpc-access-egress の設定は「all」か「private-ranges-only」があります。外部通信しないCloud Runアプリであれば「all」でもよいのですが、大体のAPIサーバーは他社クラウドサービスと連携することが多いと思うため「private-ranges-only」を設定するのではないでしょうか。

annotations = {
  "run.googleapis.com/vpc-access-connector" = google_vpc_access_connector.this.name
  "run.googleapis.com/vpc-access-egress" = "private-ranges-only"
}

以上の設定で terraform init → terraform plan → terraform apply をすると正しくリソースが作成され、Cloud Runのサービスから内部IP(例えばDBのIPが10.20.0.5ならこのIPを指定)でDBと接続することができると思います。

まとめ

Cloud Runから内部IPでセキュアな通信をしたいだけでしたが、VPCにとどまらず多くのGCPサービスを使う必要があると分かりました。
サーバーレスVPCアクセスコネクタ周りのコストや拡張性を考慮したうえでの単純な外部通信ネットワーク構成との比較ができていないのが目下の課題です。
ただ、あまりにも大量のトラフィックがないのであれば、セキュリティ的にも課題の少ないインフラ構成なのではないかと思います。
ぜひ試してみてください。


この記事が気に入ったらサポートをしてみませんか?