Event-driven, Google Cloud Function in TypeScript

February 1, 2021
Event-driven, Google Cloud Function in TypeScript
A short guide in setting up a new serverless system deployed into Google Cloud Platform (GCP) using Google Pub/Sub, Google Cloud Function, Google Cloud Build and Spinnaker.

Our specific problem

Our internal services had several ways to send notifications, whether that be slack or email notifications for example. As a result, the implementation slightly differed across our services which ultimately led to:

  • Inconsistent branding & language in these notifications
  • Updates needed to be made to several microservices and monoliths
  • Inconsistent development environments
  • Maintaining and scanning a number of third party libraries
  • Different versions of libraries in different projects
  • Different templates for different notifications

Our approach

The first thing to tackle was — why can’t we have a single service that could send notifications across all our systems?

We first analysed:

  • How many services need to send notifications? Answer was less than 5 services
  • How many notifications does each service send? Answer was less than 50 per day
  • Meaning at peak, we are talking about only generating around 250 notifications per day

We also studied the costs of maintaining — say, a single container (or multiple for HA) running in a Kubernetes Cluster vs the cost of a serverless implementation. We also looked at the difference in engineering effort. For instance, serverless implementations do not require:

  • Maintaining Infrastructure as Code (IaaC) such as Kubernetes manifests
  • Complex deployment pipelines
  • Scraping of business logic metrics (number of successful requests, failed requests etc)
  • SLA up-time checks

Our solution

In the context of generating emails or slack notifications, we decided this should be designed as an asynchronous process. We had no specific requirements to make this feature synchronous.

All this led to adopting an event-driven, serverless solution.

Our chosen technology stack was:

  • Node.js with TypeScript
  • Google Cloud Functions
  • Google Pub/Sub

Let’s get coding

Project set up

It’s important to have a good local development environment which can mock event-driven behaviours. To achieve this we used https://cloudevents.io/.

I installed cloudevents with Go, and exported a path in my bash profile.

go get github.com/cloudevents/conformance/cmd/cloudevents
export PATH=$(go env GOPATH)/bin:$PATH

We then set up a new Node.js app with the most important snippets of package.json shown below.

From the above snippet, the most important things to take out of it are:

  • We are requiring @google-cloud/functions-framework: 1.7.1
  • We are using a command provided by the library in our start script function-framework --source=lib/ --target=emailService --signature-type=cloudevent
  • If you omit the signature-type parameter, GCP Cloud Function will assume your function will receives events via HTTP.

How does TypeScript work here?

Good question you ask… Given that our application is required to be built before it can run as a Cloud Function, we need to add a special prepare script, which runs the npm run build or npm run clean && tsc -p.

When GCP deploys the Cloud Function from source, the prepare script will be run. The build script is compiling our TypeScript files to JavasScript and sticking them in lib/ as detailed in our tsconfig.json file shown below.

Our implementation of index.ts

When the application starts, the @google-cloud/functions-framework will look for an exported function in the index.js file in the specified source directory, remember:

"start": "functions-framework --source=lib/ --target=emailService --signature-type=cloudevent"

In this example, we are looking for an exported function called emailService in index.js denoted by the --target parameter.

Our TypeScript implementation of src/index.ts (compiled to lib/index.js) looks something like this:


Key points from our implementation above:

  • Google Cloud Functions that subscribe to Google Pub/Sub will receive a data key within the message object as a base64 encoded string which we decode to an object. This step is optional but we chose to publish our event data as json objects.
  • We validate this object against our schema before executing the core functionality
  • message and context provide useful information about the Pub/Sub event which you may choose to utilise throughout your application

How can you test it?

Going back to cloudevents in our local environment, you can simply start your Node.js application and run a command like this in your terminal:

cloudevents send http://localhost:8080 \
— id abc-123 \
— source cloudevents.conformance.tool \
— datacontenttype “text/plain” \
— data “eyJhY3Rpb24iOiJDb21wbGV0ZWQiLCJub3RpZmljYXRpb25fdHlwZSI6ImVtYWlsIiwibm
90aWZpY2F0aW9uX2JvZHkiOiJodHRwczovL2djc2J1Y2tldC5jb20iLCJub3RpZmljYXRpb25fdGV
tcGxhdGUiOiJDU1ZfRVhQT1JUUyIsInVuaXF1ZV9pZGVudGlmaWVyIjoieXl5eW1tZGQtc2hhIiwiY29
uc3VtZXIiOiJXZWJBcHAiLCJlbWFpbF9oYXNoIjoiNzYwZDU1YjI3MjkxYTlmYjE3NjU1OGIwMWU3M
TA0YzA6NzJiZmYyYzdiMGRlMjA5YWNmODc5ZjY0MTA4MDA1MjlmNDczMmE1MzdkZjYwMmE3Z
jYzMWE2ZDcyN2RlZmJjYiJ9” \
— type foo.bar

Where the parameter data contains the base64 encoded string of what information you wish to pass into your Cloud Function. You can create this string with something like:

const messageData = Buffer.from(JSON.stringify({ something: “something”})).toString(“base64”);

How is this orchestrated?

Creating your Cloud Functions in GCP

The GCP console here is fairly intuitive, so I will not go into lots of detail.

Key things to set up:

  • Create your Google Pub/Sub topic (we called ours notification_requests)
  • Create a CloudFunction
  • Trigger is Google Pub/Sub set to receive events from notification_requests
  • Can add any run-time environment variables you require
  • Set your NODE_ENV here as a build-time variable (was important for our implementation)
  • Set runtime — Node.js 12
  • Source code is Cloud Source Repository (where we setup a mirror repository from GitHub)

CI/CD

The tooling we used:

  • GitHub (source code control) mirrored to Cloud Source Repository
  • Google Cloud Build (CI)
  • Spinnaker (CD)

On GitHub Pull Request we run a script in CloudBuild to execute tests, see cloudbuild-pr.yaml . This ensures that the all tests are run and passing before being able to merge into the Main branch.


On merge to Main branch we then run a Spinnaker pipeline which executes a CloudBuild script to deploy the Function to our non-production environment.

Important: Be sure to include the --source flag otherwise the Cloud Function will just deploy the same version of Git it is currently running. Setting it as shown above deploys the latest version from your Main branch.

We also created a VPC connector so that our Cloud Function can communicate with our internal services within our VPC.

Conclusion

TypeScript for JS is a great framework to build type-safe Node.js applications. With the right steps, it can be deployed to serverless platforms with relative ease.

There are no doubt many other solutions out there that could satisfy our requirements but nonetheless, we were able to achieve a highly resilient, scalable system using the approach outlined in this article at relatively low cost and ease.

Happy coding!

Authored by Robert Cicero, Engineering Manager


Sign up to the latest DueDil news!
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.