Skip to content

How to use with multiple cloud functions #59

Description

@RSpace

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?

Activity

  1. stew-r commented on Apr 23, 2019

    @stew-r

    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.

  2. watsab commented on Apr 24, 2019

    @watsab

    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 !

  3. jessep commented on Apr 25, 2019

    @jessep

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

  4. dennisnewel commented on Apr 27, 2019

    @dennisnewel

    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

  5. grant commented on Apr 27, 2019

    @grant
    Contributor

    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

  6. grayside commented on May 15, 2019

    @grayside
    Contributor

    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
    
  7. dennisnewel commented on May 15, 2019

    @dennisnewel

    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

  8. quantuminformation commented on May 28, 2019

    @quantuminformation

    What was the reason to archive the old emulator?

  9. stew-r commented on May 31, 2019

    @stew-r

    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.

  10. oshliaer commented on Jun 27, 2019

    @oshliaer

    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.

  11. dupski commented on Jul 12, 2019

    @dupski

    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
  12. quantuminformation commented on Jul 13, 2019

    @quantuminformation

    @dupski would you use the same index for prod?

  13. quantuminformation commented on Jul 13, 2019

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

  14. dupski commented on Jul 13, 2019

    @dupski

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

  15. GitTom commented on Aug 19, 2019

    @GitTom

    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.

  16. 14 remaining items

  17. grant commented on May 23, 2022

    @grant
    Contributor

    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.

  18. oshliaer commented on May 31, 2022

    @oshliaer

    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?

  19. 0xdevalias commented on Dec 19, 2022

    @0xdevalias

    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.

  20. davidhughhenrymack commented on Feb 15, 2023

    @davidhughhenrymack

    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)

  21. lvl99 commented on Feb 15, 2023

    @lvl99

    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:

    • Cloud functions are designed to take an input via trigger (http or pubsub) to perform an operation
    • That you could cram in as few (1) or as many (to the moooon) operations as you want on and configure your single function with a router based on req.path (http trigger only — pubsub could have switch..case routing based on some arbitrary property provided in the payload)

    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.

  22. josephlewis42 commented on May 24, 2023

    @josephlewis42

    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.

  23. koistya commented on Oct 9, 2023

    @koistya

    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

  24. valentijnnieman commented on Oct 17, 2023

    @valentijnnieman

    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!

  25. jokester commented on Apr 17, 2024

    @jokester

    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 can believe 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, and I recommend people reading this, to use time more efficiently.

  26. servefast-cto commented on Oct 8, 2024

    @servefast-cto

    Any news on this any development done regarding this ? We always need some kind of hacks to make things work with GCP so disappointing

  27. Dzivo commented on Nov 28, 2024

    @Dzivo

    I keep getting to this issue for a year now and want to migrate from firebase emulator to functions framework but without this it is not worth it just makes the setup to complicated

  28. aleclarson commented on May 2, 2025

    @aleclarson

    I've made a minimal solution that you can integrate into your own project by copying a single file. It uses esbuild to compile and watch the individual modules containing your Google Cloud Run functions. TypeScript is supported. The script is easily customizable (e.g. preferred filename pattern, root directory).

    https://gist.github.com/aleclarson/97c1d93e2c784a4570def280d6c88b00

    It's licensed as MIT. I believe Google should merge my solution into the @google-cloud/functions-framework package for Node.js to achieve an optimal DX.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions