4 steps to supercharge Planet Scale, Global architectures on Google Cloud
In part 1 of this post, I showed you how to create a globally load balanced, self-healing, auto-scaled, and secure architecture on Google Cloud (Serverless of course!). In this post, we will add 3 improvements to this architecture and automate it’s deployment. Here’s what I’ll cover in this post:
- Optimize content delivery by adding Cloud CDN
- Enhance security posture by adding Identity Aware Proxy (IAP)
- Add Google Firestore for global data persistence
- Automate deployment via Pulumi IaC
Step 1: Optimize content delivery (static assets)
The previous architecture relies on Cloud Run to respond to all incoming traffic. This is technically feasible, however it’s expensive and inefficient.
The goal is to offload all static assets that don’t require any active compute to be delivered by Cloud CDN instead.
Cloud CDN will serve assets from the Google edge network closest to the user. The Global Load Balancer will integrate with a Backend bucket to store the actual assets. For maximum efficiency, Cloud CDN maintains a cache and attempts to serve directly from that cache instead of reaching to the Google Cloud Storage bucket (GCS).
We will configure all requests to the root of the domain /
to be served by CDN. You can enable this at the Load Balancer console.
First, goto the Backend Configuration and create a Backend Bucket. Make sure to enable Cache static content. My bucket name is gmao-web-app
. Make sure the Bucket has an index.html
file that will be served by default.
Next, goto Routing Rules and add a path that will be served by Cloud CDN. I want the root /
of the domain to serve a static web application and the /api
path to be served by the global Cloud Run service that we deployed earlier. Here’s how to set that up:
Finally, make sure the bucket has public access enabled and set the default file to be index.html.
To set the default file, use gsutil to set the MainPageSuffix to index.html
:
gsutil web set -m index.html -e 404.html gs://gmao-web-app
Detailed docs are available here and a full walk through here.
Deploy a Static Website
One of the best ways to create a web application is to build it in React and compile it as a static Single Page Application (SPA) — then deploy to your GCS bucket. Cloud CDN will serve the assets and the entire application will run client side with no web servers necessary.
Here is my sample Github repo that show you how to build a basic React app that can be deployed to any service capable of serving HTTP traffic.
Step 2: Enhance security with Identity Aware Proxy
Identity Aware Proxy (IAP) is a global Google security service that authorizes user requests to applications based on properties in the user’s request context. IAP supports integration with various 3rd party Identity providers such as Google, Facebook, etc as well as GCP project specific IAM users. Plus, it’s #serverless — there are no servers to manage or scale!
The goal is to add context aware security to our application. We will enable IAP on the GALB to protect the application on Cloud Run.
For this reference architecture, we will enable IAP for IAM users, not external Identities. We will protect the application on path /api
. Goto the IAP console, locate the Backend for your service, and enable IAP. This will take short duration to provision — just wait until the status is OK.
Then add Principals and assign them the IAP-Secured Web App User role.
Any principal that has this role will be prompted to authenticate when they request path /api
Step 3: Add Global Persistence
Google Cloud provides a wide range of database services that serve different purposes. I choose Google Cloud Firestore to add global persistence to this reference architecture because it is designed to offer global scalability, multi-region replication with 99.999% SLA, strong consistency by default, and no server management. It’s a NoSQL Document database that organizes documents in groups called Collections.
Firestore’s strong consistency allows this architecture to read and write from any region with consistent results. For each user that will use the system, I preload a document with its ID set to the user’s email. The document itself stores user information as follows:
{
"firstName": "George",
"lastName": "Mao",
"email": "user@iamgeorge.altostrat.com"
}
After successful authentication, IAP will inject 3 headers into the downstream request as follows:
The goal is to extract the authorized user context proved from IAP and use it to pull the associated document from the Firestore backend.
First, we need create a new Service Account that has permissions to interact with Firestore and assign it to the Cloud Run Function. The default service account attached to Cloud Run resources does not provide any permissions. The role we need to add is roles/datastore.user.
If you are using the the console, Create a new Service Account then assign the Cloud Datastore User role.
If you are using the glcoud cli, just execute the following commands:
gcloud iam service-accounts create SERVICE_ACCOUNT_NAME \
--description="DESCRIPTION" \
--display-name="DISPLAY_NAME"
gcloud projects add-iam-policy-binding PROJECT_ID \
--member="serviceAccount:SERVICE_ACCOUNT_NAME@PROJECT_ID.iam.gserviceaccount.com" \
--role="roles/datastore.user"
Second, we need to update the Cloud Run Function code to:
- Extract the
x-goog-authenticated-user-email
header from the request - Lookup the Document in Firestore with the matching ID
let userEmail = req.headers['x-goog-authenticated-user-email'];
console.log(userEmail);
// Remove the accounts.gooogle.com string
userEmail=userEmail.replace("accounts.google.com:", '');
Note: In a real production environment, you should also validate the JWT token in the
x-goog-iap-jwt-assertion
header
Get the document in Firestore that has the same ID:
let doc = await readDoc(userEmail);
async function readDoc(userEmail) {
try {
console.log(`Reading document: ${userEmail}`)
const document = firestore.collection('person').doc(userEmail);
// Read the document.
const doc = await document.get();
if (!doc.exists) {
console.log('No such document!');
return;
}
console.log('Read the document');
console.log(doc.data());
return doc.data();
}
catch (error) {
console.error('Error retrieving Firestore document:', error);
return null;
}
}
Full updated code is available on in my Github repo here.
Step 4: Automate deployment
Pulumi IaC is a great way to describe your Cloud architecture using common programming languages you already know. It has significant support for GCP and automates all deployment tasks.
The goal is to describe all major components of this architecture using Pulumi NodeJs libraries and fully automated the deployment
I’ll cover some of the key configurations below. To create the Cloud Run resources: Stage the code as a zip file in a GCS bucket, then deploy the Cloud Run Function.
To create the Firestore Database and add a document:
The source code that handles the deployment via Pulumi is available here — including configuration of all other resources in this architecture, such as the Global ALB, Cloud CDN, and Firestore.
Run pulumi up
to execute the deployment. You should see a diff that shows the resources that will be deployed. Confirm these are accurate and select yes
.
Summary
In part 1 of this post, I showed you how to create a globally load balanced architecture.
In this post, we enhanced the architecture by adding global content delivery, security at the API layer, and global persistence capabilities.
Review the source code for:
- Static web app deployed to Cloud CDN
- Global, Serverless Cloud Run Functions backend
- Deployment automation via Pulumi
Reach out to me if you have any questions & Happy Coding :)