diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/.gitignore b/samples/x-nb-psc-sb-psc-ilb-crun/.gitignore
new file mode 100644
index 0000000..6811358
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/.gitignore
@@ -0,0 +1,8 @@
+.env
+.terraform.lock.hcl
+terraform.tfstate.backup
+terraform.tfstate
+.terraform.tfstate.lock.info
+.DS_Store
+AM-SetAudience.xml
+.terraform
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/README.md b/samples/x-nb-psc-sb-psc-ilb-crun/README.md
new file mode 100644
index 0000000..7c55f3d
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/README.md
@@ -0,0 +1,119 @@
+# How to consume internal Cloud Run apps from Apigee X using a single Service Attachment
+
+## Overview
+
+This sample emphasises on the southbound connectivity based on
+Private Service Connect (PSC) to communicate with internal Cloud Run
+applications through an internal HTTPS (L7) load balancer.
+
+
+
+An internal HTTPS load balancer (L7 ILB) is used to serve several Cloud Run apps using Serverless NEG and URL Mask.
+
+A serverless NEG backend can point to several Cloud Run services.
+
+A URL mask is a template of your URL schema. The serverless NEG uses this template to map the request to the appropriate service.
+
+The L7 ILB is accessed through a PSC Service Attachment that can be reached by an ApigeeX instance via a PSC endpoint Attachment. These two attachments (Endpoint and Service attachments) must be part of the same GCP region.
+
+Apigee X uses a target endpoint to reach the L7 ILB through the PSC channel. This target endpoint is configured in HTTPS and uses a dedicated hostname (the hostname of the l7 ILB). Therefore we need two DNS resolutions:
+
+- The first on Apigee X (target endpoint) to point to the PSC endpoint attachment: a private Cloud DNS (A record) and a DNS peering are required to manage the resolution. This private zone is configured on the consumer VPC (named apigee-network in the picture above) that is peered with the Apigee X VPC. The DNS peering is configured between the two VPCs for a particular domain (iloveapis.io in the example above)
+- The second on the PSC service attachment to point to the L7 ILB: a private Cloud DNS (A record) is required to manage the resolution. This private zone is configured on the VPC (named ilb-network in the picture above). Note that the L7 ILB presents an SSL certificate used to establish a secured communication
+
+An identity token (ID token) is required to consume the different Cloud Run services.
+This ID token is generated on Apigee X and transmitted as a bearer token to the Cloud Run apps.
+
+A service account (```sa_apigee_apiproxy```) is created for this purpose and used to deplpy the API proxy (```cloudrun-api-v1```) on the Apigee X instance.
+The permission of this service account is ``` roles/run.invoker ```
+
+## Setup Instructions
+
+You can implement a full or partial install of the sample
+
+### Full installation: Apigee X (PSC NB) + Southbound connectivity to Cloud Run apps using PSC
+
+For this type of installation, we consider that an Apigee X instance has already been provisionned.
+
+### Partial installation: Southbound connectivity to Cloud Run apps using PSC
+
+For this type of installation, we consider that an Apigee X instance has already been provisionned.
+
+We do not care about the existing northbopund connectivity, which can rely on VPC peering or PSC.
+
+Our focus here is the deployment of 3 (basic) internal Cloud Run services and the network components
+to consume them from the Apigee X instance. Apigee is of course used to expose these services
+as APIs and API products.
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [google](#requirement\_google) | 4.58.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [google](#provider\_google) | 4.58.0 |
+| [null](#provider\_null) | 3.2.1 |
+| [tls](#provider\_tls) | 4.0.4 |
+
+## Modules
+
+| Name | Source | Version |
+|------|--------|---------|
+| [cloud\_run](#module\_cloud\_run) | GoogleCloudPlatform/cloud-run/google | ~> 0.2.0 |
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [google_apigee_endpoint_attachment.endpoint_attachment](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/apigee_endpoint_attachment) | resource |
+| [google_artifact_registry_repository.docker-main](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/artifact_registry_repository) | resource |
+| [google_compute_address.default](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_address) | resource |
+| [google_compute_forwarding_rule.default](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_forwarding_rule) | resource |
+| [google_compute_network.default](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_network) | resource |
+| [google_compute_region_backend_service.default](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_region_backend_service) | resource |
+| [google_compute_region_network_endpoint_group.cloudrun_neg](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_region_network_endpoint_group) | resource |
+| [google_compute_region_ssl_certificate.default](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_region_ssl_certificate) | resource |
+| [google_compute_region_target_https_proxy.default](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_region_target_https_proxy) | resource |
+| [google_compute_region_url_map.https_lb](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_region_url_map) | resource |
+| [google_compute_service_attachment.psc_ilb_service_attachment](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_service_attachment) | resource |
+| [google_compute_subnetwork.default](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_subnetwork) | resource |
+| [google_compute_subnetwork.proxy_subnet](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_subnetwork) | resource |
+| [google_compute_subnetwork.psc_ilb_nat](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/compute_subnetwork) | resource |
+| [google_dns_managed_zone.private-zone-apigee](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/dns_managed_zone) | resource |
+| [google_dns_managed_zone.private-zone-ilb](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/dns_managed_zone) | resource |
+| [google_dns_record_set.a-apigee](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/dns_record_set) | resource |
+| [google_dns_record_set.a-ilb](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/dns_record_set) | resource |
+| [google_project_iam_member.sa_apigee_apiproxy](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/project_iam_member) | resource |
+| [google_project_service.gcp_services](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/project_service) | resource |
+| [google_service_account.service_account_apiproxy](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/service_account) | resource |
+| [google_service_networking_peered_dns_domain.apigee](https://registry.terraform.io/providers/hashicorp/google/4.58.0/docs/resources/service_networking_peered_dns_domain) | resource |
+| [null_resource.login_image_build](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
+| [null_resource.search_image_build](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
+| [null_resource.translate_image_build](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource |
+| [tls_private_key.default](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/private_key) | resource |
+| [tls_self_signed_cert.default](https://registry.terraform.io/providers/hashicorp/tls/latest/docs/resources/self_signed_cert) | resource |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [apigee\_endpoint\_attachment](#input\_apigee\_endpoint\_attachment) | Apigee endpoint attachment value. | `string` | n/a | yes |
+| [consumer\_vpc](#input\_consumer\_vpc) | Consumer VPC network name. | `string` | n/a | yes |
+| [gcp\_project\_id](#input\_gcp\_project\_id) | The GCP project ID to create the gcp resources in. | `string` | n/a | yes |
+| [gcp\_region](#input\_gcp\_region) | The GCP region to create the gcp resources in. | `string` | n/a | yes |
+| [gcp\_service\_list](#input\_gcp\_service\_list) | The list of required Google apis | `list(string)` |
[
"artifactregistry.googleapis.com",
"cloudbuild.googleapis.com",
"run.googleapis.com",
"dns.googleapis.com",
"compute.googleapis.com",
"logging.googleapis.com",
"monitoring.googleapis.com"
]
| no |
+| [gcp\_zone](#input\_gcp\_zone) | The GCP zone to create the gcp resources in. | `string` | n/a | yes |
+| [repository\_id](#input\_repository\_id) | Repository id of the artifact registry. | `string` | n/a | yes |
+| [url\_mask](#input\_url\_mask) | URL mask of the serverless network endpoint group (neg). | `string` | n/a | yes |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [service\_urls](#output\_service\_urls) | Cloud Run service URLs |
+
\ No newline at end of file
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/cleanup.sh b/samples/x-nb-psc-sb-psc-ilb-crun/cleanup.sh
new file mode 100755
index 0000000..c8a8b1c
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/cleanup.sh
@@ -0,0 +1,44 @@
+#!/bin/sh
+# 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.
+
+set -e
+
+SCRIPTPATH="$( cd "$(dirname "$0")" || exit >/dev/null 2>&1 ; pwd -P )"
+
+# Ask for input parameters if they are not set
+
+[ -z "$GCP_PROJECT_ID" ] && printf "GCP project id: " && read -r GCP_PROJECT_ID
+
+###
+### destroy_common_gcp_resources()
+###
+destroy_common_gcp_resources() {
+
+ terraform init
+ terraform destroy --var-file="./input.tfvars" \
+ -var "gcp_project_id=${GCP_PROJECT_ID}"
+ -auto-approve
+}
+
+##
+###
+### destroy_common_gcp_resources()
+###
+main() {
+ cd ${SCRIPTPATH}
+ destroy_common_gcp_resources
+}
+
+main "${@}"
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/cloudrun-api-v1.xml b/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/cloudrun-api-v1.xml
new file mode 100644
index 0000000..fe974a4
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/cloudrun-api-v1.xml
@@ -0,0 +1,14 @@
+
+
+
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/policies/RF-404NotFound.xml b/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/policies/RF-404NotFound.xml
new file mode 100644
index 0000000..f01f7b8
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/policies/RF-404NotFound.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+ {"error":"not_found"}
+ 404
+ Not Found
+
+
+
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/proxies/default.xml b/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/proxies/default.xml
new file mode 100644
index 0000000..dad25ed
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/proxies/default.xml
@@ -0,0 +1,55 @@
+
+
+
+
+
+
+ (proxy.pathsuffix MatchesPath "/login") and (request.verb = "GET")
+
+
+
+
+ (proxy.pathsuffix MatchesPath "/search") and (request.verb = "GET")
+
+
+
+
+ (proxy.pathsuffix MatchesPath "/translate") and (request.verb = "GET")
+
+
+
+
+
+
+ RF-404NotFound
+
+
+
+
+
+
+
+
+ AM-SetAudience
+
+
+
+
+
+ /v1/crun
+
+
+ default
+
+
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/targets/default.xml b/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/targets/default.xml
new file mode 100644
index 0000000..ae8107c
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/cloudrun-api-v1/apiproxy/targets/default.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+ https://internal.example.com
+
+
+
+
+
+
+
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/input.tfvars b/samples/x-nb-psc-sb-psc-ilb-crun/input.tfvars
new file mode 100644
index 0000000..cd84f21
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/input.tfvars
@@ -0,0 +1,41 @@
+/**
+ * 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.
+ */
+
+/**
+ ** gcp variables
+*/
+gcp_region = "europe-west1"
+gcp_zone = "europe-west1-b"
+
+/**
+ ** artifact registry variables
+*/
+repository_id = "docker-main"
+
+/**
+ ** serverless neg variables
+*/
+url_mask = "/"
+
+/**
+ ** name of the apigee endpoint attachment
+*/
+apigee_endpoint_attachment = "pscendpoint"
+
+/**
+ ** consumer vpc network name
+*/
+consumer_vpc = "apigee-network"
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/main.tf b/samples/x-nb-psc-sb-psc-ilb-crun/main.tf
new file mode 100644
index 0000000..f12063d
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/main.tf
@@ -0,0 +1,342 @@
+/**
+ * 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.
+ */
+
+variable "gcp_service_list" {
+ description = "The list of required Google apis"
+ type = list(string)
+ default = [
+ "artifactregistry.googleapis.com",
+ "cloudbuild.googleapis.com",
+ "run.googleapis.com",
+ "dns.googleapis.com",
+ "compute.googleapis.com",
+ "logging.googleapis.com",
+ "monitoring.googleapis.com"
+ ]
+}
+
+resource "google_project_service" "gcp_services" {
+ for_each = toset(var.gcp_service_list)
+ project = var.gcp_project_id
+ service = each.key
+ disable_dependent_services = true
+}
+
+resource "google_artifact_registry_repository" "docker-main" {
+ location = var.gcp_region
+ repository_id = var.repository_id
+ description = "Main Docker Repository for Cloud Run"
+ format = "DOCKER"
+ depends_on = [
+ google_project_service.gcp_services
+ ]
+}
+
+resource "null_resource" "login_image_build" {
+ triggers = {
+ always_run = timestamp()
+ }
+
+ provisioner "local-exec" {
+ working_dir = "${path.module}/services/login"
+ command = "gcloud builds submit --tag ${var.gcp_region}-docker.pkg.dev/${var.gcp_project_id}/docker-main/login"
+ }
+
+ depends_on = [
+ google_artifact_registry_repository.docker-main
+ ]
+}
+
+resource "null_resource" "search_image_build" {
+ triggers = {
+ always_run = timestamp()
+ }
+
+ provisioner "local-exec" {
+ working_dir = "${path.module}/services/search"
+ command = "gcloud builds submit --tag ${var.gcp_region}-docker.pkg.dev/${var.gcp_project_id}/docker-main/search"
+ }
+
+ depends_on = [
+ google_artifact_registry_repository.docker-main
+ ]
+}
+
+resource "null_resource" "translate_image_build" {
+ triggers = {
+ always_run = timestamp()
+ }
+
+ provisioner "local-exec" {
+ working_dir = "${path.module}/services/translate"
+ command = "gcloud builds submit --tag ${var.gcp_region}-docker.pkg.dev/${var.gcp_project_id}/docker-main/translate"
+ }
+
+ depends_on = [
+ google_artifact_registry_repository.docker-main
+ ]
+}
+
+module "cloud_run" {
+ source = "GoogleCloudPlatform/cloud-run/google"
+ version = "~> 0.2.0"
+
+ #3 Cloud Run internal services
+ for_each = toset([
+ "login",
+ "search",
+ "translate"
+ ])
+
+ # Required variables
+ service_name = each.key
+ project_id = var.gcp_project_id
+ location = var.gcp_region
+ image = "${var.gcp_region}-docker.pkg.dev/${var.gcp_project_id}/docker-main/${each.key}"
+ service_annotations = {
+ "run.googleapis.com/ingress": "internal"
+ }
+
+ depends_on = [
+ google_project_service.gcp_services,
+ null_resource.login_image_build,
+ null_resource.search_image_build,
+ null_resource.translate_image_build
+ ]
+
+}
+
+# VPC network
+resource "google_compute_network" "default" {
+ name = "l7-ilb-network"
+ auto_create_subnetworks = false
+}
+
+# Proxy-only subnet
+resource "google_compute_subnetwork" "proxy_subnet" {
+ name = "l7-ilb-proxy-subnet"
+ ip_cidr_range = "10.0.0.0/24"
+ region = var.gcp_region
+ purpose = "REGIONAL_MANAGED_PROXY"
+ role = "ACTIVE"
+ network = google_compute_network.default.id
+}
+
+# Backend subnet
+resource "google_compute_subnetwork" "default" {
+ name = "l7-ilb-subnet"
+ ip_cidr_range = "10.0.1.0/24"
+ region = var.gcp_region
+ network = google_compute_network.default.id
+}
+
+# Reserved internal address
+resource "google_compute_address" "default" {
+ name = "l7-ilb-ip"
+ subnetwork = google_compute_subnetwork.default.id
+ address_type = "INTERNAL"
+ address = "10.0.1.5"
+ region = var.gcp_region
+ project = var.gcp_project_id
+}
+
+# Regional forwarding rule
+resource "google_compute_forwarding_rule" "default" {
+ project = var.gcp_project_id
+ name = "psc-l7-ilb-forwarding-rule"
+ region = var.gcp_region
+ depends_on = [google_compute_subnetwork.proxy_subnet]
+ ip_address = google_compute_address.default.id
+ load_balancing_scheme = "INTERNAL_MANAGED"
+ port_range = "443"
+ target = google_compute_region_target_https_proxy.default.id
+ network = google_compute_network.default.id
+ subnetwork = google_compute_subnetwork.default.id
+ network_tier = "PREMIUM"
+}
+
+# Self-signed regional SSL certificate for testing
+resource "tls_private_key" "default" {
+ algorithm = "RSA"
+ rsa_bits = 2048
+}
+
+resource "tls_self_signed_cert" "default" {
+ private_key_pem = tls_private_key.default.private_key_pem
+
+ # Certificate expires after 100 days.
+ validity_period_hours = 2400
+
+ # Generate a new certificate if Terraform is run within three
+ # hours of the certificate's expiration time.
+ early_renewal_hours = 3
+
+ # Reasonable set of uses for a server SSL certificate.
+ allowed_uses = [
+ "key_encipherment",
+ "digital_signature",
+ "server_auth",
+ ]
+ dns_names = ["internal.example.com"]
+ subject {
+ common_name = "internal.example.com"
+ organization = "Exo, Inc"
+ }
+}
+
+resource "google_compute_region_ssl_certificate" "default" {
+ name_prefix = "exco-sslcert-"
+ private_key = tls_private_key.default.private_key_pem
+ certificate = tls_self_signed_cert.default.cert_pem
+ region = var.gcp_region
+ lifecycle {
+ create_before_destroy = true
+ }
+}
+
+# Regional target HTTPS proxy
+resource "google_compute_region_target_https_proxy" "default" {
+ name = "l7-ilb-target-https-proxy"
+ region = var.gcp_region
+ url_map = google_compute_region_url_map.https_lb.id
+ ssl_certificates = [google_compute_region_ssl_certificate.default.self_link]
+}
+
+# Regional URL map
+resource "google_compute_region_url_map" "https_lb" {
+ name = "l7-ilb-regional-url-map"
+ region = var.gcp_region
+ default_service = google_compute_region_backend_service.default.id
+}
+
+resource "google_compute_region_backend_service" "default" {
+ load_balancing_scheme = "INTERNAL_MANAGED"
+ backend {
+ group = google_compute_region_network_endpoint_group.cloudrun_neg.id
+ balancing_mode = "UTILIZATION"
+ }
+ region = var.gcp_region
+ name = "region-backend-service"
+ protocol = "HTTPS"
+}
+
+resource "google_compute_region_network_endpoint_group" "cloudrun_neg" {
+ name = "cloudrun-neg"
+ network_endpoint_type = "SERVERLESS"
+ region = var.gcp_region
+ cloud_run {
+ url_mask = var.url_mask
+ }
+}
+
+resource "google_compute_subnetwork" "psc_ilb_nat" {
+ name = "psc-ilb-nat"
+ region = var.gcp_region
+ network = google_compute_network.default.id
+ purpose = "PRIVATE_SERVICE_CONNECT"
+ ip_cidr_range = "10.75.0.0/28"
+}
+
+resource "google_compute_service_attachment" "psc_ilb_service_attachment" {
+ name = "psc-attachment-ilb"
+ region = var.gcp_region
+ description = "Service attachment for ilb configured with Terraform"
+ project = var.gcp_project_id
+ enable_proxy_protocol = false
+ connection_preference = "ACCEPT_AUTOMATIC"
+ nat_subnets = [google_compute_subnetwork.psc_ilb_nat.id]
+ target_service = google_compute_forwarding_rule.default.id
+}
+
+resource "google_apigee_endpoint_attachment" "endpoint_attachment" {
+ org_id = "organizations/${var.gcp_project_id}"
+ endpoint_attachment_id = var.apigee_endpoint_attachment
+ location = var.gcp_region
+ service_attachment = google_compute_service_attachment.psc_ilb_service_attachment.id
+}
+
+# Service account used by apigee apiproxy to invoke a cloud run app using id token
+resource "google_service_account" "service_account_apiproxy" {
+ account_id = "apigee-apiproxy"
+ display_name = "invoke cloud run app from apigee apiproxy"
+}
+
+locals {
+ service_account_a = "serviceAccount:apigee-apiproxy@${var.gcp_project_id}.iam.gserviceaccount.com"
+ }
+
+resource "google_project_iam_member" "sa_apigee_apiproxy" {
+ for_each = toset([
+ "roles/run.invoker"
+ ])
+ role = each.key
+ project = var.gcp_project_id
+ member = local.service_account_a
+ depends_on = [google_service_account.service_account_apiproxy]
+}
+
+resource "google_dns_managed_zone" "private-zone-apigee" {
+ name = "private-zone-apigee"
+ dns_name = "example.com."
+ description = "ExCo private DNS zone (Apigee)"
+ visibility = "private"
+ private_visibility_config {
+ networks {
+ network_url = "projects/${var.gcp_project_id}/global/networks/${var.consumer_vpc}"
+ }
+ }
+ depends_on = [
+ google_project_service.gcp_services
+ ]
+}
+
+resource "google_dns_managed_zone" "private-zone-ilb" {
+ name = "private-zone-ilb"
+ dns_name = "example.com."
+ description = "ExCo private DNS zone (L7 ILB)"
+ visibility = "private"
+ private_visibility_config {
+ networks {
+ network_url = "projects/${var.gcp_project_id}/global/networks/l7-ilb-network"
+ }
+ }
+ depends_on = [
+ google_project_service.gcp_services
+ ]
+}
+
+resource "google_dns_record_set" "a-apigee" {
+ name = "internal.${google_dns_managed_zone.private-zone-apigee.dns_name}"
+ managed_zone = google_dns_managed_zone.private-zone-apigee.name
+ type = "A"
+ ttl = 300
+ rrdatas = [google_apigee_endpoint_attachment.endpoint_attachment.host]
+}
+
+resource "google_dns_record_set" "a-ilb" {
+ name = "internal.${google_dns_managed_zone.private-zone-ilb.dns_name}"
+ managed_zone = google_dns_managed_zone.private-zone-ilb.name
+ type = "A"
+ ttl = 300
+ rrdatas = ["10.0.1.5"]
+}
+
+resource "google_service_networking_peered_dns_domain" "apigee" {
+ project = var.gcp_project_id
+ name = "apigee-dns-peering"
+ network = var.consumer_vpc
+ dns_suffix = google_dns_managed_zone.private-zone-apigee.dns_name
+}
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/outputs.tf b/samples/x-nb-psc-sb-psc-ilb-crun/outputs.tf
new file mode 100644
index 0000000..4d384de
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/outputs.tf
@@ -0,0 +1,20 @@
+/**
+ * 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.
+ */
+
+output "service_urls" {
+ description = "Cloud Run service URLs"
+ value = values(module.cloud_run)[*].service_url
+}
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/pictures/overview.png b/samples/x-nb-psc-sb-psc-ilb-crun/pictures/overview.png
new file mode 100644
index 0000000..bb46da0
Binary files /dev/null and b/samples/x-nb-psc-sb-psc-ilb-crun/pictures/overview.png differ
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/pipeline.sh b/samples/x-nb-psc-sb-psc-ilb-crun/pipeline.sh
new file mode 100755
index 0000000..0faf64f
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/pipeline.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+# 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.
+
+set -e
+
+SCRIPTPATH="$( cd "$(dirname "$0")" || exit >/dev/null 2>&1 ; pwd -P )"
+
+# Ask for input parameters if they are not set
+
+[ -z "$GCP_PROJECT_ID" ] && printf "GCP project id: " && read -r GCP_PROJECT_ID
+[ -z "$APIGEE_X_ORG" ] && printf "Apigee X Organization: " && read -r APIGEE_X_ORG
+[ -z "$APIGEE_X_ENV" ] && printf "Apigee X Environment: " && read -r APIGEE_X_ENV
+[ -z "$APIGEE_X_HOSTNAME" ] && printf "Apigee X Hostname: " && read -r APIGEE_X_HOSTNAME
+
+###
+### create_common_gcp_resources()
+###
+create_common_gcp_resources() {
+
+ terraform init
+
+ terraform plan -var "gcp_project_id=${GCP_PROJECT_ID}" \
+ --var-file=./input.tfvars
+
+ terraform apply -var "gcp_project_id=${GCP_PROJECT_ID}" \
+ --var-file=./input.tfvars \
+ -auto-approve
+}
+
+##
+###
+### create_common_gcp_resources()
+###
+main() {
+ cd "$SCRIPTPATH"
+ create_common_gcp_resources
+ # Cloud Run app url
+ APP_URL=$(terraform output -json service_urls | jq -r '.[0]')
+
+ # dynamic set of the audience
+ CLOUDRUN_APP_SUFFIX=$(echo "${APP_URL}" | sed "s/https:\/\/login//")
+ export CLOUDRUN_APP_SUFFIX
+ envsubst < "$SCRIPTPATH"/templates/AM-SetAudience.template.xml > "$SCRIPTPATH"/cloudrun-api-v1/apiproxy/policies/AM-SetAudience.xml
+
+ # deploy the Apigee api proxy
+ APIGEE_TOKEN="$(gcloud config config-helper --force-auth-refresh --format json | jq -r '.credential.access_token')"
+ SA_EMAIL="apigee-apiproxy@$GCP_PROJECT_ID.iam.gserviceaccount.com"
+ sackmesser deploy --googleapi \
+ -o "$APIGEE_X_ORG" \
+ -e "$APIGEE_X_ENV" \
+ -t "$APIGEE_TOKEN" \
+ -h "$APIGEE_X_HOSTNAME" \
+ -d "$SCRIPTPATH"/cloudrun-api-v1 \
+ --deployment-sa "$SA_EMAIL"
+}
+
+main "${@}"
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/provider.tf b/samples/x-nb-psc-sb-psc-ilb-crun/provider.tf
new file mode 100644
index 0000000..6f28e18
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/provider.tf
@@ -0,0 +1,30 @@
+/**
+ * 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.
+ */
+
+terraform {
+ required_providers {
+ google = {
+ source = "hashicorp/google"
+ version = "4.58.0"
+ }
+ }
+}
+
+provider "google" {
+ project = var.gcp_project_id
+ region = var.gcp_region
+ zone = var.gcp_zone
+}
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/login/.dockerignore b/samples/x-nb-psc-sb-psc-ilb-crun/services/login/.dockerignore
new file mode 100644
index 0000000..29d6828
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/login/.dockerignore
@@ -0,0 +1,3 @@
+node_modules
+npm-debug.log
+
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/login/Dockerfile b/samples/x-nb-psc-sb-psc-ilb-crun/services/login/Dockerfile
new file mode 100644
index 0000000..ebfe054
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/login/Dockerfile
@@ -0,0 +1,32 @@
+# 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.
+FROM node:16
+
+# Create app directory
+WORKDIR /usr/src/app
+
+# Install app dependencies
+# A wildcard is used to ensure both package.json AND package-lock.json are copied
+# where available (npm@5+)
+COPY package*.json ./
+
+RUN npm install
+# If you are building your code for production
+# RUN npm ci --only=production
+
+# Bundle app source
+COPY . .
+
+EXPOSE 8080
+CMD [ "node", "index.js" ]
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/login/index.js b/samples/x-nb-psc-sb-psc-ilb-crun/services/login/index.js
new file mode 100644
index 0000000..e56ce8d
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/login/index.js
@@ -0,0 +1,31 @@
+/**
+ 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
+
+ https://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.
+
+*/
+const express = require('express');
+const app = express();
+
+app.get('/login', (req, res) => {
+ const name = process.env.NAME || 'Login';
+ res.json({
+ service: "login",
+ id: "123-abc"
+ });
+});
+
+const port = parseInt(process.env.PORT) || 8080;
+app.listen(port, () => {
+ console.log(`Login service: listening on port ${port}`);
+});
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/login/package.json b/samples/x-nb-psc-sb-psc-ilb-crun/services/login/package.json
new file mode 100644
index 0000000..640456d
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/login/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "login",
+ "description": "Simple Login service in Node",
+ "version": "1.0.0",
+ "private": true,
+ "main": "index.js",
+ "scripts": {
+ "start": "node index.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "author": "Google LLC",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "express": "^4.17.1"
+ }
+}
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/search/.dockerignore b/samples/x-nb-psc-sb-psc-ilb-crun/services/search/.dockerignore
new file mode 100644
index 0000000..29d6828
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/search/.dockerignore
@@ -0,0 +1,3 @@
+node_modules
+npm-debug.log
+
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/search/Dockerfile b/samples/x-nb-psc-sb-psc-ilb-crun/services/search/Dockerfile
new file mode 100644
index 0000000..ebfe054
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/search/Dockerfile
@@ -0,0 +1,32 @@
+# 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.
+FROM node:16
+
+# Create app directory
+WORKDIR /usr/src/app
+
+# Install app dependencies
+# A wildcard is used to ensure both package.json AND package-lock.json are copied
+# where available (npm@5+)
+COPY package*.json ./
+
+RUN npm install
+# If you are building your code for production
+# RUN npm ci --only=production
+
+# Bundle app source
+COPY . .
+
+EXPOSE 8080
+CMD [ "node", "index.js" ]
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/search/index.js b/samples/x-nb-psc-sb-psc-ilb-crun/services/search/index.js
new file mode 100644
index 0000000..234d0a5
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/search/index.js
@@ -0,0 +1,31 @@
+/**
+ 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
+
+ https://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.
+
+*/
+const express = require('express');
+const app = express();
+
+app.get('/search', (req, res) => {
+ const name = process.env.NAME || 'Search';
+ res.json({
+ service: "search",
+ id: "456-def"
+ });
+});
+
+const port = parseInt(process.env.PORT) || 8080;
+app.listen(port, () => {
+ console.log(`Search service: listening on port ${port}`);
+});
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/search/package.json b/samples/x-nb-psc-sb-psc-ilb-crun/services/search/package.json
new file mode 100644
index 0000000..41dd294
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/search/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "search",
+ "description": "Simple Search service in Node",
+ "version": "1.0.0",
+ "private": true,
+ "main": "index.js",
+ "scripts": {
+ "start": "node index.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "author": "Google LLC",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "express": "^4.17.1"
+ }
+}
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/.dockerignore b/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/.dockerignore
new file mode 100644
index 0000000..29d6828
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/.dockerignore
@@ -0,0 +1,3 @@
+node_modules
+npm-debug.log
+
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/Dockerfile b/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/Dockerfile
new file mode 100644
index 0000000..ba53edb
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/Dockerfile
@@ -0,0 +1,33 @@
+# 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.
+FROM node:16
+
+# Create app directory
+WORKDIR /usr/src/app
+
+# Install app dependencies
+# A wildcard is used to ensure both package.json AND package-lock.json are copied
+# where available (npm@5+)
+COPY package*.json ./
+
+RUN npm install
+# If you are building your code for production
+# RUN npm ci --only=production
+
+# Bundle app source
+COPY . .
+
+EXPOSE 8080
+CMD [ "node", "index.js" ]
+
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/index.js b/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/index.js
new file mode 100644
index 0000000..ae01fd7
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/index.js
@@ -0,0 +1,31 @@
+/**
+ 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
+
+ https://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.
+
+*/
+const express = require('express');
+const app = express();
+
+app.get('/translate', (req, res) => {
+ const name = process.env.NAME || 'Translate';
+ res.json({
+ service: "translate",
+ id: "789-ghi"
+ });
+});
+
+const port = parseInt(process.env.PORT) || 8080;
+app.listen(port, () => {
+ console.log(`Translate service: listening on port ${port}`);
+});
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/package.json b/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/package.json
new file mode 100644
index 0000000..f928cfb
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/services/translate/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "translate",
+ "description": "Simple Translate service in Node",
+ "version": "1.0.0",
+ "private": true,
+ "main": "index.js",
+ "scripts": {
+ "start": "node index.js"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "author": "Google LLC",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "express": "^4.17.1"
+ }
+}
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/templates/AM-SetAudience.template.xml b/samples/x-nb-psc-sb-psc-ilb-crun/templates/AM-SetAudience.template.xml
new file mode 100644
index 0000000..d727843
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/templates/AM-SetAudience.template.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ flow.audience
+ https:/{proxy.pathsuffix}$CLOUDRUN_APP_SUFFIX
+
+
diff --git a/samples/x-nb-psc-sb-psc-ilb-crun/variables.tf b/samples/x-nb-psc-sb-psc-ilb-crun/variables.tf
new file mode 100644
index 0000000..ba67396
--- /dev/null
+++ b/samples/x-nb-psc-sb-psc-ilb-crun/variables.tf
@@ -0,0 +1,50 @@
+/**
+ * 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.
+ */
+
+variable "gcp_project_id" {
+ type = string
+ description = "The GCP project ID to create the gcp resources in."
+}
+
+variable "gcp_region" {
+ type = string
+ description = "The GCP region to create the gcp resources in."
+}
+
+variable "gcp_zone" {
+ type = string
+ description = "The GCP zone to create the gcp resources in."
+}
+
+variable "repository_id" {
+ type = string
+ description = "Repository id of the artifact registry."
+}
+
+variable "url_mask" {
+ type = string
+ description = "URL mask of the serverless network endpoint group (neg)."
+}
+
+variable "apigee_endpoint_attachment" {
+ type = string
+ description = "Apigee endpoint attachment value."
+}
+
+variable "consumer_vpc" {
+ type = string
+ description = "Consumer VPC network name."
+}
\ No newline at end of file