目次
背景と課題
社内ツールや検証用の管理画面をCloud Runで公開すると、URLを知る全員がアクセスできてしまいます。認証をアプリ側に自前実装する方法もありますが、アプリのコードに認可ロジックを持ち込みたくないケースは少なくありません。
そこで有効なのが Cloud IAP(Identity-Aware Proxy) です。IAPをロードバランサーの前段に置けば、アプリを一切改修せずに「指定したGoogleアカウントだけがログインして到達できる」限定公開を実現できます。認証・認可をインフラ層に寄せられるのが最大の利点です。
ポイントは、Cloud Runにカスタムドメインを割り当てるだけでは完結しないことです。IAPを有効化するには、カスタムドメインを持つ組織アカウント、すなわち Google Workspace / Cloud Identity への登録と、そのアカウントでのGoogle Cloud組織の作成が前提になります。手順が多く依存関係も複雑なため、全体像を押さえてから着手するのが安全です。
全体像と前提
最終的な構成は次の流れになります。
- ユーザー → カスタムドメイン(DNS) → 外部HTTPSロードバランサー → IAP認証 → Cloud Run
これを成立させるために、以下を順に揃えていきます。有料の登録(ドメイン・Workspace・Cloud Identity)が含まれる点に注意してください。
手順
- カスタムドメインの取得(有料)
IAPで使うカスタムドメインを、お名前.comなどのレジストラで取得します。組織・DNS検証の起点になります。
- Google Workspaceへの登録(有料)
取得したドメインを組織アカウントとして扱うために登録します。
- Cloud Identityへの登録(有料)
IAPが必要とする組織のIDプロバイダを用意します。
- Google Cloudの利用登録(無料枠あり)
作成したカスタムドメインのユーザーアカウントで、Google Cloudの利用登録を行い、組織を作成します。IAPは組織配下のプロジェクトで有効化するため、この組織の作成が要になります。
- TXTレコードの追加
Cloud Identity利用にあたり、Google側で払い出される確認コードを、レジストラの管理画面からDNSのTXTレコードに追加し、ドメイン所有を証明します。
- サンプルコードのダウンロード、Terraformのインストール
GCP公式の Cloud Run Explore を土台にします。Cloud Run・ロードバランサー・IAPを一括で構成するTerraformコードがここに揃っています。
- サービスのデプロイ
tfvarsに必要な入力値を設定し、terraform apply を実行します。これでCloud Run・外部ロードバランサー・IAPがまとめてデプロイされます。
- Aレコードの追加
作成されたロードバランサーのIPアドレスを、公開したいサブドメインに割り当てるため、レジストラのDNSにAレコードを追加します。反映はお名前.comの場合 数時間から24時間程度 かかることがあります(実測では1〜2時間で疎通しました)。
- サブドメインにアクセスする
指定したサブドメインへアクセスすると、Googleのログインページにリダイレクトされます。IAPで許可したアカウントでログインすると、保護されたサービスに到達できます。

- サービスの片付け
検証後は課金を避けるため、terraform destroy でデプロイしたリソースを削除します。
コード
GCPのサンプルTerraformに手を加えた構成を掲載します。IAPクライアント・ブランド、外部HTTPSロードバランサー、Cloud Run、セキュリティポリシーを1セットで管理しています。
- main.tf
/**
* Copyright 2023 Google LLC
* Modifications Copyright 2024 Ryo M
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
locals {
gclb_create = var.custom_domain == null ? false : true
iap_sa_email = try(google_project_service_identity.iap_sa[0].email, "")
iap_brand = var.iap.enabled ? "projects/${module.project.number}/brands/${var.existing_iap_brand_id}" : null
}
module "project" {
source = "../../../modules/project"
billing_account = (var.project_create != null
? var.project_create.billing_account_id
: null
)
parent = (var.project_create != null
? var.project_create.parent
: null
)
name = var.project_id
services = [
"run.googleapis.com",
"compute.googleapis.com",
"iap.googleapis.com"
]
project_create = var.project_create != null
}
module "cloud_run" {
source = "../../../modules/cloud-run"
project_id = module.project.project_id
name = var.run_svc_name
region = var.region
containers = {
default = {
image = var.image
}
}
iam = {
"roles/run.invoker" = (local.gclb_create && var.iap.enabled
? ["serviceAccount:${local.iap_sa_email}"]
: ["allUsers"]
)
}
ingress_settings = var.ingress_settings
}
resource "google_compute_global_address" "default" {
count = local.gclb_create ? 1 : 0
project = module.project.project_id
name = "glb-ip"
}
module "glb" {
source = "../../../modules/net-lb-app-ext"
count = local.gclb_create ? 1 : 0
project_id = module.project.project_id
name = "glb"
address = google_compute_global_address.default[0].address
backend_service_configs = {
default = {
backends = [
{ backend = "neg-0" }
]
health_checks = []
port_name = "http"
security_policy = try(google_compute_security_policy.policy[0].name,
null)
iap_config = try({
oauth2_client_id = google_iap_client.iap_client[0].client_id,
oauth2_client_secret = google_iap_client.iap_client[0].secret
}, null)
}
}
health_check_configs = {}
neg_configs = {
neg-0 = {
cloudrun = {
region = var.region
target_service = {
name = var.run_svc_name
}
}
}
}
protocol = "HTTPS"
ssl_certificates = {
managed_configs = {
default = {
domains = [var.custom_domain]
}
}
}
}
resource "google_compute_security_policy" "policy" {
count = local.gclb_create && var.security_policy.enabled ? 1 : 0
name = "cloud-run-policy"
project = module.project.project_id
rule {
action = "deny(403)"
priority = 1000
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = var.security_policy.ip_blacklist
}
}
description = "Deny access to list of IPs"
}
rule {
action = "deny(403)"
priority = 900
match {
expr {
expression = "request.path.matches(\"${var.security_policy.path_blocked}\")"
}
}
description = "Deny access to specific URL paths"
}
rule {
action = "allow"
priority = "2147483647"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
description = "Default rule"
}
}
resource "google_iap_client" "iap_client" {
count = var.iap.enabled ? 1 : 0
display_name = var.iap.oauth2_client_name
brand = local.iap_brand
lifecycle {
precondition {
condition = var.existing_iap_brand_id != ""
error_message = "existing_iap_brand_id must be set when iap.enabled is true."
}
}
}
resource "google_iap_brand" "project_brand" {
count = var.iap.enabled && var.create_iap_brand ? 1 : 0
support_email = var.iap.email
application_title = var.iap.application_title
project = module.project.project_id
}
resource "google_iap_web_iam_member" "iap_iam" {
count = local.gclb_create && var.iap.enabled ? 1 : 0
project = module.project.project_id
role = "roles/iap.httpsResourceAccessor"
member = "user:${var.iap.email}"
}
resource "google_project_service_identity" "iap_sa" {
provider = google-beta
count = local.gclb_create && var.iap.enabled ? 1 : 0
project = module.project.project_id
service = "iap.googleapis.com"
}
output "iap_brand" {
value = local.iap_brand
}
output "iap_client" {
value = {
enabled = var.iap.enabled
id = var.iap.enabled ? google_iap_client.iap_client[0].id : null
display_name = var.iap.enabled ? google_iap_client.iap_client[0].display_name : null
client_id = var.iap.enabled ? google_iap_client.iap_client[0].client_id : null
}
}
output "project_number" {
value = module.project.number
}- variables.tf
/**
* Copyright 2023 Google LLC
* Modifications Copyright 2024 Ryo M
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
variable "project_id" {
type = string
description = "Project ID"
}
variable "project_create" {
type = object({
billing_account_id = string
parent = string
})
description = "Project creation parameters"
default = null
}
variable "run_svc_name" {
type = string
description = "Cloud Run service name"
}
variable "region" {
type = string
description = "Region for Cloud Run service"
}
variable "image" {
type = string
description = "Container image for Cloud Run service"
}
variable "ingress_settings" {
type = string
description = "Ingress settings for Cloud Run service"
default = "all"
}
variable "custom_domain" {
type = string
description = "Custom domain for the load balancer"
default = null
}
variable "security_policy" {
type = object({
enabled = bool
ip_blacklist = list(string)
path_blocked = string
})
description = "Security policy settings"
default = {
enabled = false
ip_blacklist = []
path_blocked = ""
}
}
variable "iap" {
type = object({
enabled = bool
email = string
oauth2_client_name = string
application_title = string
})
description = "IAP settings"
}
variable "create_iap_brand" {
type = bool
default = false
description = "Whether to create a new IAP brand"
}
variable "existing_iap_brand_id" {
type = string
description = "The ID of the existing IAP brand (usually the project number)"
}- terraform.tfvars
project_id = "your_project_id"
run_svc_name = "my-cloud-run-service"
region = "europe-west1"
image = "us-docker.pkg.dev/cloudrun/container/hello"
ingress_settings = "internal-and-cloud-load-balancing"
custom_domain = "your_domain"
security_policy = {
enabled = true
ip_blacklist = ["79.149.0.0/16"]
path_blocked = "/admin/*"
}
iap = {
enabled = true
email = "your_email"
oauth2_client_name = "Cloud Run IAP"
application_title = "My Application"
}
create_iap_brand = false
existing_iap_brand_id = "your brand"
project_create = null- outputs.tf
/**
* Copyright 2023 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
# Custom domain for the Load Balancer. I'd prefer getting the value from the
# SSL certificate but it is not exported as output
output "custom_domain" {
description = "Custom domain for the Load Balancer."
value = local.gclb_create ? var.custom_domain : "none"
}
output "default_URL" {
description = "Cloud Run service default URL."
value = module.cloud_run.service.status[0].url
}
output "load_balancer_ip" {
description = "LB IP that forwards to Cloud Run service."
value = local.gclb_create ? module.glb[0].address : "none"
}
Brand IDは、Method: projects.brands.list で取得できます。existing_iap_brand_id にはこの値(通常はプロジェクト番号ベースのブランドID)を設定します。
ハマりどころと勘所
- カスタムドメインを付けるだけでは不十分です。IAPは組織配下のリソースを前提とするため、Workspace / Cloud Identity 登録と組織作成が事実上の必須要件になります。着手前にこの依存関係を把握しておくと手戻りを防げます。
- IAPの有効化にはブランド(同意画面)とOAuthクライアントが必要です。ブランドが未作成だと
existing_iap_brand_idが空になり、google_iap_clientの precondition で弾かれます。既存ブランドがある場合はcreate_iap_brand = falseとしてIDを流用します。 - DNSのAレコード反映には時間差があります。疎通しない場合はTTLと伝播待ちを疑い、しばらく置いてから再アクセスしてください。
- 検証環境はロードバランサーやマネージド証明書などで課金が発生します。使い終わったら
terraform destroyで確実に片付けます。
その他参考
- Cloud Run と Identity-Aware Proxy で社内限定サービスを構築する
- CloudRunで動作するサービスをIAP(Identity Aware Proxy)で保護する構成をTerraformで実装する
さいごに
IAPを使えば、アプリを改修せずに認証・認可をインフラ層へ委ねた限定公開を構成できます。Workspace・Cloud Identity・DNS・Terraformが絡む依存関係さえ押さえれば、再現性の高いIaCとして運用に載せられます。クラウドの認証・限定公開やIaCのご相談は、お問い合わせから。