やってみた: 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

やってみた: cloud run domain mapping w/terraform

Cloud Run のカスタムドメインってやつを terraform (超noob) でやってみた。休みボケでやる気ないのでソースとかあんま貼ってません...

前提: cloud run に自分のドメインを設定する方法

...は、2つある。1つめは load balancing を使う方法で、もう1つは "domain mapping" っていう機能を使う方法。

domain mapping を使ったほうが設定は圧倒的に楽なのだが、 preview 機能なので本番とかは微妙かも。今回は preview は別に良かったんだけど、別用途で load balancer がいるので前者にしてみた。

domain mapping w/terraform

最初勘違いしてこっちでやってて、せっかくなので書いておく。

前提として、全部 terraform ってわけにはいかなくてドメインを Cloud Domains で取得するにしてもこれはコンソールでしかできない。で、 Cloud Domains でドメインを買うと、自動的に DNS zone も作成されるので、 terraform には

を variables として渡す。

コードサンプルは↓で、下記のサイトをめちゃめちゃ参考にさしてもらいました (bow)。

流れ的には

  • Cloud Run は定義されてる前提
  • google_cloud_run_domain_mapping で domain mapping のリソースを定義する。ドメイン名はここに渡す
  • domain mapping を定義すると、 DNS に設定すべき A/AAAA (それぞれ IPv4/IPv6 の値) レコードの値が GCP から渡される
  • ので、それを DNS に設定する。

ちなみに TTL とかは terraform のサンプルコードを参考にしただけなので、これがベストなのかはあんまりわかってない。

resource "google_cloud_run_domain_mapping" "app" {
  location = "asia-northeast1"
  name     = var.app_domain_name

  metadata {
    namespace = google_cloud_run_v2_service.app.project
  }

  spec {
    route_name = google_cloud_run_v2_service.app.name
  }
}


locals {
  app_dns_rr_a     = [for rr in google_cloud_run_domain_mapping.app.status[0].resource_records : rr.rrdata if rr.type == "A"]
  app_dns_rr_aaaa  = [for rr in google_cloud_run_domain_mapping.app.status[0].resource_records : rr.rrdata if rr.type == "AAAA"]
  app_dns_rr_cname = [for rr in google_cloud_run_domain_mapping.app.status[0].resource_records : rr.rrdata if rr.type == "CNAME"]
}


resource "google_dns_record_set" "app_a" {
  count        = length(local.app_dns_rr_a) > 0 ? 1 : 0
  managed_zone = var.app_dns_managed_zone_name
  name         = "${var.app_domain_name}."
  type         = "A"
  ttl          = 300
  rrdatas      = local.app_dns_rr_a
}

resource "google_dns_record_set" "app_aaaa" {
  count        = length(local.app_dns_rr_aaaa) > 0 ? 1 : 0
  managed_zone = var.app_dns_managed_zone_name
  name         = "${var.app_domain_name}."
  type         = "AAAA"
  ttl          = 300
  rrdatas      = local.app_dns_rr_aaaa
}

resource "google_dns_record_set" "app_cname" {
  count        = length(local.app_dns_rr_cname) > 0 ? 1 : 0
  managed_zone = var.app_dns_managed_zone_name
  name         = "${var.app_domain_name}."
  type         = "CNAME"
  ttl          = 300
  rrdatas      = local.app_dns_rr_cname
}

load balancer + custom domain w/terraform

こっちは作成するリソースがめっちゃ多くて、なぜかというと GCP の load balancer は1つの "load balancer" っていうリソースがあるわけじゃなくて、複数のリソースを組み合わせてつくるかららしい。terraform コードは↓の記事を見つつ書いてった(あざます)。

terraform だけだとそれぞれのリソースが何してるか謎すぎたので、 chat gpt に聞きつつ整理してみたのがこれ↓

注意点として、これを入れると Cloud Run の ingress が internal/load balancer で、 load balancer の方にはもともとの https://*.run.app は設定してないので、こっちの URL はアクセスできない。自分の環境だと

Error: Page not found
The requested URL was not found on this server.

っていうメッセージが出た。

日記: 最近やったこと

4月中旬〜5月末の有給消化でやったことめも

  • Data flow, Datastream quickstart をやってみる
  • Go に入門してみる
  • Fundamentals of Data Engineering を読む
  • terraform 入門してみる
  • あおいさんの k8s 本やってみる

GW 終わるくらいまではやる気があって色々やってたけど、後半はもう完全に休むモードになってた

小ネタ: Cloud Run で server IP address をとる方法

背景: 事情により Cloud Run がどの IP でコードを実行しているのか知りたかった

結論、 Cloud Run への POST/GET のエントリを開いて、 httpRequest.serverIp を見ればわかる (リクエストの一番最初につくられるエントリ)。

これをたまたま見つけたとき、、ほんとに Cloud Run の IP なのか気になったので探してみたら↓のドキュメントが見つかった。

https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#HttpRequest

serverIp は

The IP address (IPv4 or IPv6) of the origin server that the request was sent to. This field can include port information. Examples: "192.168.1.1", "10.0.0.1:80", "FE80::0202:B3FF:FE1E:8329".

とのことなので、たぶん探してるもののはず... この文章だと厳密にいえばリクエストが「送信された」サーバと実行するサーバが別の場合、探してるものではない可能性があるけど、もし違ったらおしえてほしい :pleading_face:

あと、地味に知らなかったのが Cloud Run の region と IP address の region は全然関係ないということ。

この SO だと Google のrecognized 人から回答来てる↓

All Google Cloud IPs (including the Compute Engine VMs) will seem like they're coming out of "Mountain View, CA, USA" regardless of the region they're running in. This is because they belong to the same global IP block that Google has.

This IP pool is also likely used by other Google products like Cloud Run, Cloud Functions, App Engine and Compute Engine as well.

Most probably, Google doesn’t dedicate certain IP ranges to specific regions, as Google’s IP blocks are meant to be routed to the nearest Google datacenter, then enter Google’s private network, and be routed internally.

GCP の IP は基本カリフォルニアの Mountain View の IP になる

Cloudflare Pages にデプロイした Sveltekit app で TypeError: Unsupported cache mode: no-cache を踏んだ

TL;DR

原因は Cloudflare pages が Node.js runtime ではないことで、 wrangler.tomlcompatibility_flags = ["nodejs_compat"] を書いてデプロイしたら治った。

see: https://discord.com/channels/1066956501299777596/1239118579509628929/1239252696566075593

起きていたこと

前提: Trigger.dev はサーバレスの background job platform (とてもおすすめ)

  • Sveltekit
  • Trigger.dev (background jobs)
  • Supabase (DB + auth)
  • Cloudflare Pages

というスタックのアプリで、ある日突然特定の server action が 500 を変えすようになった。初回のデプロイは普通に通って、 1-2ヶ月ふつうに動いていたのにある日突然動かなくなるというなかなか怖い現象があった。

調査

問題のアプリはツイッター的なものなんだけど、「投稿」時にまずエラーになっていた。この時点で、

  • 画面を更新すると投稿は増えている → supabase との接続は問題なし
  • 削除処理は普通に動く → create server action の何かしらのコードが悪さをしている

とわかっていたので、途中途中に console.log を仕込んでいった。

で、デプロイして投稿をすると、どうやら xxJob.invoke() の手前で落ちてるとわかった。たしかに Trigger.dev のダッシュボードを見ると、prod だと直近の発火履歴がなかった。

もろもろ Trigger.dev のコードを見たりググったりしたものの、ローカルだと普通に動くという問題もあって Trigger.dev のディスコ鯖に投稿した。あと、ググっても結構ヒットしなかった。

fix

夕方 JST くらいに投稿して、 2AM くらいにさっそく公式から回答が来て、結論

  1. Trigger.dev client を呼ぶときに API キーを明示的に指定してね (process.env がないから SDK 側でよしなに処理...というのができない)
  2. wrangler.tomlcompatibility_flags = ["nodejs_compat"] を指定してね

とのことだった。1はすでにやってたので、2を実施したら一瞬で治った。

一応 chat gpt の話をめも的に貼っとく。

残る謎

まじで今までなんで動いていたのかだけが謎。 たしかに Trigger.dev は最初から no-cache ヘッダをつけてはなかったんだけど、追加したコミットは 5-6mo 前なので自分のアプリをデプロイするよりずっと前のはずで、パッケージもアプリ構築当時の最新を使っていた気がする。

https://github.com/triggerdotdev/trigger.dev/blame/2081cb15b89dd4de2fd34a7ea0a0171d9cc69d2f/packages/trigger-sdk/src/apiClient.ts#L854

ジョブが静かに死んでいたという形跡もないので、あとは Cloudflare Pages がよしなに処理していてくれたとかなのかもしれない。ウーン...

まとめ

  • TypeError: Unsupported cache mode: no-cache を踏んだ
  • Cloudflare runtime が Node.js でないことが原因
  • wrangler.toml に compatibility_flags = ["nodejs_compat"] を追加したらなおった

めも: GCP の API key について

まずそもそも API key はによる認証はサポートされてないことが多い。

Most Google Cloud APIs don't support API keys. Check that the API that you want to use supports API keys before using this authentication method.

https://cloud.google.com/docs/authentication/api-keys

サポート有無の一覧的なものは見つけられなかったけど、今回対象にしてた Cloud Text-to-Speech については REST API Reference の「試してみよう」的なところに API key があったので、そうやって確認するしかないのかも?

https://cloud.google.com/text-to-speech/docs/reference/rest/v1beta1/text/synthesize

API を叩くときに key の情報は ?key=<YOUR_API_KEY> とやるっぽい。こちらもドキュメントの記載は見つけられなかったけど、

  • 上の画面から試してみる
  • Network タブから確認

すると、どんな形で API を叩いているかわかる

あとは、 API キーを使えるドメインを制限している場合、 Referer ヘッダに値を入れないと 403 になる

Cloudflare Pages に Cloudflare Access をつける

ユースケース: 雑につくったアプリを Cloudflare Pages にアップしたが、有象無象にアクセスされたくないので雑に認証をつけたい

毎回微妙に手順を忘れるので書いておく

まず Cloudflare Pages (以下 Pages) に Cloudflare Access (以下 Access) をつけるとき、 Access 側のページからは設定できない。かわりに、 Pages から Manage タブ > Access Policy のとこから "Enable access policy" をクリックして設定をはじめる。

Access 側では、デフォルトのドメイン設定だと preview のページにしかアクセス制限がかからないので、subdomain なしのドメインを追加する。

ここまでやれば、 email x OTP でのアクセス制限はかけられる。なんだけど、Access からメールが来るのにやたら時間がかかるので Google 認証もつけたい。 Google 認証はアプリケーション単位ではなく、 Cloudflare のアカウント単位で設定するもので、 Access の Settings ページからやる。

手順はここ↓ で、 GCP 側に OAuth consent screen と OAuth client をつくる。

consent screen のポイント:

  • user type: External
  • authorized domains: client を作るときに勝手に追加されるので、consent screen を作るときに設定しなくて ok
  • scopes: .../auth/userinfo.email を追加するといいよ、と上の docs に書いてある
  • test users: ... はいらない (これも docs に書いてある)

OAuth client のポイント:

  • type: Web application
  • origin/redirect URI: docs に書いてある & ためしに Cloudflare Access のログイン画面を自分のアプリに行って開けばわかる

ここも参考になった、あざます https://egashira.dev/blog/uses-google-oauth-for-cloudflare-pages#google-cloud-console-%E3%81%A7-oauth-%E5%90%8C%E6%84%8F%E7%94%BB%E9%9D%A2%E3%81%AE%E8%A8%AD%E5%AE%9A

ここまでやったら Cloudflare Access 全体の設定に Google が IdP として追加されたので、最後に Pages の AccessGoogle を追加する。