functions-framework-nodejs icon indicating copy to clipboard operation
functions-framework-nodejs copied to clipboard

How to use with multiple cloud functions

Open RSpace opened this issue 6 years ago • 31 comments

I have a handful of cloud functions that work together and plan to add more. With the NodeJS 6 Cloud Function Emulator, I can run all of them on a single port for local development and manual integration testing.

With this framework, I need to run each cloud function on a separate port, which can become quite a hassle as the number of functions increase.

How will multiple Cloud Functions be better supported going forward?

RSpace avatar Apr 23 '19 11:04 RSpace

Hi RSpace -- this is the second time I've seen this feedback. We have some thinking to do. I don't have a good answer for you just yet but wanted to acknowledge that we're aware of your request.

stew-r avatar Apr 23 '19 23:04 stew-r

Hi ! I'm also interested in this feature. Especially since I'm working with Docker, it means that I need to restart my container when I want to work on another function... Thanks !

watsab avatar Apr 24 '19 07:04 watsab

Just came to see if there was an answer. Unfortunately, this is currently a blocker for me.

jessep avatar Apr 25 '19 15:04 jessep

Same issue here. Not sure how I should/could spin up all the functions at the same time for local development without a huge overhead

dennisnewel avatar Apr 27 '19 05:04 dennisnewel

We'll need to look into how to configure a reverse proxy to host multiple functions on the sample port. This would probably be a wrapper library on top of the functions framework.

Technically speaking, for both local and GCF functions, hosting multiple functions is the same as hosting multiple Node servers.

Some reading: https://itnext.io/hosting-multiple-apps-on-the-same-server-implement-a-reverse-proxy-with-node-a4e213497345

grant avatar Apr 27 '19 20:04 grant

Is the goal having a single port, or having an easy way to spin up multiple functions without port conflicts?

Much simpler than a proxy would be a script that can look at a directory structure such as:

  • my-functions
    • function1
    • function2
    • function3

Then figure out how to assign an unused port to each of them, starting each in sequence.

Developers can implement aspects of this for themselves, here's a couple approaches to help demonstrate the point and enable anyone that wants to try rolling their own solution:

Multiple Functions in One Package

One npm start can be run to spin up all the functions.

First, add "concurrently", one of several packages that facilitate concurrent command execution in a npm script:

npm install --save-dev concurrently

Configure package.json scripts.start:

"concurrently --kill-others \"PORT=9001 functions-framework --target=function1\" \"PORT=9002 functions-framework --target=function2\""

One Function per Package

The developer will visit the directory of each function they want, running npm start as needed:

Let's collect a couple lessons as we go to build up to the new start command:

Decide how to count the number of running function instances:

$(ps -ef | grep function-framework | wc -l)

Route the output to a log file so we can do all this work in a single shell:

...  >> ff.log 2>&1

Configure package.json scripts.start:

PORT=$((PORT+$(ps -ef | grep function-framework | wc -l))) functions-framework --target=function1 >> ff.log 2>&1

Configure the environment for a starting PORT variable:

export PORT=9000

grayside avatar May 15 '19 18:05 grayside

For me it’s about having a single port as that’s how the functions are consumed once in GCF. It’d be a fair amount of extra work to update my front end (JavaScript single page web app) to hit different ports for different API endpoints (functions) for just one environment.

I could likely do all this myself in 15 different ways, but the reason I’m hanging on to the old emulator is that I don’t have to :) it works similarly enough to GCF that I don’t have to worry about it

dennisnewel avatar May 15 '19 18:05 dennisnewel

What was the reason to archive the old emulator?

quantuminformation avatar May 28 '19 08:05 quantuminformation

It is not, and has not been, properly supported for a while. The Functions Framework is actually used as part of the core product -- we add it to your function code to make it runnable -- so that's a long-term supported path.

We understand that there is currently a use case that is not as seamless using this new tool ("I want to serve multiple functions on my local machine and have them represented in a manner similar to how they're served in production"). We have a few potential solutions in mind, just need to get around to evaluating/implementing them.

stew-r avatar May 31 '19 23:05 stew-r

I'm still wary of the idea of a proxy.

Now I have two dimensions of a project in the context of functions (as a service): (1) functions as Google Cloud entities, (2) functions as elements of a code base. I already use something like http-proxy-middleware in some projects. And I'm worried about the large number of middle proxies.

Does it complicate the development and deployment system?

All my projects already look like @grayside @grant said.

oshliaer avatar Jun 27 '19 04:06 oshliaer

Found a simple workaround for this - just define an "index" function which then calls your other functions for local development, e.g.:

export function index(req: Request, res: Response) {
  switch (req.path) {
    case '/oauth2_init':
      return oauth2_init(req, res)
    case '/oauth2_callback':
      return oauth2_callback(req, res)
    case '/my_func':
      return my_func(req, res)
    default:
      res.send('function not defined')
  }
}

and have that as the target when running the function framework locally, i.e.:

npx @google-cloud/functions-framework --target=index

dupski avatar Jul 12 '19 00:07 dupski

@dupski would you use the same index for prod?

quantuminformation avatar Jul 13 '19 07:07 quantuminformation

What do you guys think of the approach that is being done here:

https://github.com/gothinkster/gcp-datastore-cloud-functions-realworld-example-app/blob/master/index.js

specifically with this router:

https://github.com/gothinkster/gcp-datastore-cloud-functions-realworld-example-app/blob/master/src/API.js

Could this play nice with testing multiple functions locally with the FF?

The only thing with that repo is that is isn't that recent, if anyone has any other examples of Real world apps using GC, I'm all ears.

quantuminformation avatar Jul 13 '19 07:07 quantuminformation

@QuantumInformation I guess you could do, but I export them seperately as well for prod at the moment

dupski avatar Jul 13 '19 08:07 dupski

To my thinking, it is important that the solution work the same for GCF and Cloud Run (and any other Knative) but I guess that's guaranteed since they will all be based on the Functions Framework.

But it also be nice if it were not too difficult to transition from Firebase Functions to Google Cloud Functions.

Something that is nice about Firebase is that we can have a single node project / package that can handle all of the functions and events needed for that app. I guess Firebase does by allowing an unlimited number of handlers in the one node project, but then the tool does as many deployments as needed. It services all HTTP functions from a single port during emulation, but one weakness is that it doesn't support any local emulation for events.

GitTom avatar Aug 19 '19 21:08 GitTom

@dupski yeah if you can export them separately then you have better analytics.

quantuminformation avatar Sep 06 '19 12:09 quantuminformation

Related, but not exactly multiple cloud functions is Express Routing with Cloud Functions: https://medium.com/google-cloud/express-routing-with-google-cloud-functions-36fb55885c68

Similar to the @dupski's suggestion, Express can be used for sub-routes.

Basic code:

const express = require('express');

// Create an Express object and routes (in order)
const app = express();
app.use('/users/:id', getUser);
app.use('/users/', getAllUsers);
app.use(getDefault);

// Set our GCF handler to our Express app.
exports. index = app;

Then call your function like http://localhost:8080/index/users/123.


Exposing multiple Node cloud functions on the same port is like exposing multiple express apps on the same port. Here are example solutions: https://stackoverflow.com/a/11228833/1233286 https://stackoverflow.com/a/11226253/1233286

grant avatar Sep 20 '19 14:09 grant

finally got around to trying this out and hit a snag that others might be interested in.

I followed Russel Briggs' approach as that seemed to fit best with how i was thinking of my functions (a bunch of different functions that each make up an endpoint in a RESTful API)

The problem I faced was that I was doing additional routing inside the function (i.e. get /customers, get customers/:id/reports) and with production functions the name of the function is removed from the req.path (which I was using for internal routing), so a call to /customers/:id/reports would end up being /:id/reports by the time I got to internal routing. The setup suggested by Russel didn't do this "snipping" so the request /customers/:id/reports would be exactly the same: /customers/:id/reports which messed up my internal routing.

The solution is basically to do this snipping via the req.url parameter, before passing on the request; this will magically update all the other parameters and "snip" off the function name. It'll look something like this:

case "customers":
  req.url = req.url.replace("/customers","");
  return customers(req, res);

So simple yet so elusive...took a couple of attempts :) hopefully this can save someone else a headache

On Fri, Sep 20, 2019 at 7:52 AM Grant Timmerman [email protected] wrote:

Related, but not exactly multiple cloud functions is Express Routing with Cloud Functions:

https://medium.com/google-cloud/express-routing-with-google-cloud-functions-36fb55885c68

Similar to the @dupski's suggestion https://github.com/GoogleCloudPlatform/functions-framework-nodejs/issues/23#issuecomment-510704908, Express can be used for sub-routes.

Basic code:

const express = require('express'); // Create an Express object and routes (in order)const app = express();app.use('/users/:id', getUser);app.use('/users/', getAllUsers);app.use(getDefault); // Set our GCF handler to our Express app.exports. index = app;

Then call your function like http://localhost:8080/index/users/123.

Exposing multiple Node cloud functions on the same port is like exposing multiple express apps on the same port. Here are example solutions: https://stackoverflow.com/a/11228833/1233286 https://stackoverflow.com/a/11226253/1233286

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/GoogleCloudPlatform/functions-framework-nodejs/issues/23?email_source=notifications&email_token=AAHK6VYLVX77DJPIITX4UR3QKTPT3A5CNFSM4HHXE6ZKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD7G6ESY#issuecomment-533586507, or mute the thread https://github.com/notifications/unsubscribe-auth/AAHK6V5P5TNYR7LUO4ULYPLQKTPT3ANCNFSM4HHXE6ZA .

dennisnewel avatar Sep 29 '19 06:09 dennisnewel

How could one do the same for event triggered functions?

lvl99 avatar Jan 09 '20 09:01 lvl99

finally got around to trying this out and hit a snag that others might be interested in. I followed Russel Briggs' approach as that seemed to fit best with how i was thinking of my functions (a bunch of different functions that each make up an endpoint in a RESTful API) The problem I faced was that I was doing additional routing inside the function (i.e. get /customers, get customers/:id/reports) and with production functions the name of the function is removed from the req.path (which I was using for internal routing), so a call to /customers/:id/reports would end up being /:id/reports by the time I got to internal routing. The setup suggested by Russel didn't do this "snipping" so the request /customers/:id/reports would be exactly the same: /customers/:id/reports which messed up my internal routing. The solution is basically to do this snipping via the req.url parameter, before passing on the request; this will magically update all the other parameters and "snip" off the function name. It'll look something like this: case "customers": req.url = req.url.replace("/customers",""); return customers(req, res); So simple yet so elusive...took a couple of attempts :) hopefully this can save someone else a headache On Fri, Sep 20, 2019 at 7:52 AM Grant Timmerman @.***> wrote: Related, but not exactly multiple cloud functions is Express Routing with Cloud Functions: https://medium.com/google-cloud/express-routing-with-google-cloud-functions-36fb55885c68 Similar to the @dupski's suggestion <#23 (comment)>, Express can be used for sub-routes. Basic code: const express = require('express'); // Create an Express object and routes (in order)const app = express();app.use('/users/:id', getUser);app.use('/users/', getAllUsers);app.use(getDefault); // Set our GCF handler to our Express app.exports. index = app; Then call your function like http://localhost:8080/index/users/123. ------------------------------ Exposing multiple Node cloud functions on the same port is like exposing multiple express apps on the same port. Here are example solutions: https://stackoverflow.com/a/11228833/1233286 https://stackoverflow.com/a/11226253/1233286 — You are receiving this because you commented. Reply to this email directly, view it on GitHub <#23?email_source=notifications&email_token=AAHK6VYLVX77DJPIITX4UR3QKTPT3A5CNFSM4HHXE6ZKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD7G6ESY#issuecomment-533586507>, or mute the thread https://github.com/notifications/unsubscribe-auth/AAHK6V5P5TNYR7LUO4ULYPLQKTPT3ANCNFSM4HHXE6ZA .

at GCF context there was anv variable FUNCTION_NAME with name of function being executed. Does local functions-framework provide that part of execution framework?

koteisaev avatar Jul 01 '20 22:07 koteisaev

Found a simple workaround for this - just define an "index" function which then calls your other functions for local development

@dupski AFAIK by bundling multiple functions together you could be making cold starts worse depending on the number of dependencies each function is using.

PierBover avatar Jul 22 '20 18:07 PierBover

How could one do the same for event triggered functions?

I managed to figure this out (only because I now just started to test with PubSub triggered functions!). Regarding my setup, this is ONLY for localhost testing.

Generally I have a basic function which acts as a router for locally testing all my functions (so not destined for deploying to Cloud Function!)

I can trigger a PubSub function using the router to feed in a PubSub event based on a request:

// The functions I want to test
const { testHttp } = require("./testHttp");
const { testPubSub } = require("./testPubSub);

/**
 * Convert a JSON request to rudimentary PubSub event:
 *
 * Request body:
 *
 * {"data":"This is a test","attributes":{"example":"attribute"}}
 *
 * Converts to:
 *
 * {"data":"VGhpcyBpcyBhIHRlc3Q=","attributes":{"example":"attribute"}}
 *
 * This is just a convenience method to base64 encode the data property.
 */
const simulatePubSubEventFromRequest = (req) => {
  if (
    req.headers["content-type"].indexOf("application/json") > -1 &&
    Object.getPrototypeOf(req.body) === Object.prototype
  ) {
    const message = {};
    if (req.body.attributes) message.attributes = req.body.attributes;
    if (req.body.data) {
      const data = Buffer.from(Object.getPrototypeOf(req.body.data) === Object.prototype
        ? JSON.stringify(req.body.data)
        : req.body.data, 'binary').toString('base64');
      message.data = data;
    }
    return message;
  }
}

/**
 * Rudimentary router function only for localhost testing
 */
exports.index = (req, res) => {

  // Routing based on the path, e.g. http://localhost:8080/testPubSub
  switch (req.path) {

    case "/testHttp":
      return testHttp(req, res);

    case "/testPubSub":
      return testPubSub(simulatePubSubEventFromRequest(req));

    default:
      res.send("No function to test!");
  }
}

lvl99 avatar Sep 21 '20 10:09 lvl99

Found a simple workaround for this - just define an "index" function which then calls your other functions for local development

@dupski AFAIK by bundling multiple functions together you could be making cold starts worse depending on the number of dependencies each function is using.

Yes, but I think that solution is for development purpose only. Otherwise a reverse proxy is the only way to run multiple functions. Like when you're developing an OAuth login /login & /callback are needed at the same time. something like functions-framework --targets=auth, callback would be really helpful here. 😅

sarangnx avatar Nov 11 '20 14:11 sarangnx

Still no solution for this problem?

roytouw7 avatar Jul 31 '21 16:07 roytouw7

GCP a million miles behind the competition in terms of serverless function capabilities and dev experience. This has been in beta for over a year.

Still not clear...

  • How to run multiple functions in single setup?!
  • Surely doesn't make sense to have one function per package.json? 👎🏻
  • Also, when running multiple sets of function, port clashes?

More...

  • Global edge functions, where are they?
  • Modern build tools, esbuild, vite, where's the modernization and integration?
  • Firebase using Gen2 functions, where's the support?

The competition...

  • AWS have near mili second deployments on lambda functions through serverless framework or CDK.
  • How does firebase functions or gen2 stack up against the competition?
  • Still having discussions on how to get multiple functions running in a single file....

It's all love here for Google tech, but I am seriously left feeling like GCP is being left behind at the moment.

Ultimately why has this issue been closed, it hasn't really been resolved?

algoflows avatar May 21 '22 12:05 algoflows

@algoflows I see you're a clever man. But I can't understand why you are trying to propagate something vague in a specific question thread. Do you have something wrong with GCP? Relax. Change it to whatever suits you!

oshliaer avatar May 23 '22 07:05 oshliaer

@contributorpw Fully relaxed here, just giving feedback that's all :)

GCP Suits fine, but there's defo room for improvement especially with Gen2 functions considering the improvements across the space. Surely feedback should be encouraged, even if it's a little blunt, better to talk straight about the product and service you invision.

algoflows avatar May 23 '22 07:05 algoflows

@algoflows atm I just create a new cloud function per unit of business logic, I used to use express on firebase functions but its just bloat. Glad to see you around

quantuminformation avatar May 23 '22 07:05 quantuminformation

GCP a million miles behind the competition in terms of serverless function capabilities and dev experience. This has been in beta for over a year.

Still not clear...

  • How to run multiple functions in single setup?!
  • Surely doesn't make sense to have one function per package.json? 👎🏻
  • Also, when running multiple sets of function, port clashes?

More...

  • Global edge functions, where are they?
  • Modern build tools, esbuild, vite, where's the modernization and integration?
  • Firebase using Gen2 functions, where's the support?

The competition...

  • AWS have near mili second deployments on lambda functions through serverless framework or CDK.
  • How does firebase functions or gen2 stack up against the competition?
  • Still having discussions on how to get multiple functions running in a single file....

It's all love here for Google tech, but I am seriously left feeling like GCP is being left behind at the moment.

Ultimately why has this issue been closed, it hasn't really been resolved?

You can build a router based on req.path in your Cloud Function along with whatever functionality you want to route it to. Am I missing something with what you're asking for? I currently only do this for localhost testing, but I think it's def possible in prod.

It's also super easy to connect Cloud Functions with Endpoints, and you get added benefit of easy way to restrict access (via API keys or OAuth) and rate limit. With Endpoints you can configure to forward the path, e.g. https://api.example.com/hello/whatever would be sent to https://example.cloudfunctions.net/example/hello/whatever. If you really wanted you could have a single Cloud Function managing different endpoints.

Personally I like having atomic Cloud Functions, but it does make it slightly more complicated/management overhead when working on shared functionality and needing to deploy 5+ CFs. I've got a home-build compilation (using Webpack) and deployment system which seems to work well for my needs though (compiles JS and tree-shakes unused functionality, packages, etc.).

Everything is possible!

Everything is possible!

lvl99 avatar May 23 '22 07:05 lvl99

Thanks for the comment @algoflows.

There are a couple options for multiple cloud functions, depending on the use-case, discussed above:

  • URL Routing: Route requests based on the URL path (custom or using Express routing)
  • Use Firebase Functions : https://firebase.google.com/docs/functions/organize-functions
    • See https://github.com/firebase/functions-samples/issues/384 (based on user feedback)
  • Use Pub/Sub: Connect functions in an event-driven architecture

Ideally your function does only one thing and does it really well. This allows your functions to scale independently and encourages a microservice architecture (rather than large monolithic function).

Personally, using these options have worked for my apps. The function frameworks are purposefully lightweight, making it easier to use other tools, like esbuild, webpack, typescript, etc for Node functions. I don't recommend hosting frontend / vite apps in a function for multiple reasons (use Cloud Run).


It would be helpful if there's a specific pain-point or an issue with one of the above options. Maybe we could add some tooling or enable a specific devX with this tool or a different tool.

We've left this issue open to encourage discussion. As @lvl99 said, anything is possible.

grant avatar May 23 '22 17:05 grant