Firebase: Developing serverless functions in Go
The Go runtime for Google Cloud Functions was released into beta this month. I’m a huge fan of statically typed languages, and of Golang in particular. Therefore I’m excited about the possibility of developing serverless functions in Go, and deploying them on the Google Cloud Platform (GCP). In this post we look at how to implement a Cloud Function in Go, and set it up to be called from a Cloud Firestore trigger. Then we are going to make things more interesting by throwing the Firebase Realtime Database also into the mix.
Our use case is pretty simple. Suppose we have a movie reviews app. Users post reviews, which are stored in a Firestore collection named movie_reviews
. Whenever a new review is posted, we want to analyze it, score the content, and award the author of the review some points. Total scores of users are kept up-to-date in the Firebase Realtime Database. We can certainly implement this use case with just Firestore. But I’m also using the Realtime Database to demonstrate integrating multiple services in the Firebase ecosystem via Cloud Functions. Also I want to try the Firebase Admin SDK in the new Cloud Functions runtime.
Setting up
We need a Firebase project with Firestore and Cloud Functions enabled. If you don’t have one already, just go ahead and create a new Firebase project. Note that this actually creates a GCP project under the hood. Check out the getting started guides for Firestore and Cloud Functions to learn how to enable those services in a new project.
Next, install the Google Cloud SDK in your local development environment so that you have the gcloud
command-line tool at your disposal. Also make sure you have Golang 1.11 installed, and that you are able to locally build and run Go programs. Finally, install the Firebase Admin SDK by running the following command:
$ go get -u firebase.google.com/go
This installs the Admin SDK to your GOPATH
along with its required dependencies.
Coding the Cloud Function
Start by creating a new directory named scorer
somewhere in your local GOPATH
. Then create a file named scorer.go
in that directory. This file is going to contain all the code we implement for this example. You can find the complete scorer.go
file in GitHub.
There are two noteworthy components in our Cloud Function implementation:
- An
init()
function that contains package-level initialization logic. - A
ScoreReview()
function that contains the main body of our serverless function.
An init()
function in Go is a standard way to implement some one-time initialization logic for a package. Listing 1 shows what the init()
function for our scorer
package should look like.
Here we initialize the Firebase Admin SDK, and create a new db.Client
for later use. Be sure to change the DatabaseURL
setting (line 17 in listing 1) to point to your own Firebase Realtime Database instance. As a best practice, you should always reuse the instances of firebase.App
and database.Client
. Therefore by putting the above code in an init()
function, we ensure that it runs only once. Multiple executions of the same Cloud Function instance will use the same db.Client
. However, do note that this does not implement a global singleton. There can be multiple Cloud Function instances active at the same time, and instances are started and stopped based on the load.
The exported ScoreReview()
function contains the main business logic of our serverless function. Listing 2 shows how it is implemented.
This function receives a Context
and a FirestoreEvent
as arguments. FirestoreEvent
is comprised of all the data passed in by the Cloud Firestore trigger. Specifically, it contains the contents of the Firestore document that triggered the Cloud Function. We run a mock scoring function on the movie review text, and use the previously initialized db.Client
instance to update the total score of the corresponding author. We use Transaction()
instead of Set()
in order to prevent the lost update problem when writing to the Realtime Database.
Declaring dependencies
Before we can deploy our function to GCP, we need to create a Go module file (go.mod
) containing the dependencies required by our code. Execute the following commands in the scorer
directory to auto-generate the required manifests.
$ go mod init
$ go mod tidy
The go mod
command inspects the imports in the *.go
files to determine which dependencies are needed to build the code. It generates two new files based on the findings. At this point our project directory looks like this.
scorer/
├── go.mod
├── go.sum
└── scorer.go
Feel free to open and explore the contents of the auto-generated files. The go.mod
file lists the Firebase Admin SDK, and the required dependencies of the Admin SDK. The go.sum
file contains the versions and checksums of all dependencies, so the dependency tree can be reliably reproduced later.
Go modules are an experimental new feature in Golang 1.11 for facilitating module versioning and dependency management. Based on the go.mod
file, Google Cloud Functions will fetch the required dependencies, and build our code in the cloud.
Deploying to the Cloud
Execute the following command from the scorer
directory to deploy our function to the cloud. Make sure to replace <PROJECT_ID>
placeholder with your own GCP project ID.
$ gcloud functions deploy ScoreReview --runtime go111 \
--trigger-event providers/cloud.firestore/eventTypes/document.create \
--trigger-resource "projects/<PROJECT_ID>/databases/(default)/documents/movie_reviews/{pushId}"
The trigger-event
flag indicates that our function should be invoked when new documents are created in Cloud Firestore. The trigger-resource
flag specifies the Firestore path that will be watched for document creation events. The way we have set it up, our function will get triggered every time a new document is added to the movie_reviews
top-level collection. This is indicated by the wildcard {pushId}
at the end of the trigger resource path.
The deployment can take a few minutes. Your code is uploaded to the cloud, where it is built and deployed as a serverless function. If all goes well you should see an output similar to the following.
Deploying function (may take a while - up to 2 minutes)...done.
availableMemoryMb: 256
entryPoint: ScoreReview
eventTrigger:
eventType: providers/cloud.firestore/eventTypes/document.create
failurePolicy: {}
resource: projects/.../databases/(default)/documents/movie_reviews/{pushId}
service: firestore.googleapis.com
labels:
deployment-tool: cli-gcloud
name: projects/.../locations/us-central1/functions/ScoreReview
runtime: go111
serviceAccountEmail: ...
status: ACTIVE
timeout: 60s
updateTime: '2019-01-26T23:02:15Z'
versionId: '1'
Trying it out
Use the Firebase Console to create a new Firestore collection named movie_reviews
, and add a few child documents to it. Each document should contain at least two fields — author
and text
. Figure 1 shows what this should look like.
Each addition of a document triggers an execution of our serverless function several seconds later. From the Firebase Console you can observe the user scores being written to the Realtime Database as shown in figure 2.
You can also check the Cloud Functions logs in the GCP console for further confirmation. Figure 3 shows what to expect.
Conclusion
In this article we looked at consuming Cloud Firestore events from a serverless function implemented in Go. We also used the Firebase Admin SDK to interact with the Firebase Realtime Database. The techniques and APIs described in this post can be used to integrate with a wide range of services in GCP and Firebase. Google Cloud Functions facilitate receiving events from Cloud Storage, Cloud PubSub, Firebase Realtime Database and more. The Firebase Admin SDK enables accessing services like Firebase Auth, Firebase Cloud Messaging and more. The availability of a Go runtime for Cloud Functions means developers can now implement serverless functions that use any combination of the above products while enjoying the simplicity, performance and type safety of Go.