By guillaume blaquiere.Jun 8, 2021
Function as a Service, or FaaS, has been a cornerstone in app development. Popularized by AWS Lambda service, all the major Cloud Providers offer their version, with different features. And they also extend this principle to containers, with Cloud Run on Google Cloud for example.
On Google Cloud, Cloud Functions is the FaaS service and, to use it, you have to enforce a predefined code structure. In Python, to handle HTTP requests, the function signature is the following
def my_function(request):
...
return "response", 200 #http code
FaaS common issues
When you deploy a function, it’s “only one function”, to achieve only one task. This opinionated design allows a better scalability and separation of concern.
The result of this choice can be seen in the available URL to call the function
https://<region>-<projectID>.cloudfunctions.net/<functionName>
Only one path is suitable. No subpath configuration, no path routing definition.
However, sometimes, you would like to do more, even if it’s not a good pattern, you need (or want) to use REST API definition to access to resources, by using path parameters.
For example: /<functionName>/user/<userId>
Flask routing and Cloud Functions limitation
Flask framework proposes an idiomatic way to achieve this. You define your Flask App, your route, and that’s all (you can find that in any tutorial/getting started)
from flask import Flask, request
app = Flask("internal")
@app.route('/user/<string:id>', methods=['GET', 'POST'])
def users(id):
<do something>
print(id)
return id, 200
Sadly, with Cloud Functions, and even if Cloud Functions is based on Flask request definition, you can’t because you don’t manage the Flask root app, only the Cloud Functions endpoint, that’s all!!
To provide a better understanding, the implementation is similar to that
############### MANAGED BY GOOGLE ####################from flask import Flask, request
app = Flask("google_managed")
@app.route('/my_function', methods=['GET', 'POST'])
def common_cloud_functions_function():
return my_function(request)############### MANAGED AND PROVIDED BY YOU ####################
def my_function(request):
...
return "response", 200 #http code
Manual Flask invocation
Flask is a framework that performs 2 main tasks
- Perform routing based on URL map
- Listen and serve a webserver
The principle is to reuse only the routing part, without the listen and serve; this part is performed by the Cloud Functions runtime.
The main interest of this solution is the capacity to reuse the Flask idiomatic path rule definition, and to keep an environment familiar to the developers.
So, when a request comes in, we have to:
- Create a new context
- Copy the request’s main values (data, headers, path,…)
- Dispatch and process the request
So, let’s code that !
from flask import Flask, request#Define an internal Flask app
app = Flask("internal")#Define the internal path, idiomatic Flask definition
@app.route('/user/<string:id>', methods=['GET', 'POST'])
def users(id):
print(id)
return id, 200#Comply with Cloud Functions code structure for entry point
def my_function(request):
#Create a new app context for the internal app
internal_ctx = app.test_request_context(path=request.full_path,
method=request.method)
#Copy main request data from original request
#According to your context, parts can be missing. Adapt here!
internal_ctx.request.data = request.data
internal_ctx.request.headers = request.headers
#Activate the context
internal_ctx.push()
#Dispatch the request to the internal app and get the result
return_value = app.full_dispatch_request()
#Offload the context
internal_ctx.pop()
#Return the result of the internal app routing and processing
return return_value
The Cloud functions deploys as usual and offer the same endpoint. However, this time, you can request your new paths. Try this request
curl https://<region>-<projectID>.cloudfunctions.net/<functionName>/user/123# The return is 123
Unlock Cloud Functions limitation….. or not!!
This solution allows you to unlock the routing capacity of Cloud Functions and to do more than a single purpose endpoint. You can gain in consistency and efficiency (less functions to deploy)
However, keep in mind that’s a workaround, even a hack, and Cloud Functions aren’t designed for this purpose.
If you want to create an API backend, to handle concurrent requests on the same instance, to have a developer friendly environment (for test, packaging, portability,…), Cloud Run is a much more suitable product for that!
Disclaimer: I’m not a Python developer and some code parts could be improved to be more idiomatic. Don’t hesitate to comment to improve these code samples!
The original article published on Medium.