Local Cloud Functions


Anyone I’ve worked with recently knows how I feel about cloud functions. They’re fantastic. Cloud Functions were also my introduction to Python when my old boss gave me a function in node.js and said “write this in Python”.

If you’re familiar with cloud functions then feel free to skip to the instructions, but for those who are just starting off, a Cloud Functions are part of Google’s serverless code offering along with App Engine, which I also rate, and Cloud Run, which I have yet to delve into. Between them they offer developers an easy way to deploy and run code without having to worry about provisioning instances or patching servers. You just make the code the best you can and Google will look after the rest.

However, there is a but (if there weren’t then this would be a pretty boring post). I really hate the deployment of functions. You write your code and open the interface, or maybe you prefer the CLI, and deploy the code. Great. Then you try and run it, only to find out that it doesn’t work. At least that was what happened for me, my first function had over 60 versions before it finally did what I wanted! At the time I kept thinking that there must be a better way to test this than endlessly deploying versions until it works and as it so happens there is…

The functions framework is a package produced by Google to enable you to simulate the cloud function endpoint on your local machine. Although this is available in multiple languages I’m going to focus on Python as that’s the language I use most.

Triggers

There are a number of different ways to invoke a cloud function and which method you choose depends on your use case. All the available methods are listed in the Google Cloud documentation, but probably the two most common and flexible methods are HTTP Requests and PubSub Topics.

HTTP requests are probably the simplest trigger: You have an endpoint, send a request, get a response. It’s great if you have other dependencies that need the output of your function e.g. a function that predicts the next page a user will visit so it can be pre-loaded. You call the function, it runs and you get the output so you can pre-load that page.

PubSub, Google’s managed messaging stream, is suitable for any of the reasons you’d need a messaging queue for. If you want to send the same payload to multiple places, if you want increased fault tolerance in case part of your pipeline fails or if you need to manage high velocity data. Amazon has a great page covering these reasons and a few others as well.

Running Functions Locally

Cloud functions are the simplest trigger and the best place to start, but I will cover PubSub in a future post. When you run a function, you’ll get a cloud functions endpoint in the format:

REGION-PROJECT_NAME.cloudfunctions.net/FUNCTION_NAME

The endpoint can receive both POST and GET requests. Both POST and GET requests are used to return values from your server; GET requests use querystring parameters, which are limited in size, whereas POST requests send data in the message body so can send more data. There are benefits and drawbacks to both, so if you’re not sure which one to use, have a look at the w3 schools explanation.

For this example, we’re going to use Google’s Hello World function from their python sample docs GitHub. You can download the file from GitHub or copy and paste it from below and save it as main.py.

from flask import escape

def hello_http(request):
    """HTTP Cloud Function.
    Args:
        request (flask.Request): The request object.
        <http://flask.pocoo.org/docs/1.0/api/#flask.Request>
    Returns:
        The response text, or any set of values that can be turned into a
        Response object using `make_response`
        <http://flask.pocoo.org/docs/1.0/api/#flask.Flask.make_response>.
    """
    request_json = request.get_json(silent=True)
    request_args = request.args

    if request_json and 'name' in request_json:
        name = request_json['name']
    elif request_args and 'name' in request_args:
        name = request_args['name']
    else:
        name = 'World'
    return 'Hello {}!'.format(escape(name))

This is a basic function that returns “Hello {Name}!”, if a name is provided, or “Hello World!” if it’s not.

You can see the if…elif…else statement, that uses the name parameter from the JSON body (a POST request), or it uses the name query string parameter (a GET request), or if neither parameter is present, it defaults to “World”. The escape function from the Flask package is used to convert certain characters (&<>”’) into HTML safe characters, so users can’t inject HTML into the function via the request

Prerequisites

This demo uses Python 3, so if you don’t have that, you’ll need to install it for your operating system. You’ll also need the pip and virtualenv packages (or your preferred method of managing environments).

Environment Setup

First, let’s create a new environment:

python3 -m venv env

And then activate the environment:

On Mac/Linux this is done with:

source env/bin/activate

On windows this is done with:

\env\Scripts\activate.bat

Next, install all the packages we’ll need for this demo:

pip3 install flask functions-framework

That’s the environment set up, you’re ready to test the function.

Execution

First make sure that you’re in the same folder as your main.py file. Then start the functions framework by typing:

functions-framework --target=hello_http

This will start the endpoint and direct it to the function. If you’re doing this with your own function, this would be the main function of the file.

There are a few flags that you can add to this to customise how the framework runs, such as changing which port the function runs on, the name of the file to execute and the type of trigger for the function. The Function Signature Type is one to remember as it allows you to test PubSub triggers, and will be covered in a future blog post.

Command-line flag Environment variable Description
--host HOST The host on which the Functions Framework listens for requests.
Default: 0.0.0.0
--port PORT The port on which the Functions Framework listens for requests.
Default: 8080
--target FUNCTION_TARGET The name of the exported function to be invoked in response to requests.
Default: function
--signature-type FUNCTION_SIGNATURE_TYPE The signature used when writing your function. Controls unmarshalling rules and determines which arguments are used to invoke your function.
Default: http; accepted values: http or event
--source FUNCTION_SOURCE The path to the file containing your function.
Default: main.py (in the current working directory)
--debug DEBUG A flag that allows to run functions-framework to run in debug mode, including live reloading.
Default: False

Source: Functions Framework GitHub

Once the function is running, you can go to your localhost and see the end result: local-cloud-functions-running-1.png and if you add a query string parameter, the parameter is parsed into the response: local-cloud-functions-running-name.png

This also works if you make a POST request:

curl --location --request POST 'localhost:8080' \
--header 'Content-Type: application/json' \
--data-raw '{"name":"James"}'

local-cloud-functions-running-post.png

And that’s it, you’ve successfully tested your cloud function on your local machine. Let me know what you think, if there’s any areas that are unclear or need more explaining or any questions you might have in the comments section below.