dart firebase flutter cloud-computing serverless gcp

Going Serverless with Dart: Building Cloud Functions on GCP

Dinko Marinac

Dinko Marinac

6 min read
Going Serverless with Dart: Building Cloud Functions on GCP

Introduction

Welcome to part two of my series “Going Serverless with Dart”. After exploring AWS Lambda, let’s see how to write and deploy serverless functions to Google Cloud Platform (GCP).

Before we begin, let’s quickly recap what serverless computing means. I really like this explanation from CloudFlare:

Serverless computing is a method of providing backend services on an as-used basis. A serverless provider allows users to write and deploy code without the hassle of worrying about the underlying infrastructure. Note that despite the name serverless, physical servers are still used but developers do not need to be aware of them.

In other words, serverless computing lets you use just the backend components you need, without dealing with infrastructure management. While you only pay for what you use and get automatic scaling, be aware of the risk of unexpected high bills from traffic spikes or potential DDOS attacks.

Google offers its serverless components through Google Cloud Platform (GCP). Their serverless compute offering is called Cloud Run Functions.

Google also provides the Functions Framework, which is an open-source development framework that lets you write portable serverless applications.

Let me show you how it works.

Dart Functions Framework

While the Functions Framework supports multiple languages, we’ll focus on Dart implementation, which provides a familiar environment for Flutter developers.

The framework implements the CloudEvents specification, an open standard for describing event data that makes your functions portable across different platforms and environments. This means you can write your function once and run it:

  • Locally during development
  • In Cloud Functions for production
  • In Cloud Run as a containerized service
  • Or even in any other platform that supports CloudEvents

Bear in mind that this is community-supported project, meaning there is no official level of support. While it may seem we are writing “cloud functions” what we are really doing here is creating a Cloud Run service. Cloud Functions under the hood are Cloud Run Services with a single endpoint, but that is usually masked by the official framework.

Writing Your First Cloud Function in Dart

To create a cloud function with Dart, you’ll need:

  1. Dart SDK installed
  2. Google Cloud CLI (gcloud) installed and configured
  3. A Firebase or a Google Cloud project with billing enabled

Firebase is recommended since it’s a wrapper around GCP which sets a lot of things up for you.

I have prepared a Github repo if you want to get your hand dirty and try all this yourself, but I will explain everything here step by step.

Firstly, let’s add functions_framework and dart_firebase_admin packages:

dependencies:
  functions_framework: ^0.4.0
  dart_firebase_admin: ^0.2.0
  protobuf: ^3.1.0
  shelf: ^1.0.0

dev_dependencies:
  build_runner: ^2.0.0
  functions_framework_builder: ^0.4.0

dart_firebase_admin will be used to interact with Firestore.

final _adminApp = FirebaseAdminApp.initializeApp(
  Env.projectId,
  Credential.fromApplicationDefaultCredentials(),
);

Firestore get firestore => Firestore(_adminApp);

Environment variables will be set during deployment, but luckily, all we need is the project id because GCP environment already specifies all the credentials that we need.

import 'dart:io';

class Env {
  static String get projectId => _env('PROJECT_ID');

  static String _env(String key) {
    final value = Platform.environment[key];
    if (value == null) {
      throw ArgumentError('Environment variable $key is not set');
    }
    return value;
  }
}

Now create a function that responds to Firestore document creation events:

import 'dart:async';

import 'package:functions_framework/functions_framework.dart';
import 'package:gcp_functions_framework_firestore/src/firebase.dart';
import 'package:gcp_functions_framework_firestore/src/function_types.dart';

@CloudFunction()
FutureOr<void> oncreatetodo(CloudEvent event) async {
  // Check if document path is available
  final path = event.subject?.replaceFirst('documents/', '');
  if (path == null) {
    return;
  }

  // Update document title
  final documentEventData =
      DocumentEventData.fromBuffer(event.data as List<int>);
  final document = documentEventData.value;
  final title = document.fields['title']?.stringValue;
  await firestore.doc(path).update({'title': '$title from server!'});
}

This function:

  1. Receives a CloudEvent when a new document is created
  2. Extracts the document path from the event
  3. Parses the document data from the event
  4. Updates the document’s title by appending “from server!”

All that’s left is to run the generator dart run build_runner build --delete-conflicting-outputs to create server.dart file, which is the entry point of our function.

To test the function locally, you can run the server using dart run bin/server.dart --target=oncreatetodo --signature-type=cloudevent and send the cloud event example like this one. Make sure to update the json with what the function expects.

Deployment

To deploy your function manually, you’ll need to run several gcloud commands. Let’s go through them step by step:

  1. First, create an EventArc service account:

    gcloud iam service-accounts create eventarc-production \
        --description="EventArc Production Service Account" \
        --display-name="EventArc production"
  2. Grant the EventArc receiver role to the service account:

    gcloud projects add-iam-policy-binding YOUR_PROJECT_ID \
        --member="serviceAccount:eventarc-production@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
        --role="roles/eventarc.eventReceiver"
  3. Create and configure the Compute Engine default service account:

    gcloud iam service-accounts create YOUR_PROJECT_ID \
        --description="Compute Engine default service account" \
        --display-name="Compute Engine default service account"
  4. Grant yourself deployment permissions:

    gcloud iam service-accounts add-iam-policy-binding \
        YOUR_PROJECT_ID@appspot.gserviceaccount.com \
        --member="user:YOUR_EMAIL" \
        --role="roles/iam.serviceAccountUser"
  5. Deploy your function to Cloud Run:

    gcloud run deploy oncreatetodo \
        --source=. \
        --no-allow-unauthenticated \
        --project=YOUR_PROJECT_ID \
        --region=europe-west3 \
        --set-env-vars PROJECT_ID=YOUR_PROJECT_ID
  6. Create an EventArc trigger for Firestore events:

    gcloud eventarc triggers create oncreatetodo \
        --location=europe-west3 \
        --destination-run-service=oncreatetodo \
        --event-filters="type=google.cloud.firestore.document.v1.created" \
        --event-filters="database=(default)" \
        --event-filters="namespace=(default)" \
        --event-filters-path-pattern="document=todos/{todoId}" \
        --event-data-content-type="application/protobuf" \
        --service-account="eventarc-production@YOUR_PROJECT_ID.iam.gserviceaccount.com" \
        --project=YOUR_PROJECT_ID
    

While these steps work, they’re quite verbose and error-prone to type manually. To make this process easier, you can use Makefile that makes deployment and creating a trigger easy. Other steps have to be done only before the first deployment.

Congratulations 🎉, you have successfully written and deployed your first Dart Cloud Function on GCP!

Conclusion

We’ve explored how to create serverless functions with Dart using Google’s Functions Framework. While the setup requires several steps, the framework provides a solid foundation for building scalable applications that can run anywhere supporting CloudEvents. Combined with Firestore integration and the familiar Dart environment, it’s particularly appealing for Flutter developers looking to add backend functionality to their applications.

You can find all the code and automation tools in the GitHub repository.

If you have found this useful, make sure to like and follow for more content like this. To know when the new articles are coming out, follow me on Twitter or LinkedIn.

Dinko Marinac

Dinko Marinac

Software Developer specializing in Flutter, Android, and mobile development. Writing about code, architecture, and developer productivity.