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!"
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!'
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
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
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
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:
...
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:
...
Next steps
- Found this project useful? Give it a star it on Github
- Found a bug in the Python Functions Framework? File an issue
- Want similar updates? Follow me on Twitter