やってみた: Cloud Run にカスタムドメインと IAP を設定する

これの続きみたいなものなんだけど、ドメインの設定後にもともとやりたかった IAP が設定できたのであらためて書いてみる。

やりたいこと

  • IAP で cloud run にアクセスできる人を限定したい
  • 自分のドメインを設定したい

参考記事

terraform 公式 docs のほか、これらのブログにめっちゃ助けられた

ポイント

IAP の service identity

terraform 経由で IAP の設定をすると、↓にある通り IAP 用の "service identity" ってやつが作れらないので、それも別途作成する必要がある

https://cloud.google.com/iap/docs/enabling-cloud-run#troubleshooting_errors:~:text=The%20IAP%20service%20account%20is%20not%20provisioned

terraform はこう

data "google_project" "project" {}

resource "google_project_service_identity" "iap" {
  provider = google-beta

  project = data.google_project.project.project_id
  service = "iap.googleapis.com"
}

タイトルの通りで、 API 非対応のリソースは手でつくる必要がある。terraform には

を渡した

つくるリソースは Cloud Run の integrations からわかる

load balancer まわりのリソースは作るものが多くてわかりにくいけど、実は Cloud Run の integrations タブから Custom domains - Google Cloud Load Balancing を選ぶと、リソースの一覧がみれる

OAuth client の redirect URI/Javascript Origin

...ってのがどこにも書いてなかったんだけど、たぶんこう。

  • javascript origin: https://iap.https://iap.googleapis.com
  • redirect URI: https://iap.googleapis.com/v1/oauth/clientIds/<YOUR_OAUTH_CLIENT_ID>:handleRedirect

どうやって見つけたかというと、両方に最初自分のアプリケーションの URL を設定していたんだけど、アクセスすると 400 redirect_uri_mismatch が出て、そのエラーメッセージに "redirect URI はこれだよ" って書いてあった。

コード

Cloud Run

Cloud Run まわりでは、 Cloud Run そのものと、invoke する権限を Cloud Run 用の service account と allUsers (つまり public に公開する) に与える。ingress は internal (i.e. VPC内) か load balancer からに限定することで、 IAP をバイパスされないようにしていて、 allUsers といっても IAP で許可された人のみに限定してる。

resource "google_cloud_run_v2_service" "app" {
  name     = "app"
  location = "asia-northeast1"
  ingress  = "INGRESS_TRAFFIC_INTERNAL_LOAD_BALANCER"

  template {
    containers {
      image = var.app_image_url
      resources {
        limits = {
          cpu    = "2"
          memory = "1024Mi"
        }
      }
      # TODO: set environment variables
    }
    service_account = var.app_service_account_email
  }
}

resource "google_cloud_run_v2_service_iam_member" "app_member" {
  name   = google_cloud_run_v2_service.app.name
  role   = "roles/run.invoker"
  member = "serviceAccount:${var.app_service_account_email}"
}

resource "google_cloud_run_v2_service_iam_member" "app_access_unauthenticated" {
  name   = google_cloud_run_v2_service.app.name
  role   = "roles/run.invoker"
  member = "allUsers"
}

Cloud DNS

DNS ではまず先に Cloud Domains でドメインをとっておく。すると、ドメインと managed zone が作られるので、

  • ドメインapp_domain_name
  • zone 名 app_dns_managed_zone_name

をそれぞれ terraform に渡す。

その後、 terraform では load balancer 用に IP を払い出すのでそれを A レコードにセットして、SSL 証明書もつくっておく。

resource "google_compute_global_address" "app_load_balancer_ip" {
  name       = "app-load-balancer-ip"
  ip_version = "IPV4" # is default
}

resource "google_dns_record_set" "app_a_record" {
  managed_zone = var.app_dns_managed_zone_name
  name         = "${var.app_domain_name}."
  type         = "A"
  ttl          = 300
  rrdatas      = [google_compute_global_address.app_load_balancer_ip.address]
}

resource "google_compute_managed_ssl_certificate" "app" {
  name = "app-ssl-certificate"
  managed {
    domains = [var.app_domain_name]
  }
}

load balancer

ここはリソースをいっぱいつくるところで、それぞれの関係性はこんなイメージ↓

IAP の設定は backend service に対して実施して、ここで oauth client の id + secret を渡す。

resource "google_compute_url_map" "app" {
  name            = "app-url-map"
  default_service = google_compute_backend_service.app.id

  host_rule {
    hosts        = [var.app_domain_name]
    path_matcher = "allpaths"
  }

  path_matcher {
    name = "allpaths"
    # NOTE: default service is required at the this level too
    default_service = google_compute_backend_service.app.id
  }
}

resource "google_compute_target_https_proxy" "app" {
  name             = "app-https-proxy"
  ssl_certificates = [google_compute_managed_ssl_certificate.app.id]
  url_map          = google_compute_url_map.app.id
}

resource "google_compute_global_forwarding_rule" "app" {
  name                  = "app-forwarding-rule"
  load_balancing_scheme = "EXTERNAL"
  ip_address            = google_compute_global_address.app_load_balancer_ip.address
  ip_protocol           = "TCP"
  port_range            = "443-443"
  target                = google_compute_target_https_proxy.app.id
}
resource "google_compute_backend_service" "app" {
  name = "app-backend-service"
  backend {
    group = google_compute_region_network_endpoint_group.app.id
  }
  iap {
    oauth2_client_id     = var.app_oauth_client_id
    oauth2_client_secret = var.app_oauth_client_secret
  }
}

IAP の許可設定

許可するメールアドレスを locals で定義して、それらに対して IAP-secured Web App User のロールを付与する。メアドのリストは多分 terraform 直書きしない方がいいと思うけど、まあ一旦..

locals {
  app_user_emails = toset([
    "user-1@example.com",
    "user-2@example.com",
  ])
}
resource "google_iap_web_backend_service_iam_member" "app_users" {
  for_each = { for email in local.app_user_emails : email => email }

  web_backend_service = google_compute_backend_service.app.name
  role                = "roles/iap.httpsResourceAccessor"
  member              = "user:${each.value}"
}

IAP の service identity

最初に書いた通り、 service identity をつくる。

data "google_project" "project" {}

# see: https://cloud.google.com/iap/docs/enabling-cloud-run#troubleshooting_errors
resource "google_project_service_identity" "iap" {
  provider = google-beta

  project = data.google_project.project.project_id
  service = "iap.googleapis.com"
}

動作確認

以上を設定して、

  • 自分のアプリにアクセスして
  • OAuth 画面が出てくる
  • Google login してアプリ画面が表示される

ってなれば OK