IAPでサービスを限定公開する

September 08, 2024

目次

概要

Cloud Runで立てたサービスに指定したユーザーだけログインできるようにしたかったので、Cloud IAPを試しました。
簡単にできるだろうと思い意気揚々と進めていたら、沼でしたので、備忘録として残しておきます。

後述しますが、カスタムドメイン取得だけで万事OKだと思ったのですが、terraformでエラーを見るとIAPの登録が必要とわかりました。 そして、IAPを利用するためには、Goolge Workspace、Cloud Identityへの登録、取得したカスタムドメインのユーザーでGCPを登録し直して、組織の利用を有効化して、、、という長い道のりがあるのですが、 やると決めてしまったので、意を決して突き進んだという経緯があります。
お陰で日曜が丸々潰れました。

手順

  1. カスタムドメインの取得(有料)

お名前ドットコムとかからドメインを購入します。

  1. Goolge Workspaceへの登録(有料)
  1. Cloud Identityへの登録(有料)
  1. Google Cloudの利用登録(無料枠あり)

作成したカスタムドメインのユーザーアカウントで、Google Cloudの利用登録を行います。 そして、組織の作成をします。

  1. TXTレコードの追加

Cloud Identity利用にあたって、Google側で払い出される確認コードを、レジストラの管理画面からDNSのTXTレコードに追加する必要があります。

  1. サンプルコードのダウンロード、terraformのインストール

GCPが提供している公式のドキュメント『Cloud Run Explore』を参考にします。
terraformのコードもこちらにあります。

  1. サービスのデプロイ

tfvarsファイルに入力値として与える必要事項を入力して、terraform applyします。するとGCPへサービスのデプロイがされます。

  1. Aレコードの追加

作成されたロードバランサーのIPアドレスを、サブドメインにマッピングします。 そのためにレジストラのDNSにAレコードを追加します。
このマッピング作業は、お名前ドットコムの場合、数時間から24時間程度掛かります。 私の場合、登録してから1、2時間で繋がりました。

  1. サブドメインにアクセスする

指定したサブドメインに何度かアクセスを試みていると、お馴染みのGoogleのログインページにリダイレクトされますので、指定したアカウントでログインしてください。
ログインに成功すると、こんな画面が見れます。

run-iap

10.サービスの片付け
課金を避けるため、terraform destoryでデプロイしたサービスを削除します。

コード

GCPのサンプルのterraformファイルに手を加えたので、こちらに載せておきます。

  • main.tf
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
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
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
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を使えばわかります。

その他参考

さいごに

ブログの体裁がみにくくてすみません。フロントエンド勉強します。


Please share it if you like!

Profile picture

Written by mtzk who lives and works as a programmer in Tokyo.