Running multiple Google Cloud functions locally with the functions-framework

th0rgall

Thor Galle

Posted on October 27, 2022

Running multiple Google Cloud functions locally with the functions-framework

So, you want to run multiple node.js Google Cloud Functions locally at the same time using Google’s functions-framework?

You might have previously written cloud functions in Firebase, where the Firebase Local Emulator Suite allowed you to run all your functions simultaneously, on a single local server, with a single command (firebase emulators:start).

The function framework does not provide an emulator that can do this out-of-the-box. However, you can very easily write one yourself, and approximate Firebase’s local development experience this way.

This approach combines your functions in a single Express “meta” app for development purposes only. You can still deploy the functions individually to Google Cloud.

Example setup

In this example I have the following directory structure:

├── package.json
└── src
    ├── index.js
    └── functions
        ├── firstFunction.js
        └── secondFunction.js
Enter fullscreen mode Exit fullscreen mode

The function scripts

The two functions themselves are full-fledged Express.js handlers, like they would be in Firebase.

To test that the two functions can interact, the first function returns HTTP 302 redirect, which redirects to a GET request on the second function.

// src/functions/firstFunction.js
export const firstFunction = async (req, res) => {
    res.redirect('/secondFunction');
}
Enter fullscreen mode Exit fullscreen mode
// src/functions/secondFunction.js
export const secondFunction = async (req, res) => {
    res.send("OK! You were redirected here.");
}
Enter fullscreen mode Exit fullscreen mode

package.json

The package.json refers to the src/index.js as the main node script. We also need to tell the functions-framework to target the index export within the index.js module:

// package.json

...
"type": "module",
// Tells the functions-framework where to look for exports.
"main": "src/index.js", 
"scripts": {
    "start": "functions-framework --target=index", // Select target export
    "debug": "functions-framework --target=index --debug"
}
...
Enter fullscreen mode Exit fullscreen mode

index.js

The index.js file is the core of this setup. It’s where we will expose all functions combined on a single local address, as well as expose functions individually.

// src/index.js
import express from "express"
import { firstFunction } from "./functions/firstFunction.js";
import { secondFunction } from "./functions/secondFunction.js";

// Solution to expose multiple cloud functions locally
const app = express();
app.use('/firstFunction', firstFunction);
app.use('/secondFunction', secondFunction);


export {app as index, firstFunction, secondFunction};
Enter fullscreen mode Exit fullscreen mode

The local functions-framework will target the index export exported from index.js, see the package.json above. The index export is only meant for local development purposes, so we can run multiple functions at once locally.

We still export the individual functions too, so we can easily deploy both functions individually. See “Deploying functions individually” below.

Running the functions together

Now if we run npm run start, a development server will start on http://localhost:8080.

Running curl http://localhost:8080/firstFunction will print OK! You were redirected here., demonstrating that both functions are running at the same time.

If you still want to test a function in isolation, you can run functions-framework --target=firstFunction instead, after which you can call it as usual with curl http://localhost:8080.

Deploying functions individually

Functions can still be individually deployed, with the gcloud CLI:

gcloud functions deploy firstFunction --project=my-project --runtime nodejs16 --trigger-http --allow-unauthenticated  --security-level=secure-always --region=eyour-region --entry-point=firstFunction --memory=128MB --timeout=60s
Enter fullscreen mode Exit fullscreen mode
gcloud functions deploy secondFunction --project=my-project --runtime nodejs16 --trigger-http --allow-unauthenticated  --security-level=secure-always --region=your-region --entry-point=secondFunction --memory=128MB --timeout=60s
Enter fullscreen mode Exit fullscreen mode

The key here is --entrypoint firstFunction flag, which is similar to --target flag on the functions-framework command. It selects the module export of the index script that should be seen as the entry point for the cloud function.

You could also deploy the index export as a single function that combines all functions in one, but then you would have call /index/firstFunction and /index/secondFunction on the cloud, and you then can’t scale or modify the function runtimes individually anymore.


Caveats

Using Express “sub apps” this way is not a 100% watertight way to emulate multiple individual functions running in Google Cloud. There are some caveats.

req.originalUrl ⚠️

Express Docs

This property is much like req.url; however, it retains the original request URL, allowing you to rewrite req.url freely for internal routing purposes. For example, the “mounting” feature of app.use() will rewrite req.url to strip the mount point.

 req.originalUrl doesn’t behave as it would in a real production Google Cloud multi-function setup.

On Google Cloud, req.originalUrl excludes the function name. This is likely due to some internal redirections that Google Cloud does.

With the index.js emulator proposed by this post, req.originalUrl will still include the function name.

Make sure that your code does not depend on the value req.originalUrl for some decisions. If it does, you might need to adapt this code.

req.path behavior ✅

In Google Cloud, accessing req.path in a function running on https://your-google-cloud-domain/firstFunction will yield /, and not /firstFunction (somewhat suprisingly).

With the above caveat, you might wonder, does this behavior also not copy to our local emulator setup?

The answer is: this behavior remains the same. See the Express documentation for req.path:

When called from a middleware, the mount point is not included in req.path.

When we call app.use('/firstFunction', firstFunction);, we register firstFunction as application-level middleware onto the app with the “mount point” being /firstFunction.

More?

There might be other caveats I’m not aware of with this setup, but for now it suits my purposes, and the added local development convenience is worth any future complications.

References

Closing note: this solution is based on an answer in a related GitHub Issue thread that you might have seen already when researching this issue.

I wrote this post because that thread contains several approaches to this issue that were less relevant to my use-case (and, the thread was also filled with more drama than necessary). I hope developers used to the Firebase functions model find this setup suggestion helpful.

💖 💪 🙅 🚩
th0rgall
Thor Galle

Posted on October 27, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related