How to use Fastify on Google Cloud Functions

 Reading time ~12 minutes

Introduction

This post started as a simple guide on how to configure the Fastify web framework to run inside a Google Cloud Function, but its focus quickly switched to a deeper analysis on the authentication to Google Cloud Functions (GCF from now on).

GCF is the FaaS (Function as a Service) platform provided by Google. It’s a serverless execution environment for building and connecting cloud services. With Cloud Functions you write simple, single-purpose functions that are attached to events emitted from your cloud infrastructure and services.

There are two distinct types of Google Cloud Functions provided by the Google Cloud Platform:

  • HTTP functions. Invoked by HTTP requests
  • Event-driven functions. They can be either background functions or CloudEvent functions that are invoked indirectly in response to events, such as a message on a Pub/Sub topic, a change in a Cloud Storage bucket, or a Firebase event.

Build the Fastify app

Fastify is a web framework for NodeJS, highly focused on providing the best developer experience with the least overhead and a powerful plugin architecture. In the last few years it has quickly become one of the most popular NodeJS web frameworks reaching 17.500 stars on GitHub. Fastify v3.0 is the latest version and has been available since July 2020.

The Fastify documentation already provides instructions on how to configure the framework in order to run on Serverless environments, but it doesn’t explicitly mention how to configure the framework to target the GCF platform. However it’s not much different than running code in a AWS Lambda function or in a Google Cloud Run environment.

Notice: As the official documentation states, Fastify is not designed to run on serverless environments and probably it doesn’t even make sense to use a full featured web framework inside a stateless function, even though it’s totally possible.

First of all let’s create the main function that builds the application:

// app.js
'use strict'

const fastify = require('fastify')

function build (opts = {}) {
  const app = fastify(opts)

  app.register(require('./routes.js'))

  return app
}

module.exports = build

As you can see, we’re declaring the fastify app (with the options passed as parameter) and we immediately register the application routes. For simplicity let’s suppose that we only have the home route:

// routes.js
'use strict'

module.exports = async function (fastify, opts) {
  fastify.register(require('./routes/home'))
}

In the route we only return the application name and version number, retrieved from the package.json file.

// home.js
const packageJson = require('./package.json')

async function home (fastify, options) {
  fastify.get('/', async (request, reply) => {
    return {
      name: packageJson.name,
      version: packageJson.version
    }
  })
}

module.exports = home

Now we can define a server.js file that uses the application in a local dev environment:

'use strict'

const server = require('./app')({
  logger: true
})

server.listen(process.env.PORT || 3000, (err, address) => {
  if (err) {
    server.log.error(err.message)
    process.exit(1)
  }
})

So far so good! We can start our server using node server.js and we can see the response by visiting the http://127.0.0.1:3000.

Install the Google Cloud Functions Framework

The easiest way to add support for the GCF platform to our simple Fastify app is to use the Google Functions Framework.

The Functions Framework is an abstraction layer which aims to unmarshal incoming HTTP requests into language-specific function invocations. This library lets you write lightweight functions that run in many different environments, including:

The Functions Framework is available for several runtimes, including Go, Java, PHP and Python. As you can imagine, we’re interested in the Node.js version, which is based on the popular Express web framework.

Wait! Are you telling me that we are going to run a web framework (Fastify) inside another web framework (Express)? That doesn’t sound good!

Yes, precisely! I’m aware that this is not a good idea in production environments, however we can continue the analysis for instructional purposes. By the way, do you know that you can also run Express inside Fastify? 🤯

Notice: GCF supports the Node.js 10, 12 and 14 (public preview) runtimes. These runtimes are based on the Functions Framework. On Cloud Functions, the Functions Framework is completely optional: if you don’t add it to your package.json, it will be installed automatically during the deployment.

Let’s install it

$ npm install @google-cloud/functions-framework

then we can add a separate index.js file as the entrypoint.

Notice: Cloud Functions uses the main field in your package.json file as the entry point. If the main field is not specified, Cloud Functions by default loads code from index.js.

Fastify lets you create a custom http server based on a factory, so we can attach the handler exposed by the functions framework to Fastify. We can also reuse the initialization code from the app.js file:

'use strict'

const http = require('http')

let handleRequest = null

const serverFactory = (handler, opts) => {
  handleRequest = handler

  return http.createServer()
}

const app = require('./app')({
  serverFactory,
  logger: process.env.NODE_ENV === "development"
})

// see https://github.com/fastify/fastify/issues/946#issuecomment-766319521
// this is necessary if you want to support JSON inputs in POST/PATCH requests
app.addContentTypeParser('application/json', {}, (req, body, done) => {
  done(null, body.body);
});

exports.fastifyFunction = (req, res) => {
  app.ready(err => {
    if (err) throw err
    handleRequest(req, res)
  })
}

Cloud Functions have a name property that is set at deploy time, and once set, it cannot be changed. The name of the function is used as its identifier and it must be unique within a region.

In our case the name of our function will be fastifyFunction, as you can see from the function exported at the end of the file.

The functions framework also lets you test this setup locally:

$ npx @google-cloud/functions-framework --target=fastifyFunction

Serving function...
Function: fastifyFunction
URL: http://localhost:8080/

As you can see, by default your function will be accessible at localhost:8080 unless you explicitly define a value for the PORT environment variable.

Deploy the app on Google Cloud Functions

The fastest way to deploy code on the Google Cloud Platform is to use the Google Cloud SDK from the command line. Follow the install instructions for your OS and then make sure to run gcloud init to configure the integration with your GCP project.

Once configured you have to make sure that billing is enabled for your Cloud project and that you have the Cloud Functions and Cloud Build APIs enabled.

Deployments work by uploading an archive containing your function’s source code to a Google Cloud Storage bucket. Once the source code has been uploaded, Cloud Build automatically builds your code into a container image and pushes that image to Container Registry. Cloud Functions uses that image to create the container that executes your function.

To deploy the function using the gcloud tool you can run the following command

$ gcloud functions deploy fastifyFunction --runtime nodejs12 --trigger-http --allow-unauthenticated
  • --runtime defines the name of the runtime you intend to use. In this case we want Node.js 12 1
  • --trigger-http declares that we want to trigger the function via HTTP
  • --allow-unauthenticated specifies that the function does not require authentication. By default http functions require authentication.

Once the deploy process finishes you can take note of the httpsTrigger.url property and invoke the function from curl or from your browser. If you forgot the endpoint you can describe the function anytime to inspect its properties:

$ gcloud functions describe fastifyFunction

The url should look like this:

https://GCP_REGION-PROJECT_ID.cloudfunctions.net/fastifyFunction

Alternatively you can directly invoke functions using the gcloud CLI interface

$ gcloud functions call fastifyFunction

Notes about billing

Make sure to carefully review the pricing page for the GCF platform. It’s not uncommon nowadays to read stories of how companies nearly went bankrupt after burning a ton of money into Cloud providers.

The pricing for Google Cloud Functions include the following:

Almost all these services provide a free tier. Make sure to review the pricing documents for further details.

Add Authentication

Now let’s see how we can add authentication to our cloud function, so we can control who’s invoking it.

One of the options at our disposal is adding a Cloud Endpoint with the Extensible Service Proxy V2 (ESPv2) as an API Gateway in front of it. This solution is too complex for this scenario, so I haven’t explored it.

More options are documented in the authenticating page, based on the subject that needs authentication:

  • developers: only specific users can invoke the function during development and testing processes
  • function-to-function: only authorized functions can invoke your function
  • end-users: users that need access to an application from mobile or web clients

The first step is to make sure that your function requires authentication. Log into the Google Cloud Console and remove the allUsers member from your cloud function. Alternatively you can also use the command line to remove the member from the IAM role

$ gcloud functions remove-iam-policy-binding fastifyFunction --member=allUsers --role=roles/cloudfunctions.invoker

Notice: Redeploying the function from the command line without the --allow-unauthenticated flag will not work because the permission has already been set.

Now you can see that invoking the function will fail with a 403 error:

$ curl https://GCP_REGION-PROJECT_ID.cloudfunctions.net/fastifyFunction

<html><head>
<meta http-equiv="content-type" content="text/html;charset=utf-8">
<title>403 Forbidden</title>
</head>
<body text=#000000 bgcolor=#ffffff>
<h1>Error: Forbidden</h1>
<h2>Your client does not have permission to get URL <code>/fastifyFunction</code> from this server.</h2>
<h2></h2>
</body></html>

OAuth and OIDC

Google APIs use the OAuth 2.0 protocol for authentication and authorization. Google supports common OAuth 2.0 scenarios such as those for web server, client-side, installed, and limited-input device applications.

Explaining the flow of an OAuth 2 (authorization) and OpenID Connect (identification) application is out of the scope of the article, but their knowledge is required in order to understand how to interact with Google services. A good walkthrough to the topic is available on the Okta Blog.

Developers with the cloudfunctions.functions.invoke permission can easily invoke Cloud Functions from their local machine, given that they are authenticated with their service account, using the command line and the gcloud utility:

$ curl https://REGION-PROJECT_ID.cloudfunctions.net/FUNCTION_NAME -H "Authorization: bearer $(gcloud auth print-identity-token)"

Notice: In order for this request to work, the role assigned to the developer must contain the cloudfunctions.functions.invoke permission. By default, the Cloud Functions Admin and Cloud Functions Developer roles have this permission.

The command gcloud auth print-identity-token is used to establish an OAuth/OpenID flow between your machine and the authorization server at Google. By inspecting the network traffic you can clearly see how your computer tries a DNS query for the metadata server metadata.google.internal, but since we’re running the command from outside the Google Cloud Platform, the authorization server is going to be oauth2.googleapis.com.

Wireshark-captured network calls

The result of the commad is an OpenID token that can be used as bearer token for the cloud function invocation. OIDC tokens are actually signed JSON Web Tokens (JWT)2 and are used primarily to assert identity and not to provide any implicit authorization against a resource, unlike OAuth tokens, which do provide access.

Authenticate from a PHP Application

So far, so good! But if you’re reading this post you’re probably interested in how to invoke a Cloud Function from your our own application. In this case you have to set up a service account that is going to interact with the function on behalf of your application.

A service account is a special type of Google account intended to represent a non-human user that needs to authenticate and be authorized to access data in Google APIs.

If your code is already running from the Google Cloud Platform (for example from Compute Engine), a service account is already attached to the component automatically. This service account is customizable for a better right segregation and for applying the least privilege principle on each component.

Instead, if your code is not running on GCP, you must have a service account key file. This comes as a public/private key pair in the form of a JSON file.

Warning: These keys are created, downloadable, and managed by users. This means that you are solely responsible for them and you need to make sure that you have processes in place to address key management requirements such as storage, distribution, revocation, rotation and protection from unauthorized users.

Create a new service account and make sure to assign it at least the roles roles/cloudfunctions.invoker and roles/iam.serviceAccountTokenCreator3. After doing that, download the JSON key file and store it somewhere safe on your machine.

Now it’s time to write the actual application code. The Google Documentation is a good starting point, but it doesn’t provide a full example of how obtaining an OIDC token from a local service account key file.

This is pretty much the reason I’m writing this post. I needed to invoke a Cloud Function from a PHP application and I was not able to find a good example for this purpose.

The easiest way is to use the Google Auth Library for PHP, which is Google’s officially supported PHP client library for using OAuth 2.0 authorization and authentication with Google APIs.

Define an environment variable GOOGLE_APPLICATION_CREDENTIALS that points to your Service Account Credentials JSON file:

<?php

putenv('GOOGLE_APPLICATION_CREDENTIALS=/path/to/my/credentials.json');

Finally write the code that is going to fetch an ID token to access your Cloud Function:

<?php

// this is the URL associated with a Cloud Function or a CloudRun App
$targetAudience = 'https://GCP_REGION-PROJECT_ID.cloudfunctions.net/fastifyFunction'

// this is a Guzzle-compatible middleware which performs the OAuth flow
$middleware = ApplicationDefaultCredentials::getIdTokenMiddleware($targetAudience);

$stack = HandlerStack::create();
$stack->push($middleware);

// create the Guzzle HTTP client
$client = new Client([
    'handler' => $stack,
    'auth' => 'google_auth',
    // Cloud Run, IAP, or custom resource URL
    'base_uri' => 'https://https://GCP_REGION-PROJECT_ID.cloudfunctions.net',
]);

// make the request
$response = $client->get('/fastifyFunction');

// and show the result!
print_r((string) $response->getBody());

The previous code is basically a streamlined version of the following, which best outlines the OAuth flow:

<?php

$jsonKey = json_decode(file_get_contents('/path/to/my/credentials.json'), true);

$creds = new \Google\Auth\OAuth2([
  'audience' => $jsonKey['token_uri'],
  'issuer' => $jsonKey['client_email'],
  'signingAlgorithm' => 'RS256',
  'signingKey' => $jsonKey['private_key'],
  'signingKeyId' => $jsonKey['private_key_id'],
  'sub' => $jsonKey['client_email'],
  'tokenCredentialUri' => $jsonKey['token_uri'],
]);

$targetAudience = 'https://GCP_REGION-PROJECT_ID.cloudfunctions.net/fastifyFunction'

$creds->setAdditionalClaims([
    'target_audience' => $targetAudience
]);

$request = $creds->generateCredentialsRequest();

// create the Guzzle HTTP client
$client = new Client();
$response = $client->send($request);

$body = json_decode($response->getBody()->getContents(), true);

$idToken = $body['id_token'];

// new Guzzle HTTP Client
$client = new Client([
  'base_uri' => 'https://GCP_REGION-PROJECT_ID.cloudfunctions.net'
]);

$response = $guzzle->get('/fastifyFunction', [
  'headers' => [
    'Authorization' => $idToken
  ]
 ]);

$data = json_decode($response->getBody()->getContents());

That’s it! Hope you like it.


  1. At the time of writing NodeJS 14 was not available yet. [return]
  2. You can easily inspect the payload using jwt.io [return]
  3. IAM Permission reference [return]
comments powered by Disqus

Use context in ExUnit tests

Introduction

I’m on my journey to learn the Elixir programming language and I’m really enjoying the process so far.

Elixir …