Cloud Run with IAP recipe (#3129)
Co-authored-by: Ludovico Magnocavallo <ludomagno@google.com>
This commit is contained in:
@@ -32,6 +32,7 @@ Due to the complexity of the underlying resources, changes to the configuration
|
||||
- [Deploying changes to load balancer configurations](#deploying-changes-to-load-balancer-configurations)
|
||||
- [Changing the Network Endpoint Group](#changing-the-network-endpoint-group)
|
||||
- [Updating SSL certificate](#updating-ssl-certificate)
|
||||
- [Recipes](#recipes)
|
||||
- [Files](#files)
|
||||
- [Variables](#variables)
|
||||
- [Outputs](#outputs)
|
||||
@@ -1035,6 +1036,10 @@ After provisioning this change, and verifying that the new certificate is provis
|
||||
|
||||
<!-- TFDOC OPTS files:1 -->
|
||||
<!-- BEGIN TFDOC -->
|
||||
## Recipes
|
||||
|
||||
- [Expose Cloud Run service with Global External Application Load Balancer protected by IAP](https://github.com/GoogleCloudPlatform/cloud-foundation-fabric/blob/master/modules/net-lb-app-ext/recipe-cloud-run-iap)
|
||||
|
||||
## Files
|
||||
|
||||
| name | description | resources |
|
||||
|
||||
68
modules/net-lb-app-ext/recipe-cloud-run-iap/README.md
Normal file
68
modules/net-lb-app-ext/recipe-cloud-run-iap/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Expose Cloud Run service with Global External Application Load Balancer protected by IAP
|
||||
|
||||
This recipe demonstrates how to expose a Cloud Run Service Global External Application Load Balancer protected by IAP.
|
||||
|
||||
The architecture deployed by this recipe is the one depicted below:
|
||||
|
||||

|
||||
|
||||
Note: Make sure that the email that you pass as support email for the IAP brand is the email of a group in which the identity executing terraform is a member with the role MANAGER. Otherwise an error will be raised. Also bear in mind only organization internal brands can be created using Terraform.
|
||||
|
||||
This recipe addresses common requirements of backends protected by IAP:
|
||||
|
||||
* CORS
|
||||
|
||||
When a browser sends a CORS preflight OPTIONS request, it typically doesn't include any authentication credentials (like IAP session cookies). Since IAP is designed to protect an application by requiring authentication, it often blocks these unauthenticated OPTIONS requests, returning an errorinstead of the necessary CORS headers. The browser then sees this as a CORS failure and blocks the subsequent actual request.
|
||||
Google Cloud's IAP has a setting, `access_settings.cors_settings.allow_http_options`, that needs to be set to true. This allows IAP to pass OPTIONS requests to your backend without requiring authentication. The backend application must then be configured to correctly respond to these OPTIONS requests with the appropriate CORS headers (e.g., Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers). This tells the browser that cross-origin requests are permitted.
|
||||
|
||||
* Programmatic access using a service account
|
||||
|
||||
To access a service exposed with Global External Application Load Balancer protected by IAP programmatically impersonating a service account:
|
||||
|
||||
* The service account to use for programmatic access must be granted the IAP-Secured Web App User role (`roles/iap.httpsResourceAccessor`) on the backend service of your Global External Application Load Balancer.
|
||||
|
||||
* To access the IAP-protected service from code impersonating a service account, an ID token signed issues for this one needs to be obtained. The key is to generate an ID token with the correct audience. The audience for an IAP-protected resource is the OAuth 2.0 Client ID that IAP uses.
|
||||
|
||||
To try out that programmatic access works for this particular service do the following you can run the command returned as output.
|
||||
<!-- BEGIN TFDOC -->
|
||||
## Variables
|
||||
|
||||
| name | description | type | required | default |
|
||||
|---|---|:---:|:---:|:---:|
|
||||
| [project_id](variables.tf#L39) | Project ID. | <code>string</code> | ✓ | |
|
||||
| [region](variables.tf#L44) | Region. | <code>string</code> | ✓ | |
|
||||
| [support_email](variables.tf#L49) | Support email for IAP brand. | <code>string</code> | ✓ | |
|
||||
| [_testing](variables.tf#L17) | Populate this variable to avoid triggering the data source. | <code title="object({ name = string number = number services_enabled = optional(list(string), []) })">object({…})</code> | | <code>null</code> |
|
||||
| [accesors](variables.tf#L27) | List of identities able to access the service via IAP (e.g. group:mygroup@myorg.com). | <code>list(string)</code> | | <code>[]</code> |
|
||||
| [impersonators](variables.tf#L33) | List of identities able to impersonate the service account for programmatica access. | <code>list(string)</code> | | <code>[]</code> |
|
||||
|
||||
## Outputs
|
||||
|
||||
| name | description | sensitive |
|
||||
|---|---|:---:|
|
||||
| [application_service_account_email](outputs.tf#L26) | Application service account email. | |
|
||||
| [command](outputs.tf#L31) | Command. | |
|
||||
| [oauth2_client_id](outputs.tf#L40) | OAuth client ID. | |
|
||||
| [url](outputs.tf#L45) | URL to access service exposed by IAP. | |
|
||||
<!-- END TFDOC -->
|
||||
## Tests
|
||||
|
||||
```hcl
|
||||
module "test" {
|
||||
source = "./fabric/modules/net-lb-app-ext/recipe-cloud-run-iap"
|
||||
project_id = "project-1"
|
||||
_testing = {
|
||||
name = "project-1"
|
||||
number = 1234567890
|
||||
}
|
||||
region = "europe-west1"
|
||||
support_email = "mygroup1@myorg.com"
|
||||
accesors = [
|
||||
"group:mygroup2@myorg.com"
|
||||
]
|
||||
impersonators = [
|
||||
"group:mygroup3@myorg.com"
|
||||
]
|
||||
}
|
||||
# tftest modules=6 resources=24
|
||||
```
|
||||
BIN
modules/net-lb-app-ext/recipe-cloud-run-iap/diagram.png
Normal file
BIN
modules/net-lb-app-ext/recipe-cloud-run-iap/diagram.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
152
modules/net-lb-app-ext/recipe-cloud-run-iap/main.tf
Normal file
152
modules/net-lb-app-ext/recipe-cloud-run-iap/main.tf
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
|
||||
locals {
|
||||
hostname = "${module.addresses.global_addresses.glb.address}.nip.io"
|
||||
url = "https://${local.hostname}"
|
||||
}
|
||||
|
||||
module "project" {
|
||||
source = "../../../modules/project"
|
||||
name = var.project_id
|
||||
project_reuse = {
|
||||
use_data_source = var._testing == null
|
||||
project_attributes = var._testing
|
||||
}
|
||||
services = [
|
||||
"cloudbuild.googleapis.com",
|
||||
"iap.googleapis.com",
|
||||
"run.googleapis.com"
|
||||
]
|
||||
}
|
||||
|
||||
module "application_service_account" {
|
||||
source = "../../../modules/iam-service-account"
|
||||
project_id = var.project_id
|
||||
name = "application"
|
||||
iam = {
|
||||
"roles/iam.serviceAccountTokenCreator" = var.impersonators
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_iap_brand" "iap_brand" {
|
||||
support_email = var.support_email
|
||||
application_title = "Test Application"
|
||||
project = module.project.id
|
||||
}
|
||||
|
||||
resource "google_iap_client" "iap_client" {
|
||||
display_name = "Test Client"
|
||||
brand = google_iap_brand.iap_brand.name
|
||||
}
|
||||
|
||||
module "backend_service" {
|
||||
source = "../../../modules/cloud-run-v2"
|
||||
project_id = module.project.id
|
||||
name = "backend"
|
||||
region = var.region
|
||||
containers = {
|
||||
hello = {
|
||||
image = "us-docker.pkg.dev/cloudrun/container/hello"
|
||||
}
|
||||
}
|
||||
iam = {
|
||||
"roles/run.invoker" = [
|
||||
module.project.service_agents.iap.iam_email
|
||||
]
|
||||
}
|
||||
deletion_protection = false
|
||||
service_account_create = true
|
||||
}
|
||||
|
||||
module "addresses" {
|
||||
source = "../../../modules/net-address"
|
||||
project_id = module.project.id
|
||||
global_addresses = {
|
||||
glb = {}
|
||||
}
|
||||
}
|
||||
|
||||
module "glb" {
|
||||
source = "../../../modules/net-lb-app-ext"
|
||||
project_id = module.project.id
|
||||
name = "glb"
|
||||
protocol = "HTTPS"
|
||||
use_classic_version = false
|
||||
forwarding_rules_config = {
|
||||
"" = {
|
||||
address = (
|
||||
module.addresses.global_addresses.glb.address
|
||||
)
|
||||
}
|
||||
}
|
||||
backend_service_configs = {
|
||||
default = {
|
||||
backends = [
|
||||
{ backend = "neg-backend" }
|
||||
]
|
||||
health_checks = []
|
||||
iap_config = {
|
||||
oauth2_client_id = google_iap_client.iap_client.client_id
|
||||
oauth2_client_secret = google_iap_client.iap_client.secret
|
||||
}
|
||||
port_name = ""
|
||||
}
|
||||
}
|
||||
health_check_configs = {}
|
||||
neg_configs = {
|
||||
neg-backend = {
|
||||
cloudrun = {
|
||||
region = var.region
|
||||
target_service = {
|
||||
name = "backend"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ssl_certificates = {
|
||||
managed_configs = {
|
||||
default = {
|
||||
domains = [local.hostname]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource "google_iap_web_backend_service_iam_binding" "iam_bindings" {
|
||||
project = module.project.id
|
||||
web_backend_service = module.glb.backend_service_names["default"]
|
||||
role = "roles/iap.httpsResourceAccessor"
|
||||
members = concat(
|
||||
var.accesors,
|
||||
[
|
||||
module.application_service_account.iam_email
|
||||
])
|
||||
}
|
||||
|
||||
resource "google_iap_settings" "iap_settings" {
|
||||
name = "projects/${module.project.number}/iap_web/forwarding_rule/services/${module.glb.forwarding_rules[""].name}"
|
||||
access_settings {
|
||||
cors_settings {
|
||||
allow_http_options = true
|
||||
}
|
||||
oauth_settings {
|
||||
programmatic_clients = [
|
||||
google_iap_client.iap_client.client_id
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
48
modules/net-lb-app-ext/recipe-cloud-run-iap/outputs.tf
Normal file
48
modules/net-lb-app-ext/recipe-cloud-run-iap/outputs.tf
Normal file
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
|
||||
locals {
|
||||
command_tpl = <<EOT
|
||||
curl -v -H "Authorization: Bearer $(gcloud auth print-identity-token \
|
||||
--audiences $${aud} \
|
||||
--impersonate-service-account $${sa} \
|
||||
--include-email)" $${url}
|
||||
EOT
|
||||
}
|
||||
|
||||
output "application_service_account_email" {
|
||||
description = "Application service account email."
|
||||
value = module.application_service_account.email
|
||||
}
|
||||
|
||||
output "command" {
|
||||
description = "Command."
|
||||
value = templatestring(local.command_tpl, {
|
||||
aud = google_iap_client.iap_client.client_id
|
||||
sa = module.application_service_account.email
|
||||
url = local.url
|
||||
})
|
||||
}
|
||||
|
||||
output "oauth2_client_id" {
|
||||
description = "OAuth client ID."
|
||||
value = google_iap_client.iap_client.client_id
|
||||
}
|
||||
|
||||
output "url" {
|
||||
description = "URL to access service exposed by IAP."
|
||||
value = local.url
|
||||
}
|
||||
52
modules/net-lb-app-ext/recipe-cloud-run-iap/variables.tf
Normal file
52
modules/net-lb-app-ext/recipe-cloud-run-iap/variables.tf
Normal file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright 2025 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.
|
||||
*/
|
||||
|
||||
variable "_testing" {
|
||||
description = "Populate this variable to avoid triggering the data source."
|
||||
type = object({
|
||||
name = string
|
||||
number = number
|
||||
services_enabled = optional(list(string), [])
|
||||
})
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "accesors" {
|
||||
description = "List of identities able to access the service via IAP (e.g. group:mygroup@myorg.com)."
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "impersonators" {
|
||||
description = "List of identities able to impersonate the service account for programmatica access."
|
||||
type = list(string)
|
||||
default = []
|
||||
}
|
||||
|
||||
variable "project_id" {
|
||||
description = "Project ID."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "region" {
|
||||
description = "Region."
|
||||
type = string
|
||||
}
|
||||
|
||||
variable "support_email" {
|
||||
description = "Support email for IAP brand."
|
||||
type = string
|
||||
}
|
||||
Reference in New Issue
Block a user