🍊

2つのCloud Run にルーティングするLBをTerraformで実装しました

2023/07/24に公開

概要

以下のような構成を Terraform で実現しました。

実装

Terraform について

前提として、Terraform について理解したい方は下の記事が参考になるかと思います。

https://zenn.dev/sasakiki/articles/3c903edaea1817

技術

  • Google Cloud Platform
  • Terraform v1.5.3

事前準備

  • Terraform に紐づけるサービスアカウントが必要です。下記の実装で権限エラーになる場合は、適切な権限を付加してください。
  • 2つの Cloud Run を走らせるための Docker イメージが必要です。自分は Artifact Registry におきました。

内部構成

下のような内部構成をとります。

参考: https://cloud.google.com/load-balancing/docs/https?hl=ja

コード

実行した Terraform のコードはこちらです。

dev.tfvars
project_id = "<your project ID>"
api_domain = "<your custom domain>"
main.tf
provider "google" {
  // サービスアカウントの鍵を指定
  credentials = file("./${var.project_id}-credential.json")
  project     = var.project_id
  region      = "asia-northeast1"
  zone        = "asia-northeast1-a"
}

// Cloud Run API 環境設定
resource "google_cloud_run_service" "server-default" {
  name     = "server-default"
  location = "asia-northeast1"
  template {
    spec {
      containers {
        // image の指定。あくまでも一例です。
        image = "asia-northeast1-docker.pkg.dev/${var.project_id}/<artifact registry repository ID>/<your image name>:latest"
      }
    }
  }
  
  // コンテナへのアクセスを内部 + ロードバランサー経由に限定します。
  // Ref. https://cloud.google.com/run/docs/securing/ingress?hl=ja
  metadata {
    annotations = {
      "run.googleapis.com/ingress" = "internal-and-cloud-load-balancing"
    }
  }
}

// Cloud Run web 環境設定
resource "google_cloud_run_service" "web-default" {
  name     = "web-default"
  location = "asia-northeast1"
  template {
    spec {
      containers {
        image = "asia-northeast1-docker.pkg.dev/${var.project_id}/<artifact registry repository ID>/<your image name>:latest"
      }
    }
  }
  metadata {
    annotations = {
      "run.googleapis.com/ingress" = "internal-and-cloud-load-balancing"
    }
  }
  autogenerate_revision_name = true
}

// iam ポリシーを定義します
data "google_iam_policy" "cloud_run_public" {
  binding {
    role = "roles/run.invoker"
    members = [
      "allUsers",
    ]
  }
}

// Cloud Run ポリシー設定
// 直前で定義したポリシーをCloud Run と紐づけることで、Cloud Run へ未認証でもリクエスト可能になります。
resource "google_cloud_run_service_iam_policy" "policy-server-default" {
  location = google_cloud_run_service.server-default.location
  project  = google_cloud_run_service.server-default.project
  service  = google_cloud_run_service.server-default.name

  policy_data = data.google_iam_policy.cloud_run_public.policy_data
}
resource "google_cloud_run_service_iam_policy" "policy-web-default" {
  location = google_cloud_run_service.web-default.location
  project  = google_cloud_run_service.web-default.project
  service  = google_cloud_run_service.web-default.name

  policy_data = data.google_iam_policy.cloud_run_public.policy_data
}

// ネットワーク エンドポイント グループ の設定。どこの Cloud Run にリクエストを送るか指定します。
// 下の公式Documentにもあるように、App Engine や Cloud Functions に置き換えることも可能です。
// https://cloud.google.com/load-balancing/docs/negs/serverless-neg-concepts?hl=ja
resource "google_compute_region_network_endpoint_group" "cloudrun_neg_api" {
  name                  = "cloudrun-neg-api"
  network_endpoint_type = "SERVERLESS"
  region                = "asia-northeast1"
  cloud_run {
    service = google_cloud_run_service.server-default.name
  }
}
resource "google_compute_region_network_endpoint_group" "cloudrun_neg_web" {
  name                  = "cloudrun-neg-web"
  network_endpoint_type = "SERVERLESS"
  region                = "asia-northeast1"
  cloud_run {
    service = google_cloud_run_service.web-default.name
  }
}

// HTTPサーバー設定
resource "google_compute_backend_service" "api-default" {
  name = "lb-api-default"

  protocol    = "HTTP"
  port_name   = "http"
  timeout_sec = 30

  backend {
    group = google_compute_region_network_endpoint_group.cloudrun_neg_api.id
  }
}
resource "google_compute_backend_service" "web-default" {
  name = "lb-web-default"

  protocol    = "HTTP"
  port_name   = "http"
  timeout_sec = 30

  backend {
    group = google_compute_region_network_endpoint_group.cloudrun_neg_web.id
  }
}

// リクエストがきたURLの振り分け設定
resource "google_compute_url_map" "default" {
  name = "default-lb-urlmap"

  default_service = google_compute_backend_service.web-default.id

  host_rule {
    hosts        = ["${var.default_domain}"]
    // httpで受け取る場合は以下のように指定する
    // hosts        = [google_compute_global_address.default.address]

    path_matcher = "allpaths"
  }

  path_matcher {
    name            = "allpaths"
    default_service = google_compute_backend_service.web-default.id

    // /api にリクエストが送られたら、その部分だけ取り除いて api Cloud Run にリクエストを転送します
    path_rule {
      paths   = ["/api/*"]
      service = google_compute_backend_service.api-default.id
      route_action {
        url_rewrite {
          path_prefix_rewrite = "/"
        }
      }
    }
  }
}

// IPアドレスを保持
resource "google_compute_global_address" "default" {
  name = "default-lb-address"
}

// 上で定義した URL MAP に転送する proxy を定義
// http を受け取る形にする場合は、google_compute_target_http_proxy を利用します。SSL証明書の紐付けは必要ありません。
resource "google_compute_target_https_proxy" "default" {
  name = "default-lb-http-proxy"

  url_map = google_compute_url_map.default.id
  
  ssl_certificates = [
      google_compute_managed_ssl_certificate.default.id
  ]
}

// 証明書の発行
// httpで受け取る場合は不要
resource "google_compute_managed_ssl_certificate" "default" {
  provider = google-beta

  name = "api-lb-cert"
  managed {
    domains = ["${var.default_domain}"]
  }
}

// インターネットからリクエストを受ける forwarding role を指定
resource "google_compute_global_forwarding_rule" "default" {
  name = "default-lb-forwarding-rule"

  target     = google_compute_target_https_proxy.default.id
  port_range = "443" // proxy で http を受け取るなら、80に指定
  ip_address = google_compute_global_address.default.address
}

実行

以下のコマンドを実行することで上記構成を反映させることができます。

// 内容の確認
> terraform plan -var-file=dev.tfvars

// 実行
> terraform apply -var-file=dev.tfvars

最後に

いかがだったでしょうか。

上記の構成を応用すると、柔軟な負荷分散を行うことができるようになったり、Google Cloud Armor 等を利用することでよりセキュリティを強化した設計をとることができます。プロダクションレベルにて、十分運用可能な構成かと思います。

Contrea Tech Blog

Discussion