Skip to content

Commit

Permalink
Add example of React SPA app with Python backend.
Browse files Browse the repository at this point in the history
  • Loading branch information
rosmo committed Dec 30, 2024
1 parent d3e8078 commit 8714b1d
Show file tree
Hide file tree
Showing 28 changed files with 20,749 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,8 @@ them to fit your particular use case.
Batching in Pub/Sub's Java client API.
* [QAOA](examples/qaoa) - Examples of parsing a max-SAT problem in a
proprietary format, for Quantum Approximate Optimization Algorithm (QAOA)
* [React single-page app on Cloud Run + Cloud Storage](examples/react-spa-app) - End-to-end example of deploying
a React SPA on serverless Google Cloud services.
* [Redis Cluster on GKE Example](examples/redis-cluster-gke) - Deploying Redis
cluster on GKE.
* [Risk Analysis Asset](examples/risk-analysis-asset) - Deploying Reliability Risk analysis tool on Cloud Run.
Expand Down
140 changes: 140 additions & 0 deletions examples/react-spa-app/README.md

Large diffs are not rendered by default.

58 changes: 58 additions & 0 deletions examples/react-spa-app/backend/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Copyright 2024 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.

import functions_framework
import os
import sys
from google.auth.transport import requests as auth_requests
from google.oauth2 import id_token
from flask import abort

def validate_iap_jwt(iap_jwt, expected_audience):
"""Validate an IAP JWT.
Args:
iap_jwt: The contents of the X-Goog-IAP-JWT-Assertion header.
expected_audience: The Signed Header JWT audience. See
https://cloud.google.com/iap/docs/signed-headers-howto
for details on how to get this value.
Returns:
(user_id, user_email, error_str).
"""

try:
decoded_jwt = id_token.verify_token(
iap_jwt,
auth_requests.Request(),
# Normally we would have an audience here, but this creates a chicken and an egg problem
# due to backend service needing to point to a function, so the audience is created after
# deployment. We could store the audience in a Secret Manager secret to decouple the process,
# but this exercise is left up to the reader.
audience=None, # audience=expected_audience
certs_url="https://www.gstatic.com/iap/verify/public_key",
)
return (decoded_jwt["sub"], decoded_jwt["email"], "")
except Exception as e:
print(f"**ERROR: JWT validation error {e}**", file=sys.stderr)
return (None, None, f"**ERROR: JWT validation error {e}**")

@functions_framework.http
def hello_function(request):
if os.getenv("IAP_AUDIENCE"):
ret = validate_iap_jwt(request.headers.get("x-goog-iap-jwt-assertion"), os.getenv("IAP_AUDIENCE"))
if not ret[0]:
abort(403)
return "Hello World: %s" % (ret[0])
return 'Hello World'
16 changes: 16 additions & 0 deletions examples/react-spa-app/backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2024 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.
functions-framework
google-auth
requests
52 changes: 52 additions & 0 deletions examples/react-spa-app/cloud-armor.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright 2024 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.

resource "google_compute_security_policy" "policy" {
name = "my-react-app-backend-policy"
project = module.project.project_id

type = "CLOUD_ARMOR"

rule {
action = "allow"
priority = "2147483647"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
description = "Allow everything"
}
}

resource "google_compute_security_policy" "edge-policy" {
name = "my-react-app-frontend-policy"
project = module.project.project_id

type = "CLOUD_ARMOR_EDGE"

rule {
action = "allow"
priority = "2147483647"
match {
versioned_expr = "SRC_IPS_V1"
config {
src_ip_ranges = ["*"]
}
}
description = "Allow everything"
}
}

164 changes: 164 additions & 0 deletions examples/react-spa-app/dns.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
# Copyright 2024 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.

module "dns" {
for_each = toset(var.dns_config != null ? [""] : [])
source = "github.com/GoogleCloudPlatform/cloud-foundation-fabric//modules/dns?ref=daily-2024.12.30"
project_id = module.project.project_id
name = var.dns_config.zone_name
zone_config = {
domain = format("%s.", var.dns_config.zone_dns_name)
}

# We create the record sets separately to allow TLS certificates to be
# provisioned properly
recordsets = {}

iam = {}
}

# Create subdomain delegation records, if required

data "google_dns_managed_zone" "subdomain" {
for_each = toset(var.dns_config != null && try(var.dns_config.subdomain_delegation_zone_name, null) != null ? [""] : [])
name = var.dns_config.subdomain_delegation_zone_name
project = var.dns_config.subdomain_delegation_project_id != null ? var.dns_config.subdomain_delegation_project_id : module.project.project_id
}

resource "google_dns_record_set" "subdomain-delegation" {
for_each = toset(var.dns_config != null && try(var.dns_config.subdomain_delegation_zone_name, null) != null ? [""] : [])
name = format("%s", module.dns[""].domain)
type = "NS"
ttl = 300

project = var.dns_config.subdomain_delegation_project_id != null ? var.dns_config.subdomain_delegation_project_id : module.project.project_id

managed_zone = data.google_dns_managed_zone.subdomain[""].name

rrdatas = module.dns[""].name_servers
}


resource "google_dns_record_set" "frontend" {
for_each = toset(var.dns_config != null && var.global_lb ? [""] : [])
name = format("%s.%s", var.dns_config.frontend, module.dns[""].domain)
type = "A"
ttl = 60

project = module.project.project_id
managed_zone = module.dns[""].name

rrdatas = [module.xlb[""].address]
}

resource "google_dns_record_set" "frontend-regional" {
for_each = toset(var.dns_config != null && var.regional_lb ? [""] : [])
name = format("%s.regional.%s", var.dns_config.frontend, module.dns[""].domain)
type = "A"
ttl = 60

project = module.project.project_id
managed_zone = module.dns[""].name

rrdatas = [module.xlb-regional[""].address]
}

resource "google_dns_record_set" "backend" {
for_each = toset(var.dns_config != null && var.global_lb ? [""] : [])
name = format("%s.%s", var.dns_config.backend, module.dns[""].domain)
type = "A"
ttl = 60

project = module.project.project_id
managed_zone = module.dns[""].name

rrdatas = [module.xlb[""].address]
}

resource "google_dns_record_set" "backend-regional" {
for_each = toset(var.dns_config != null && var.regional_lb ? [""] : [])
name = format("%s.regional.%s", var.dns_config.backend, module.dns[""].domain)
type = "A"
ttl = 60

project = module.project.project_id
managed_zone = module.dns[""].name

rrdatas = [module.xlb-regional[""].address]
}

resource "google_certificate_manager_certificate" "regional-certificate" {
for_each = toset(var.dns_config != null && var.regional_lb ? [""] : [])
name = format("my-react-app-regional-cert-%s", var.region)
description = "TLS certificate for regional load balancer"
project = module.project.project_id
location = var.region

managed {
domains = [
trimsuffix(format("%s.regional.%s", var.dns_config.frontend, module.dns[""].domain), "."),
trimsuffix(format("%s.regional.%s", var.dns_config.backend, module.dns[""].domain), ".")
]
dns_authorizations = [
google_certificate_manager_dns_authorization.frontend[""].id,
google_certificate_manager_dns_authorization.backend[""].id,
]
}
depends_on = [
google_dns_record_set.frontend-auth,
google_dns_record_set.backend-auth,
]
}

resource "google_certificate_manager_dns_authorization" "frontend" {
for_each = toset(var.dns_config != null && var.regional_lb ? [""] : [])
project = module.project.project_id
name = "my-react-app-frontend-dnsauth"
location = var.region
description = "DNS authorization for frontend domain"
domain = trimsuffix(format("%s.regional.%s", var.dns_config.frontend, module.dns[""].domain), ".")
}

resource "google_certificate_manager_dns_authorization" "backend" {
for_each = toset(var.dns_config != null && var.regional_lb ? [""] : [])
project = module.project.project_id
name = "my-react-app-backend-dnsauth"
location = var.region
description = "DNS authorization for backend domain"
domain = trimsuffix(format("%s.regional.%s", var.dns_config.backend, module.dns[""].domain), ".")
}

resource "google_dns_record_set" "frontend-auth" {
for_each = toset(var.dns_config != null && var.regional_lb ? [""] : [])
name = google_certificate_manager_dns_authorization.frontend[""].dns_resource_record[0].name
type = google_certificate_manager_dns_authorization.frontend[""].dns_resource_record[0].type
ttl = 60

project = module.project.project_id
managed_zone = module.dns[""].name

rrdatas = [google_certificate_manager_dns_authorization.frontend[""].dns_resource_record[0].data]
}

resource "google_dns_record_set" "backend-auth" {
for_each = toset(var.dns_config != null && var.regional_lb ? [""] : [])
name = google_certificate_manager_dns_authorization.backend[""].dns_resource_record[0].name
type = google_certificate_manager_dns_authorization.backend[""].dns_resource_record[0].type
ttl = 60

project = module.project.project_id
managed_zone = module.dns[""].name

rrdatas = [google_certificate_manager_dns_authorization.backend[""].dns_resource_record[0].data]
}
Loading

0 comments on commit 8714b1d

Please sign in to comment.