This tutorial walks through how to use a custom Docker image to define an Fn function. Although Fn functions are packaged as Docker images, when developing functions using the fn CLI developers are not directly exposed to the underlying Docker platform. Docker isn't hidden (you can see Docker build output and image names and tags in routes), but you aren't required to be very Docker-savvy to develop functions with Fn. However, sometimes you need to handle advanced use cases and must take complete control of the creation of the function image. Fortunately the design and implementation of Fn enables you to do exactly that. Let's build a simple custom function image to walk through the process.
As you make your way through this tutorial, look out for this icon.
Whenever you see it, it's time for you to
perform an action.
This tutorial requires you to have both Docker and Fn installed. If you need help with Fn installation you can find instructions in the Introduction to Fn tutorial.
Before we can get starting there are a couple of configuration steps to take care of.
To make it possible to push images you need to authenticate yourself with your Docker repository (default is Docker Hub).
docker login
Next, if it isn't already running, you'll need to start the Fn server. We'll run it in the foreground to let us see the server log messages so let's open a new terminal for this.
-
Define the FN_REGISTRY environment variable to point the Fn server to where it should pull function images from. If using the default Docker Hub registry you just need to specify your docker user id:
export FN_REGISTRY=<yourdockerid>
-
Start the Fn server using the
fn
cli:fn start
In this tutorial we only have two artifacts: a Dockerfile and a very simple Node.js "Hello World" application that returns a customized greeting given a name.
The func.js file is nothing special and simply reads from standard input
and writes to standard output. This is the standard Fn supported mechanism
for functions to receive input and return output.
'Hot Functions'
(not discussed in this tutorial) are slightly different.
Copy/paste the following into a file named
func.js
:
name = "World";
fs = require('fs');
try {
input = fs.readFileSync('/dev/stdin').toString();
if (input) {
name = input;
}
} catch(e) {}
console.log("Hello", name, "from Node!");
The Dockerfile
for our function is also very simple. It starts with
a light alpine Node.js base image, copies the func.js
into the image,
and sets the entrypoint so that when the container is started the
func.js
is run.
NOTE: func.js
has no required Node modules but if there were
you would have to run npm install
to download them to the
/function/node_modules
folder in the generated image.
In the same folder as the
func.js
file, copy/paste
the following into a file named Dockerfile
:
FROM node:8-alpine
WORKDIR /function
ADD func.js /function/func.js
ENTRYPOINT ["node", "./func.js"]
In your working directory, build and run the image as you would any Docker image:
-
Build your function container image with
docker build
:docker build . -t <yourdockerid>/node-hello:0.0.1
-
Test the image by running it with no input:
docker run --rm <yourdockerid>/node-hello:0.0.1
The output should be:
Hello World from Node!
-
Test the image by running it with a name parameter:
echo -n "Jane" | docker run -i --rm <yourdockerid>/node-hello:0.0.1
The output should be the same as be except "Jane" in place of "World":
Hello Jane from Node!
Great! We have a working Docker image. Now let's deploy it as a function.
When developing locally you don't need to deploy to Docker Hub--the
local Fn server can find your function image on the local machine. But
eventually you are going to want to run your function on a remote
Fn server which requires you to publish your function image in
a repository like Docker Hub. You can do this with a standard docker push
but again this step is optional when we're working locally.
docker push <yourdockerid>/node-hello:0.0.1
Once we have a function container image we can associate that image with a 'route'.
-
First we need an 'application' to contain our functions. Applications define a namespace to organize functions and can contain configuration values that are shared across all functions in that application:
fn apps create demoapp
Successfully created app: demoapp
-
We then create a route that uses our manually built container image:
fn routes create demoapp /hello -i <yourdockerid>/node-hello:0.0.1
/hello created with <yourdockerid>/node-hello:0.0.1
-
We can confirm the route is correctly defined by getting a list of the routes defined for an application:
fn routes list demoapp
You should see something like:
path image endpoint /hello <yourdockerid>/node-hello:0.0.1 localhost:8080/r/demoapp/hello
At this point all the Fn server has is configuration metadata. It has the name of an application and a function route that is part of that application that points to a named and tagged Docker image. It's not until that route is invoked that this metadata is used.
Calling a function that was created through a manually defined route is no
different from calling a function defined using fn deploy
--which is exactly
as intended!
-
Call the function using
fn call
:echo -n "Jane" | fn call demoapp /hello
This will produce the expected output:
Hello Jane from Node!
-
Call the function with curl using it's http endpoint. You can find out the endpoints for each of your routes using the
fn routes list
command we used above.curl -d "Jane" http://localhost:8080/r/demoapp/hello
This will produce exactly the same output as when using
fn call
, as expected.Hello Jane from Node!
When the function is invoked, regardless of the mechanism, the Fn server looks up the function image name and tag associated with the route and has Docker run a container. If the required image is not already available locally then Docker will attempt to pull the image from the Docker registry.
In our local development scenario, the image is already on the local machine so you won't see a 'pull' message in the Fn server log.
Having completed this tutorial you've successfully built a Docker image, defined a function as implemented by that image, and invoked the function resulting in the creation of a container using that image. Congratulations!
Go: Back to Contents