Fn Development#
Fn is an open-source framework for creating a Functions-as-a-Service (FaaS) compute platform. In real terms this means we make a HTTP POST call to our Fn server, ask it to call a function which then returns a result.
Getting setup#
To run through this tutorial you must have both Docker and Fn.
For Docker installation instructions please visit the Docker installation instruction.
To install Fn follow the instructions for your operating system given on their GitHub repo.
Starting Fn#
As we’ll be developing functions locally we’ll want to start up an instance of Fn
to allow us to run (invoke) the functions
we build. During development setting the log level of Fn
to DEBUG
is highly recommended. This pipes errors that may occur
within the containers to the terminal. Without setting the log level the only error reporting shown is a less helpful
Error invoking function. status: 502 message: function failed
message.
So, open a terminal and run (and leave running)
fn start --log-level DEBUG
If you don’t want extra debug information from each invocation just run
fn start
First function#
To call functions within OpenGHG we use a routing function with Fn. This function (link to route.py) routes calls made to our Fn triggers (more on triggers later). To setup a function we first need to create an app. To do this we use
fn init --runtime python openghg_fn
This will create a folder called openghg_fn
with some boilerplate code inside. These can form a template
for creation of your own functions. If we look inside this directory we’ll see three files
$ cd openghg_fn/
$ ls
func.py func.yaml requirements.txt
Here func.py
is the function we’re going to call, func.yaml
tells Fn how to setup and run the function and requirements.txt
contains the Python requirements and is used when a Docker image is built for the function.
Function code#
First we’ll modify func.py
to contain a simple response to being called. Paste the following into func.py
:
import io
import json
import logging
from typing import Dict, Optional, Union
from fdk.response import Response
def route(ctx, data: Optional[Union[io.BytesIO, Dict]] = None) -> Response:
message = {"message": "Hello from OpenGHG"}
return_str = json.dumps(message)
headers = {"Content-Type": "application/json"}
return Response(ctx, response_data=return_str, headers=headers)
Function parameters#
Next we’ll modify func.yaml
to contain
schema_version: 20180708
name: openghg_fn
version: 0.0.1
runtime: python
entrypoint: /python/bin/fdk /function/func.py route
# Memory limit for function in MB
memory: 256
Here note the change of handler
to route
for the entrypoint.
Seeing as we’re just using a bare Fn function here we can leave requirements.txt
as it is with our only requirement being fdk
.
As each function needs to be part of an app we create an app called openghg
.
fn create app openghg
We’re now ready to deploy our function and call it.
Deployment#
To deploy the app we can run
fn --verbose deploy --local
This tells Fn give us verbose output and --local
tells it not to push our Docker image to DockerHub. After you run this command
you’ll see a lot of output as Fn builds a Docker. Hopefully at the end of the output you’ll have something similar to
Successfully built 22ed08c1f99e
Successfully tagged openghg_fn:0.0.2
Updating function openghg_fn using image openghg_fn:0.0.2...
Successfully created function: openghg_fn with openghg_fn:0.0.2
Don’t worry if some values are slightly different here.
Call#
You should then be able to invoke / call the function using
[user@computer openghg_fn]$ fn invoke openghg openghg_fn
{"message": "Hello from OpenGHG"}
We can also use curl
to trigger the function. If we do
[user@computer openghg_fn]$ fn inspect function openghg openghg_fn
{
"annotations": {
"fnproject.io/fn/invokeEndpoint": "http://localhost:8080/invoke/01ES9D6TA5NG8G00GZJ0000009"
},
"app_id": "01ES9D6T23NG8G00GZJ0000008",
"created_at": "2020-12-11T17:22:35.461Z",
"id": "01ES9D6TA5NG8G00GZJ0000009",
"idle_timeout": 30,
"image": "openghg:0.0.82",
"memory": 2048,
"name": "openghg",
"timeout": 30,
"updated_at": "2021-06-08T13:26:48.066Z"
}
We can see that there is an invocation endpoint at http://localhost:8080/invoke/01ES9D6TA5NG8G00GZJ0000009
, using curl we can
call the function like so
[user@computer openghg_fn]$ curl -X POST http://localhost:8080/invoke/01ES9D6TA5NG8G00GZJ0000009
{"message": "Hello from OpenGHG"}
Note that your invocation endpoint may differ slightly from the one shown above.
Dockerise#
As our functions will be more complex than the example given above we need to create our own custom Docker image.
To create our own Docker image for the function we’ve created above create a Dockerfile
in the openghg_fn
folder
that contains the following:
FROM fnproject/python:3.8.5
ADD requirements.txt func.py function/
WORKDIR /function
RUN pip3 install pip==20.2.4 wheel setuptools
RUN pip3 install --target /python/ -r requirements.txt
RUN rm -rf requirements.txt
ENV PYTHONPATH=/python
ENTRYPOINT ["/python/bin/fdk", "/function/func.py", "route"]
Here we’ve installed a specific pip
version 20.2.4 as this was the last version before the new resolver was introduced.
After creating our Dockerfile
we must also update func.yaml
to create tell Fn that we’re now using our own customer Docker container
schema_version: 20180708
name: openghg_fn
version: 0.0.4
runtime: docker
triggers:
- name: route
type: http
source: /openghg_fn
We can then tell Fn to deploy the image again. This will build the container using our custom Dockerfile.
fn --verbose deploy --local
Hopefully at the end of the build you’ll see something like:
Updating function openghg_fn using image openghg_fn:0.0.4...
Successfully created trigger: route
Trigger Endpoint: http://localhost:8080/t/openghg/openghg_fn
We now have a much cleaner endpoint we can use to trigger the function. Using curl
again to trigger the function. Note that
trigger/invoke/call are all used interchangeably here.
[user@computer openghg_fn]$ curl -X POST http://localhost:8080/t/openghg/openghg_fn
{"message": "Hello from OpenGHG"}
Now we have an understanding of how Fn works and how to create functions and call them we will cover the functions available in OpenGHG.
OpenGHG functions#
With OpenGHG we use a single routing function to route calls to a number of separate functions. This routing function can be found
in services/route.py
. To use this routing function we first need to setup a Docker container within which we can perform the
computation and return the data to the caller. As OpenGHG requires a number of packages we use a two step build process.
First we create a base image, called openghg-base
which contains all the requirements for OpenGHG. We then use this base image
to create a second image, called openghg-complete
, into which we copy our OpenGHG library code and services/function code.
Base image#
First we’ll look at the base image which can be found in docker/base_image
. This folder contains a Python script
that makes building the image easier and a Dockerfile.
FROM fnproject/python:3.8.5-dev as build-stage
ADD requirements-server.txt ./
RUN apt-get update && apt-get install git -y
# pip 20.2.4 is the version before the new resolver was introduced
RUN pip3 install pip==20.2.4 wheel setuptools && \
pip3 install --target /python/ -r requirements-server.txt
FROM fnproject/python:3.8.5
COPY --from=build-stage /python/ /python/
ENV PYTHONPATH=/python
ENTRYPOINT ["bash"]
This Dockerfile
is very similar to the one shown above. Some differences are that we copy an extra requirements file into the image
and install git
to allow pip
to install Acquire from GitHub. Another difference is that we use two build stages. The first using
the fnproject/python:3.8.5-dev as build-stage
. After cloning Acquire and installing all the packages into /python
we start
with a fresh image and copy only the contents of /python
into this image. This helps limit the size of the image.
To build this image run
python build.py
This will build a Docker image with the tag openghg/openghg-base:latest
. To see the available options when building the image
run the command above with -h
.
Complete image#
Now we’ve built the base image we can build the complete image containing the OpenGHG library and services code. The files to build this image
can be found in docker/
. It contains func.yaml
which tells Fn how to run our function. The Dockerfile
(shown below) uses
the openghg-base
image we build in the previous step, adding route.py
to the /function
folder and then copying the OpenGHG
code into /python
. We also copy the services code which form the functions that calls are routed to by route.py
.
FROM openghg/openghg-base:latest
ADD openghg_services/route.py /function/
WORKDIR /function
# Copy in openghg
ADD openghg /python/openghg
RUN python3 -m compileall /python/openghg/*
# Copy in the service files
RUN mkdir /python/openghg_services
ADD openghg_services /python/openghg_services
RUN python3 -m compileall /python/openghg_services/*.py
ENV PYTHONPATH=/python
ENV OPENGHG_CLOUD=1
# Become the $FN_USER so that nothing runs as root
USER $FN_USER
ENTRYPOINT ["/python/bin/fdk", "/function/route.py", "handle_invocation"]
We have also modified func.yaml
to increase the amount of memory available to this function to 2048 MB / 2 GB. If you notice functions failing unexpectedly
it may be worth trying changing this value.
schema_version: 20180708
name: openghg
version: 0.0.100
runtime: docker
memory: 2048
triggers:
- name: route
type: http
source: /
To build this image we use the build_deploy.py
Python script.
[user@computer docker]$ python build_deploy.py -h
usage: build_deploy.py [-h] [--tag TAG] [--push] [--build] [--deploy] [--build-base]
Build the base Docker image and optionally push to DockerHub
optional arguments:
-h, --help show this help message and exit
--tag TAG tag name/number, examples: 1.0 or latest. Not full tag name such as openghg/openghg-complete:latest. Default: latest
--push push the image to DockerHub
--build build the docker image. Disables Fn deploy.
--deploy buid image and deploy the Fn functions
--build-base build the base docker image before building the complete image
This script takes care of building the base image as well if you want it to. To build both the base and complete image and deploy the functions to Fn run
python build.py --build-base
If you have the base image built and have only made changes to the OpenGHG code you can just run
python build.py
Note - if you’ve made changes to either requirements.txt
or requirements-server.txt
you’ll need to do a rebuild of the base
image. This ensures all dependencies are installed in the base image.
Test a function#
Say we want to test a function such as the testconnection
function that is a part of OpenGHG. This simple function returns a simple
string of the timestamp at which the function was called to the user.
As above we inspect the funtion and find its endpoint.
[user@computer openghg_fn]$ fn inspect function openghg openghg
{
"annotations": {
"fnproject.io/fn/invokeEndpoint": "http://localhost:8080/invoke/01ES9D6TA5NG8G00GZJ0000009"
},
"app_id": "01ES9D6T23NG8G00GZJ0000008",
"created_at": "2020-12-11T17:22:35.461Z",
"id": "01ES9D6TA5NG8G00GZJ0000009",
"idle_timeout": 30,
"image": "openghg:0.0.82",
"memory": 2048,
"name": "openghg",
"timeout": 30,
"updated_at": "2021-06-08T13:26:48.066Z"
}
We can then call the function using
curl -X POST -d '{"function" : "testconnection", "args": {}}' http://localhost:8080/invoke/01ES9D6TA5NG8G00GZJ0000009
Note
Your endpoint URL will differ from the one above.
And we should recieve a response such as
{'results': 'Function run at 2021-06-08 13:34:17.095579+00:00'}
Now we know our Dockerised function can be called and works correctly.
Calling from OpenGHG#
For information on how we’ve setup calling functions from OpenGHG please see the Fn usage section of the documentation.