Learn how to use our new OpenFaaS Pro template with private pip modules, Postgresql and Flask.
In collaboration with customers, we have created a new OpenFaaS Pro template for Python, suited for commercial uses. The template is based upon our prior Open Source work, but comes with some extra features that make it easier and more convenient to run Python in a production environment. This saves you from having to fork and maintain your own version of our template, whilst still being able to use the latest features and improvements.
The OpenFaaS CLI supports mounting secrets into the build process through the OpenFaaS Pro plugin. This enables you to provide the private key, password or token required to use private Python packages from your functions in a secure way.
The new template supports two ways to install private pip modules:
- Install packages from private PyPI repositories like AWS Codeartifact using pip.conf
- Install packages from a private Git repositories using .netrc
Through build options the template makes it easy to meet build prerequisites required for certain Python packages like Pandas, Numpy, Pillow, Postgres, etc. Adding a build option to your function configuration will install a list of predefined packages into the function image at build time.
As a final feature we have made the Flask app instance available within the function handler. This allows users to make changes to the Flask app in the handler.py file for things like modifying loggers or adding OpenTelemetry. Previously the template had to be forked and modified to make changes to the Flask app instance.
In the next sections we will walk you through some examples to show you how to use the different features of the OpenFaaS Pro python template to build Python functions.
We assume some familiarity with the OpenFaaS CLI and how to build functions. If this is completely new to you take a look at:
Use private Python packages in your function.
To use private Python packages inside a function pip will need to authenticate to git or your private PyPI repository to install the Packages. To do this we need a way to provide these authentication credentials during the function build. The OpenFaaS Pro CLI plugin can mount different secrets into the build process. Any other mechanism should be considered insecure and risks leaking secrets into the final image.
To download and enable the OpenFaaS Pro plugin run:
faas-cli plugin get pro
faas-cli pro enable
Install Python packages from a private git repository.
For this example we are going to create a simple private Python package:
-
In this example the package name will be
private_package
. Create the required files__init__.py
andsetup.py
in a new directory. The directory tree should look like this:private_package/ private_package/ __init__.py setup.py
-
Edit
setup.py
to contain some basic information about the Python package:from setuptools import setup setup( name = "private_package", version = "0.0.1", author = "OpenFaaS", packages=['private_package'], )
-
Add a simple function to
__init__.py
:def greet(): return "Hello from this private python package!!!"
Create a new private repo for this package, then commit and push the code.
A new function can be created using the OpenFaaS Pro Python template. We are going to use the private python package from within this function:
faas-cli template pull https://github.com/openfaas/openfaas-pro
faas-cli new --lang python@3.8-debian \
withprivaterepo
mv withprivaterepo.yml stack.yml
Pip supports installing packages from a Git repository using the URI form:
git+https://gitprovider.com/user/repo.git@{version}
Make sure to add the URI for your private package to the requirements.txt
file of your OpenFaaS function.
The private package can now be used in the function handler:
from private_package import greet
def handle(event, context):
return {
"statusCode": 200,
"body": greet()
}
If you would try to build your function now the build would fail because there are no authentication credentials in place to access the private git repository.
Pip supports loading credentials from a user’s .netrc
file.
See the pip documentation for more information on netrc support.
Create a .netrc
file with credentials to access your repo:
machine github.com
login username
password PAT
Add the .netrc
file as a build secret in the stack.yaml
file of the function:
functions:
withprivaterepo:
lang: python@3.8-debian
handler: ./withprivaterepo
image: withprivaterepo:0.0.1
+ build_secrets:
+ netrc: ${HOME}/.netrc
The function can now be build and deployed using the OpenFaaS Pro CLI:
faas-cli pro publish -f stack.yml
faas-cli deploy
Install packages from a private PyPi repository.
If you are using a private PyPI registry the steps are very similar to the whet we described in the previous section for private git repositories:
- Create a
pip.conf
file with the configuration and credentials for your registry. - Mount the
pip.conf
file as a build secret. - Use
faas-cli pro build
orfaas-cli pro publish
to build the function.
Depending on the registry you are using your pip.conf
file might look something like this. In this example we are using AWS Codeartifact.
[global]
index-url = https://aws:CODEARTIFACT_TOKEN@OWNER-DOMAIN.d.codeartifact.us-east-1.amazonaws.com/pypi/REPOSITORY/simple/
The stack.yaml
file has to be updated to add the pip.conf
file as a build secret.
functions:
withprivate:
lang: python@3.8-debian
handler: ./withprivate
image: withprivate:0.0.1
+ build_secrets:
+ pipconf: ${HOME}/.config/pip/pip.conf
You can now use any private package from your repository in your function by including them in the requirements.txt
file.
Query a postgres database
Some libraries like psycopg2 require additional packages before they can be installed with pip. The OpenFaaS Pro template makes it easy to add these dependencies. It includes a build_option for Postgresql.
In this section we will walk through an example showing how to create a function that queries a Postgres database.
We will not go into detail on how to set up a Postgres database. You can use one of the many DBaaS services available or use arkade to quickly deploy a database in your cluster.
Run arkade install postgresql
to install a Postgresql database. After the installation it will print out all the instructions to get the password and connect to the database.
These are the sql statements we used to create a table and insert some data:
CREATE TABLE IF NOT EXISTS employee
(
id INT PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
email TEXT NOT NULL
);
INSERT INTO employee (id,name,email) VALUES
(1,'Alice','alice@example.com'),
(2,'Bob','bob@example.com')
If you did not download the OpenFaaS Pro template yet you can do so with the faas-cli:
faas-cli template pull https://github.com/openfaas/openfaas-pro
Next create a simple function to query the database.
faas-cli new --lang python@3.8-debian \
query-db
mv query-db.yml stack.yml
Update the handler.py
file for the function and don’t forget to add psycopg2
to the requirements.txt
file.
import os
import json
import psycopg2
from psycopg2.extras import RealDictCursor
dbConn = None
def initConnection():
global dbConn
with open('/var/openfaas/secrets/postgres-password', 'r') as s:
password = s.read()
database = os.getenv('DB_NAME')
user = os.getenv('DB_USER')
host = os.getenv('DB_HOST')
port = os.getenv('DB_PORT')
dbConn = psycopg2.connect(database=database,
user=user,
password=password,
host=host,
port=port)
def handle(event, context):
if dbConn == None:
initConnection()
cur = dbConn.cursor(cursor_factory=RealDictCursor)
cur.execute("""
SELECT
id, name, email
FROM employee
""")
data = json.dumps(cur.fetchall(), indent=2)
return {
"headers": {
"Content-type": "application/json"
},
"statusCode": 200,
"body": data
}
Note that we defined a function initConnection
to initialize the database connection. This way the connection can be reused for multiple function invocation.
The database connection parameters are read from environment variables and the password from an OpenFaaS secret.
The OpenFaaS philosophy is that environment variables should be used for non-confidential configuration values only, and not used to inject secrets. The secret can be created using the faas-cli
.
Update the stack.yml
file and add the environment variables and secret for the database connection. Also don’t forget to include the libpq
build option:
functions:
query-db:
lang: python@3.8-debian
handler: ./query-db
image: query-db:0.0.1
+ build_options:
+ - libpq
+ environment:
+ DB_NAME: postgres
+ DB_USER: postgres
+ DB_HOST: postgresql.default.svc.cluster.local
+ DB_PORT: 5432
+ secrets:
+ - postgres-password
Build and deploy the function with the faas-cli
. Invoking the function should return a json response that looks like this:
$ curl -i http://127.0.0.1:8080/function/query-db
HTTP/1.1 200 OK
Content-Length: 163
Content-Type: application/json
Date: Thu, 01 Dec 2022 11:48:40 GMT
Server: waitress
X-Call-Id: 49e750b6-d813-4139-9d4e-96adcf79d596
X-Duration-Seconds: 0.008393
X-Start-Time: 1669895320281999527
[
{
"id": 1,
"name": Slice",
"email": "alice@example.com"
},
{
"id": 2,
"name": "Bob",
"email": "bob@example.com"
}
]
Customise the flask app
Flask’s app variable is available from within the handler. This saves you from having to fork the template and make changes when you need more control over the app instance.
The function handler, handler.py
, that is generated when you create a new function using the OpenFaaS Pro python template includes this import statement:
from flask import current_app
app = current_app.app_context().app
Changes to the app instance, like changing the log format or adding OpenTelemetry, can simply be made in the handler.py file.
Use a custom log formatter
We are going to change the formatter for Flask’s default log handler to inject the X-Call-Id
header and some other request information in each log statement. Having the call-id in the log message can help with debugging errors as it allows you to trace back log messages to a specific request.
To log messages with a custom format we have to:
- Sub-class logging.Formatter to inject our own fields that can be used in messages.
- Change the formatter for Flask’s default log handler.
You can add the required code to the handler.py
file for your OpenFaaS function before the handle
function definition:
import logging
from flask import has_request_context, request
from flask.logging import default_handler
class RequestFormatter(logging.Formatter):
def format(self, record):
if has_request_context():
record.call_id = request.headers.get("X-Call-Id", None)
record.path = request.path
record.method = request.method
else:
record.call_id = None
record.path = None
record.method = None
return super().format(record)
formatter = RequestFormatter(
'[%(asctime)s] %(method)s %(path)s - Call-Id: %(call_id)s\n'
'%(levelname)s:%(message)s'
)
default_handler.setFormatter(formatter)
app.logger.setLevel(logging.INFO)
See Flask’s documentation to learn more about logging in Flask.
To validate that messages are logged using the custom formatter we can log some messages in our function handler:
def handle(event, context):
app.logger.info('Running function handler')
return {
"statusCode": 200,
"body": "Hello from this OpenFaaS Pro template!"
}
Use the faas-cli
to build and deploy your function. The faas-cli logs
command can be used to fetch the logs.
Invoking the function should output logs that look something like this:
2022-11-30T09:32:04Z 2022/11/30 09:32:04 stderr: [2022-11-30 09:32:04,473] GET / - Call-Id: d49de5df-7085-466e-b871-86c018212352
2022-11-30T09:32:04Z 2022/11/30 09:32:04 stderr: INFO:Running function handler
2022-11-30T09:32:04Z 2022/11/30 09:32:04 GET / - 200 OK - ContentLength: 38B (0.0016s)
By default the OpenFaaS watchdog prefixes every log line read from the function process with “Date Time” + “stderr/stdout”. In some cases this might clutter your logs. The prefixes can be disabled by setting the environment variable prefix_logs
to false on the function.
functions:
logging:
lang: python@3.8-debian
handler: ./logging
image: welteki2/flask-logging:latest
+ environment:
+ prefix_logs: false
Conclusion
We took a look at the new features of the OpenFaaS Pro Python template and showed you how to use them by means of some examples.
- We provide a safe and easy way to use private Python packages within your functions - whether that’s using a private PyPi server or a private Git repository.
- The new
libpq
build option in the template makes it easy to use Python drivers for Postgresql. - We made the Flask app instance available within the function handler allows you to easily change it without needing to fork the template.
If there are native packages you need, or combinations of apt packages you often use, please let us know so we can improve the template and add additional build options.
Some of our other content that uses Python functions: