Portable Cloud Functions with the Python Functions Framework

Dustin Ingram - Jan 9 '20 - - Dev Community

In this post, I'll show how the open-source Functions Framework for Python makes Cloud Functions portable across multiple products and ecosystems. I’ll specifically show how to migrate a Python function from Cloud Functions to a service like Cloud Run.

I'll also show you how the Functions Framework gives you the ability to test your functions locally as well.

What are the Functions Frameworks?

The Function Frameworks are open source libraries for writing portable functions -- brought to you by the Google Cloud Functions team.

The Functions Framework lets you write lightweight functions that run in many different environments, including:

  • Google Cloud Functions
  • Cloud Run and Cloud Run on GKE
  • Knative-based environments
  • Your local development machine
  • and elsewhere.

You can think of the Functions Framework for a given language as a wrapper around a single function in that language, which handles everything necessary for your function to receive HTTP requests or Cloud Events such as Pub/Sub.

The Functions Framework for Python joins previously open-sourced frameworks for several other popular languages.

Going beyond Cloud Functions

Cloud Functions is a powerful and simple tool for quickly deploying a standalone function that lets you integrate various APIs, products, services, or just respond to events.

However, that simplicity also comes at a cost: the Cloud Functions Runtime makes some opinionated choices about the environment in which your function runs, such as the runtime’s base image, the patch version of Python, and the underlying system libraries that are installed.

For most developers, this is totally fine, as the choices that have been made for the runtime are appropriate for almost all use cases.

However, occasionally you will want to make a small change, such as install a special platform-level package that you need for a particular task.

Or, you'll want to continue using your function as-is, but move it to run on Cloud Run to lower costs.

An example: Portability without the Functions Framework

Let's say we have this minimal Cloud Function:

def hello(request):
    return "Hello world!"
Enter fullscreen mode Exit fullscreen mode

This function doesn't do much: it takes an HTTP request, and returns "Hello world!" as a response.

However, if we wanted to migrate it from Cloud Functions to a service like Cloud Run, which hosts applications, not functions, we'd have to do a lot:

  • choose a web framework
  • configure the web application
  • refactor this function as a view in our web app
  • add the view as a route

By this point, our "new" function wouldn't look much at all like your original function. If you had chosen Flask as your web framework, it might look something like this:

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return 'Hello world!'
Enter fullscreen mode Exit fullscreen mode

That's a lot of extra boilerplate that you don't need.

An example: Portability with the Functions Framework

With the Functions Framework, none of this refactoring is necessary. You can deploy a function to Cloud Run without changing a single line of code in your main.py file.

Instead, you can define a Dockerfile as follows:

# Use the official Python image.
# https://hub.docker.com/_/python
FROM python:3.7-slim

# Copy local code to the container image.
ENV APP_HOME /app
WORKDIR $APP_HOME
COPY . .

# Install production dependencies.
RUN pip install functions-framework
RUN pip install -r requirements.txt

# Run the web service on container startup.
CMD exec functions-framework --target=hello
Enter fullscreen mode Exit fullscreen mode

This Dockerfile largely borrows from the Dockerfile in the Python Cloud Run quickstart, but with two key differences:

  • it installs the functions-framework dependency
  • it invokes the Functions Framework in the CMD step.

The last steps are building and deploying the container as you would with any Cloud Run application.

See this entire example on Github here.

A local development experience

Another useful feature of the Functions Framework is the ability to run your Cloud Function locally.

Using the example above, you can run your function on your host machine direction using the Function Framework's handy command-line utility:

$ functions-framework --target hello
Enter fullscreen mode Exit fullscreen mode

The downside to this is that any dependencies need to be installed globally and that there may be conflicts between what your function requires and what your host machine has available.

Ideally, we would isolate our function by building the container image we defined in our Dockerfile and running it locally:

$ docker build -t helloworld . && docker run --rm -p 8080:8080 -e PORT=8080 helloworld
Enter fullscreen mode Exit fullscreen mode

Both of these will start a local development server at http://localhost:8080 that you can send HTTP traffic to.

Bonus: Static Typing

One additional benefit to the Functions Framework is that it makes all of the the necessary types available to statically type your functions.

For example, an HTTP function could be statically typed before:

from typing import Tuple, Union
from flask.wrappers import Request

def my_function(request: Request) -> str:
    ...
Enter fullscreen mode Exit fullscreen mode

But now the Context type is available for background functions:

from typing import Dict
from google.cloud.functions.context import Context

def my_function(data: Dict, context: Context) -> None:
    ...
Enter fullscreen mode Exit fullscreen mode

Next steps


. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .