GoogleCloudPlatform / functions-framework

The Contract for Building New Function Frameworks
Apache License 2.0
120 stars 12 forks source link

How to use with multiple cloud functions #59

Open RSpace opened 5 years ago

RSpace commented 5 years ago

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?

stew-r commented 5 years ago

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.

watsab commented 5 years ago

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 !

jessep commented 5 years ago

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

dennisnewel commented 5 years ago

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

grant commented 5 years ago

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

grayside commented 5 years ago

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:

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
dennisnewel commented 5 years ago

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

quantuminformation commented 5 years ago

What was the reason to archive the old emulator?

stew-r commented 5 years ago

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.

oshliaer commented 5 years ago

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.

dupski commented 4 years ago

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
quantuminformation commented 4 years ago

@dupski would you use the same index for prod?

quantuminformation commented 4 years ago

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.

dupski commented 4 years ago

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

GitTom commented 4 years ago

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.

quantuminformation commented 4 years ago

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

grant commented 4 years ago

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

dennisnewel commented 4 years ago

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 notifications@github.com 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/issues/59, 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/issues/59?email_source=notifications&email_token=AAHK6VYLVX77DJPIITX4UR3QKTPT3A5CNFSM4HHXE6ZKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOD7G6ESY#issuecomment-533586507, or mute the thread https://github.com/notifications/unsubscribe-auth/AAHK6V5P5TNYR7LUO4ULYPLQKTPT3ANCNFSM4HHXE6ZA .

lvl99 commented 4 years ago

How could one do the same for event triggered functions?

koteisaev commented 4 years ago

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?

PierBover commented 3 years ago

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.

lvl99 commented 3 years ago

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!");
  }
}
sarangnx commented 3 years ago

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. 😅

roytouw7 commented 2 years ago

Still no solution for this problem?

algoflows commented 2 years ago

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...

More...

The competition...

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?

oshliaer commented 2 years ago

@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!

algoflows commented 2 years ago

@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.

quantuminformation commented 2 years ago

@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

lvl99 commented 2 years ago

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!

grant commented 2 years ago

Thanks for the comment @algoflows.

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

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.

oshliaer commented 2 years ago

In any case, the problem should only occur if we update many functions with common settings. Then it really takes place as a problem. But the function itself should not implement or somehow replace the server (as a service). Even feature limitations hint at this.

I sometimes run into feature publishing troubles when I update a shared module. Sometimes I have to create an npm module. And this is not always convenient in dynamic development.

Perhaps having a better publish-test tool made this easier somehow.

In any case, this has nothing to do with the functions themselves. I can hit nails not only with a hammer, but why?

0xdevalias commented 1 year ago

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.

@grant Discoverability would be the first/main one.

Ideally one (or more) of these options should be handled natively in this lib for local dev purposes. Failing that, these methods should be clearly and prominently documented in the README, as well as the relevant google cloud docs locations that describe 'testing locally'.

If I had my 'perfect world scenario', it would be to be able to use the firebase emulators for standard google-cloud functions without having to actually configure my project/etc to enable firebase.

I shouldn't need to stumble my way through google searches and random issue threads to find an answer to what is a pretty core need/feature that I would have expected to have been supported from the very beginning.

davidhughhenrymack commented 1 year ago

Echoing above - just started writing cloud functions, and was really shocked that you can't simply serve a function per URL (e.g. like Firebase)

lvl99 commented 1 year ago

Echoing above - just started writing cloud functions, and was really shocked that you can't simply serve a function per URL (e.g. like Firebase)

I’m not a Google dev, but I’ve been following this topic for a while and have also resolved it using two methodologies clearly explained in this thread. I feel from the gist your message that maybe you haven’t read this thread completely to understand that:

You are only limited by your imagination, memory, and timeout. The more operations you add to your function, the more code your function requires, the heavier the operation, the higher potential for maxing out memory and timing out.

If you want something more traditional to handle requests, like a monolith app, try App Engine. The benefit of Cloud Functions is that they run independently, quickly, and have a different scaling mechanism: can run more operations in parallel, with some trade-offs (e.g. no shared storage/local memory, unless you combine with Cloud Storage or Firestore, etc.).

It’s up to you to pick the right tool for the job, not wangle a potentially inappropriate tool for whatever requirements you may have.

josephlewis42 commented 1 year ago

I'm transferring this issue to the https://github.com/GoogleCloudPlatform/functions-framework repo because it's a common request we get across the languages that GCF supports. It was originally in ff-node.

koistya commented 8 months ago

As a workaround, reducing the number of deployed functions in favor of multi-route functions can work well in some cases.

For example, assuming you want to handle a bunch of API endpoints looking as follows:

  GET https://example.com/api/v1/posts
 POST https://example.com/api/v1/posts
  GET https://example.com/api/v1/posts/123
PATCH https://example.com/api/v1/posts/123

Create a new file for each URL route:

/v1/posts/[id].ts
/v1/posts/index.ts
/v1/authors/[username].ts
/v1/authors/index.ts

Where each route matches the following signature:

import { HttpFunction } from "@google-cloud/functions-framework";

/**
 * Fetches a post by ID.
 *
 * @example GET /api/v1/posts/1 
 */
export const GET: HttpFunction = async (req, res) => {
  const url = `https://jsonplaceholder.typicode.com/posts/${req.params.id}`;
  const fetchRes = await fetch(url);
  const post = await fetchRes.json();
  res.send(post);
};

/**
 * Updates an existing post.
 *
 * @example PATCH /api/v1/posts/1
 */
export const PATCH: HttpFunction = async (req, res) => {
  throw new Error("Not implemented");
};

At the top level, in the entry point, just load the list of files from the "routes" folder, e.g.

// https://vitejs.dev/guide/features.html#glob-import
const routeFiles = import.meta.glob(["./v1/**/*.ts", "!./**/*.test.ts"]);

Append a RegExp for each route derived from the filename using path-to-regexp, then iterate through the files and execute the matching handler function:

import { HttpFunction, http } from "@google-cloud/functions-framework";
import { HttpError, NotFound } from "http-errors";
import { match } from "path-to-regexp";

// https://vitejs.dev/guide/features.html#glob-import
const routeFiles = import.meta.glob(["./v1/**/*.ts", "!./**/*.test.ts"]);
const routes = Object.entries(routeFiles).map(...);

http("api", async (req, res) => {
  try {
    for await (const route of routes) {
      const match = route.match(req.path);

      if (match) {
        Object.assign(req.params, match.params);
        const module = await route.import();
        await module?.[req.method]?.(req, res);
        if (res.headersSent) return;
      }
    }

    throw new NotFound();
  } catch (err) {
    // TODO: Send an error response
  }
});

A unit test for each route would look similar to this (e.g. v1/posts/[id].test.ts):

import { getTestServer } from "@google-cloud/functions-framework/testing";
import supertest from "supertest";
import { expect, test } from "vitest";
import { functionName } from "../../index";

test("GET /api/v1/posts/1", async () => {
  const server = getTestServer(functionName);
  const res = await supertest(server)
    .get("/api/v1/posts/1")
    .set("Accept", "application/json");

  expect({ statusCode: res.statusCode body: res.body })
    .toEqual({/* ... */});
});

https://github.com/koistya/cloud-functions-routing

valentijnnieman commented 8 months ago

We have a set up where we have an API gateway on GCP that routes to different GC Functions. This works pretty well, every API endpoint is a separate, stand-alone function. However, local development of this is a bit of a pain. The current solution is to just have a Flask server that routes requests to different endpoints, by importing the separate function's code. That isn't ideal, because it means it will reload every function whenever something changes, which is very slow (some of the functions use SpaCy models, which are big). Currently looking into the example using skaffold + minikube, but not crazy about the idea of using k8s to do this just for local development.

TL:DR; I'd love to have some sort of documented solution for locally running multiple functions!

edit: Another solution is of course to have the router not import code, but route to different functions running on different ports. But that does mean we have to either run every function in a separate terminal window, or find a way to run all of them and combine the output to one window. It's possible, but I wanted to leave a +1 here for having some sort of documented solution for this problem!

jokester commented 2 months ago

Sorry for not being constructive.

I tried to build a pub/sub handler in JS, to transform events and archive them. After hours of struggling I started to think Cloud Function (gen2) is not the right product for me. I think I will just use Cloud Run in future.

I'm confident it works for a short JS function one can type in 1min. But a real Pub/Sub handler involving multiple modules and libraries is surprisingly hard to do right. Using a bundler can break it in a way you never expected. And it's not the last problem I encountered.

Cloud Function (gen2) uses Cloud Run anyway. You can see how the container image is built in Cloud Build. It's just the build and runtime spec is too cryptic.

Its unique capability seemed to me is to allocate fraction of 1 CPU. But the time I spent debugging already exceeded the cost of a few CPU months. I had to use my time more efficiently.