Welcome!
Serverless Functions with Project Fn
This scenario introduces serverless Functions with Project Fn. It was originally prepared for the Meetup Workshop Cloud Native application development on Oracle Cloud Infrastructure in January 2020, hosted by AMIS|Conclusion in Nieuwegein in collaboration with REAL (the Red Expert Alliance) and Link from Portugal. It was updated for the REAL OCI Handson Webinar Series that started in June 2020
The scenario uses an Ubuntu 20.04 environment with embedded browser based VS Code, Docker, Fn CLI and Fn Server running locally. It does not require an OCI Cloud instance.
You will create Functions locally and deploy them to the local Fn Server. Functions can then be invoked through the Fn CLI or through regular HTTP clients such as CURL. You will get some guidance on debugging and logging.
In step 3. you will look at testing the function - both locally, before deployment (using Jest) as well as remotely after deployment - for functionality (with Newman) and performance (using Apache Bench).
In step 4, you will look at passing (environment specific) context variables to the function's runtime deployment
The scenario works with Node (JS) and Java as runtime languages. You can experiment with Go, Ruby, Python as runtimes just as easily.
The scenario offers two bonus steps: one let's you create a Java function implemented with a GraalVM powered natively executable image (small size and faster startup ) and the other one demonstrates how any custom Docker Container image can provide the implementation of an Fn function.
Congratulations!
You've completed the scenario!
Scenario Rating
Summary
This completes your explorations with Functions on a local Fn Server. The next scenario you may want to explore looks at deploying Function to Oracle Cloud Infrastructure.
Background Resources
Tutorial Getting Started with Fn environment https://github.com/fnproject/tutorials/blob/master/install/README.md
Tutorial FN with Node https://github.com/fnproject/tutorials/blob/master/node/intro/README.md
Docs on Fn on OCI https://docs.cloud.oracle.com/iaas/Content/Functions/Tasks/functionscreatefncontext.htm
Tutorials and other background resource for Project Fn: https://github.com/fnproject/tutorials/blob/master/README.md
Your environment is currently being packaged as a Docker container and the download will begin shortly. To run the image locally, once Docker has been installed, use the commands
cat scrapbook_redexpertalliance_oci-course/introduction-fn_container.tar | docker load
docker run -it /redexpertalliance_oci-course/introduction-fn:
Oops!! Sorry, it looks like this scenario doesn't currently support downloads. We'll fix that shortly.

Steps
Serverless Functions with Project Fn
Step 1 - Install Fn and OCI environment
In this step, we will together prepare the environment for working with Fn. The Katacoda scenario environment runs Ubuntu and contains Docker. We need to add Fn CLI and Fn Server.
Wait for the Fn Server to be running
In the background we are currently preparing your Fn environment. Several Docker images are pulled and the Fn server is started. This takes up to three minutes. You can check if Fn Server is running by checking the currently running Docker containers using the following command:
docker ps
Do not continue until you see a Docker container running based on image fnproject/fnserver:latest
Get going with Fn
Check the installed version - client and server - of Fn
fn version
List the currently available Fn contexts
fn list contexts
Notice we have a default context which deploys to a local Fn server. The default context is created the first time you run the Fn CLI. However, we need to select default as our current context and set a registry value for remote or local Docker use.
The currently active context is default - this is a local context that uses the locally running Fn server for deploying functions.
Before we start using Fn, we need to configure Fn to point to an appropriate Docker registry so it knows where to push your function images to. Normally Fn points to your Docker Hub account by specifying your Docker Hub username. However, for pure local development we can simply configure Fn with an arbitrary value
So now update the registry setting for the default context to something meaningless.
fn update context registry something-meaningless
You can list the currently available Fn contexts again and see whether your changes had an effect (of course they did)
fn list contexts
Step 2 - Create your first Function (in Node)
Creating a Function with Fn
In this step we will create a simple function with Fn. We pick Node (JS) as our runtime - Go, Python, Java and Ruby are other out of the box options.
fn init --runtime node hello
cd hello
Three files have been created in the new directory hello.
ls
The fn init command generated a func.yaml function configuration file; this file provides instructions to the Fn Server to build, deploy and invoke the function. Let's look at the contents:
cat func.yaml
The generated func.yaml file contains metadata about your function and declares a number of properties including:
- schema_version--identifies the version of the schema for this function file. Essentially, it determines which fields are present in func.yaml.
- name--the name of the function. Matches the directory name.
- version--automatically starting at 0.0.1.
- runtime--the name of the runtime/language which was set based on the value set in --runtime.
- entrypoint--the name of the Docker execution command to invoke when your function is called, in this case node func.js.
- memory - maximum memory threshold for this function. If this function exceeds this limit during execution, it is stopped and error message is logged.
- timeout - maximum runtime allowed for this function in seconds. The maximum value is 300 and the default values is 30.
There are other user specifiable properties that can be defined in the yaml file for a function. We do not need those for this simple example. See Func.yaml metadata options for a complete overview of the options for the func.yaml file
The package.json file is present in (most) Node applications: it specifies all the NPM dependencies for your Node function - on third party libraries and also on the Fn FDK for Node (@fnproject/fdk).
cat package.json
You could open func.js in the text editor to see the generated functionality of the function: that is where the real action takes place when the function is invoked.
Deploy and Invoke the Function
Create an Fn application - a container for multiple related functions.
fn create app hello-app
An application acts as a namespace for functions. Some management actions are performed on applications. The number of applications allowed in an OCI tenancy is limited to 10; this number can (potentially) be increased.
Deploy the Function Hello locally, into the app that was just created
fn -v deploy --app hello-app --local
When you deploy a function like this, Fn is dynamically generating a Dockerfile for your function, building a container, and then loading that container for execution when the function is invoked.
Note: Fn is actually using two images. The first contains the necessary build tools and produces the runtime artefact. The second image packages all dependencies and any necessary language runtime components. Using this strategy, the final function image size can be kept as small as possible.
When using fn deploy --local
, fn server builds and packages your function into a container image which resides on your local machine. You can now verify that a Docker Container Image has been built for Fn Function Hello:
docker images | grep hello
Using the following command, you can check the Fn applications (or function clusters) in your current context:
fn list apps
With the next command, you can check which functions have been deployed into a specific application:
fn list functions hello-app
Time now to invoke the function. The command for invoking the function is simply: fn invoke <app-name> <function-name>
:
fn invoke hello-app hello
To send in a JSON object as input to the function, use the following command:
echo -n '{"name":"Your Own Name"}' | fn invoke hello-app hello --content-type application/json
Again, a friendly, this time personalized, welcome message should be your reward.
What is happening here: when you invoke "hello-app hello" the Fn server looked up the "hello-app" application and then looked for the Docker container image bound to the "hello" function, started the container (if it was not already running) and send the request to the handler listening inside the container.
Inspect some under-the-hood details
To easily get some understanding of what is happening when you invoke the function, you can use debug mode in Fn CLI. Invoke the function with the command prepended with DEBUG=1, to get additional debug details on the HTTP requests sent from the Fn CLI to the Fn runtime and received back:
echo -n '{"name":"Your Own Name"}' | DEBUG=1 fn invoke hello-app hello --content-type application/json
If you need more clarity on what is happening during the build process that creates the container image, you can use the verbose flag- which you need to put immediately after fn:
fn --verbose build
Capturing Logging
When calling a deployed function, Fn captures all standard error output and sends it to a syslog server, if configured. So if you have a function throwing an exception and the stack trace is being written to standard error it’s straightforward to get that stack trace via syslog.
We need to capture the logs for the function so that we can see what happens when it fails. To capture logs you need to configure the tutorial application with the URL of a syslog server. You can do this either when you create an app or after it’s been created.
When creating a new app you can specify the URL using the --syslog-url option. For existing applications, you can use fn update app to set the syslog-url setting.
We will quickly grab logging output to a local syslog server.
Open a second terminal window - for running the syslog server:
Execute the following commands to install the npm module Simple Syslog Server, copy a simple node application to run a syslog server based on the npm module and run that application:
mkdir /root/syslog
cd /root/syslog
npm install simple-syslog-server
cp /root/scenarioResources/syslog.js .
node syslog.js
The syslog server is now running and listening on port 20514 on the TCP protocol.
Switch to the original terminal window. Execute this command to configure application hello-app with the now active syslog server:
fn update app hello-app --syslog-url tcp://localhost:20514
Time now to invoke the function again and see output being sent to the syslog server
fn invoke hello-app hello
Switch to terminal 2 where the syslog server is running and check if the log output from the function was received in the syslog server.
Perhaps you feel like adding additional log output to the function and see it too being produced to the syslog server. If so, after adding a console.log or console.warn command, (and in the original terminal window) redeploy the function and invoke it again:
fn -v deploy --app hello-app --local
fn invoke hello-app hello
Using Papertrail Cloud Service for collecting and inspecting log output
A more advanced option to use as a log collection server is Papertrail - a SaaS service for log collection and inspection that you can start using for free. Set up a free Papertrail account.
On the Papertrail website, go to ‘Settings’ (top right hand corner), click on ‘Log Destinations’, and click ‘Create a Log Destination’.
In the create dialog, under TCP unselect ‘TLS’ and under both TCP and UDP select ‘Plain Text’. Click ‘Create’. You’ll see the address of your log destination displayed at the top of the page looking something like logs7.papertrailapp.com:
fn update app hello-app --syslog-url tcp://[your Papertrail destination]
You can confirm that the syslog URL is set correctly by inspecting your application:
fn inspect app hello-app
Let’s go over to the Papertrail Dashboard and click on our “System” to open a page with the log showing our function's output.
Resource: https://fnproject.io/tutorials/Troubleshooting/#LogCapturetoaLoggingService
Wrap existing Node module with Fn Function Wrapper
Suppose you already have Node code performing some valuable task. You can take the existing code and turn it into an Fn Function quite easily - using several approaches even. One is to build a custom Docker container and use it as the implementation for your function (see step 6 in this scenario). An easier one is shown next.
Copy the existing Node application existingNodeApplication.js to the folder created for function hello:
cp /root/scenarioResources/existingNodeApp.js /root/hello
This node application is quite simple, as you can verify:
cat /root/hello/existingNodeApp.js
Run the existingNodeApp:
node existingNodeApp.js YourName
Note: feel free to make changes to the existingNodeApp.js.
Open the file func.js in the text editor.
func.js
Select all current contents (CTRL + A), remove it (Del) and copy this snippet to the file:
const fdk=require('@fnproject/fdk'); const app = require( './existingNodeApp.js' ); fdk.handle(function(input){ let name = 'World'; if (input.name) { name = input.name; } return {'message': app.doYourThing(name)} })
The function hello now leverages the existing Node module existingNodeApp for the hard work this function is doing when invoked.
Deploy the Function Hello locally, into the app that was just created
fn -v deploy --app hello-app --local
Now to invoke the function:
echo -n '{"name":"Your Own Name"}' | fn invoke hello-app hello --content-type application/json
In the response, you will see the product of the existingNodeApp. The Fn function hello is now completely implemented by existingNodeApp. In this case this application is wafer thin, but in real life this can be a sizable Node application that uses scores of NPM modules. This application can be developed and tested in its own right, outside the context of the Fn framework. You have now seen how easy it is to wrap the application in/as a Function.
File Writing
If you want to write to a file from a function, it can only be to the local file system (inside the function container) and only to /tmp. Each function is configured with its own /tmp as non-persistent disk space. The size of this disk space is set as part of the function configuration.
Of course, functions can write files on/to storage services - such as Oracle Cloud Object Storage.
Step 3 - Invoke a Function using CURL
Invoke Function with CURL
In this step we will invoke the function with CURL. Subsequently, we will take a look at the context data available to the function when it is handling a request.
Getting a Function's Invoke Endpoint
In addition to using the Fn invoke command, we can call a function by using a URL. To do this, we must get the function's invoke endpoint. Use the command fn inspect function
fn inspect f hello-app hello
Get the value from the annotation fnproject.io/fn/invokeEndpoint
in the result of this inspect command.
You can invoke the function using curl at this endpoint. Set an environment variable HELLO_FUNCTION_ENDPOINT with the value of the endpoint.
export HELLO_FUNCTION_ENDPOINT=$(fn inspect f hello-app hello | jq -r '.annotations."fnproject.io/fn/invokeEndpoint"')
Now with the variable set you should be able to invoke the function using curl:
curl -X "POST" -H "Content-Type: application/json" -d '{"name":"Bob"}' $HELLO_FUNCTION_ENDPOINT
Trigger Function Execution (through HTTP)
If we would add a trigger of type http to the func.yaml for function hello, we can trigger the execution of a function by triggering the Fn runtime framework.
Open the file func.yaml
and this snippet at the end of the file:
triggers: - name: hello type: http source: /hello
Deploy the Function Hello locally, into the app that was just created
fn -v deploy --app hello-app --local
Now to invoke the function, we send an HTTP trigger to the Fn runtime server that in turn will send an HTTP request to the container that implements the function :
curl --data '{"name":"Bob"}' -H "Content-Type: text/plain" -X POST http://localhost:8080/t/hello-app/hello
Context available to a Function when processing a Request.
When you invoke a function, the request is handled and forwarded by the Fn Server to the function. This means that an HTTP request is sent to the container that implements the function. This request is received by a handler provided by the Fn FDK for Node. This handler can be seen in the file func.js - which is the generated Node implementation of the function.
Click on the file func.js
to open it in the editor. On line 4 you see the call fdk.handle()
. This initializes the Fn Runtime with a generic request handler; when a request is received it is forwarded to the function that is passed to fdk.handle() - the function that takes one parameter called input.
Add a second parameter to the function definition on line 4, to make this line read:
fdk.handle(function(input , ctx){
The FDK framework's handler (fdk.handle) will now pass the request context in this variable, in addition to the input or payload that was already passed to the function by the handler.
Change line 10 to make it read:
return {'message': app.doYourThing(name) ,'ctx':ctx}
Changes to file func.js are saved automatically by the editor - do not look for a Save button or use Ctrl+S.
Now we need to redeploy the function with the same command as before:
fn -v deploy --app hello-app --local
And invoke it, either through Fn:
echo -n '{"name":"Your Own Name"}' | fn invoke hello-app hello --content-type application/json
Or using CURL.
curl -X "POST" -H "Content-Type: application/json" -H "my-special-header: my-value" -d '{"name":"Johanna"}' $HELLO_FUNCTION_ENDPOINT
The custom HTTP header in this CURL call should now be visible in the response from the function because it is visible in the context sent to the function by Fn.
By inspecting the ctx input parameter, you can make your function interpret the request in a more encompassing way than by only inspecting the input parameter.
Detailed Overview of the Function Context
(courtesy of José Rodrigues of Link Consulting)
Context Variable (CTX)
- ctx.config : An Object containing function config variables (from the environment ) (read only)
- ctx.headers : an object containing input headers for the event as lists of strings (read only)
- ctx.deadline : a Date object indicating when the function call must be processed by
- ctx.callID : The call ID of the current call
- ctx.fnID : The Function ID of the current function
- ctx.memory : Amount of ram in MB allocated to this function
- ctx.contentType : The incoming request content type (if set, otherwise null)
- ctx.setResponseHeader(key,values...) : Sets a response header to one or more values
- ctx.addResponseHeader(key,values...) : Appends values to an existing response header
- ctx.responseContentType set/read the response content type of the function (read/write)
- ctx.httpGateway The HTTP Gateway context for this function (if set)
Resources
Check out this article for details about the contents of the Fn request context:Oracle Cloud Infrastructure Functions and Project Fn – Retrieving Headers, Query Parameters and other HTTP Request elements
Step 4 - Testing a Function
Test a Function
Originally, the Fn CLI supported the fn test
command that could run a series of predefined tests on the function. The definition of the tests was the same across all function implementation languages. However, somewhere along the way, this test support was dropped from Fn.
Testing a function is now your own responsibility - and can be done at various levels:
- test the code that implements the function - without invoking the function itself - using appropriate tooling for the relevant programming language
- test the function in its entirety - including the Fn framework - using a mechanism for testing HTTP services (such as Newman)
In order to test the function's implementation without testing the Fn framework, we should ideally implement everything that is specific to the function in a separate module and use the func.js only as the generic wrapper. We can the focus the testing effort on this separate module and all its dependencies.
Install the npm testing module jest (see jest documentation for details on how to get started). Jest has rapidly become the industry's choice for testing JavaScript & Node applications.
Execute this command to install jest as a development time dependency:
npm install --save-dev jest
Add this snippet to package.json
to have jest invoked whenever npm test is executed - creating a new property at the same level as main and dependencies :
,"scripts": { "test": "jest" }
Create the test file for module existingNodeApp; by convention, this file is typically called existingNodeApp.test.js:
touch existingNodeApp.test.js
And add the contents to existingNodeApp.test.js
- which specifies a spectacularly simple test:
const app = require( './existingNodeApp.js' ); const name ="Henk" test(`Simple test for ${name}`, () => { expect(app.doYourThing(name)).toBe(`Warm greeting to you, dear ${name} and all your loved ones`); });
Run the test using
npm test
This should report favorably on the test of module existingNodeApp.
This test of course does not test the Fn framework, the successful creation of the Docker container image and whatever is done inside func.js. It tests the core functionality that existingNodeApp provides to the wrapper function.
We can go one step further, and test func.js as well - still before the function is deployed to a container. We will use Jest - and in particular the mocking capabilities of Jest. The function - func.js - uses the Fn FDK framework - to handle HTTP requests that are handed to the function for procesing. However, in the test situation, we want to test outside the scope and context of the Fn framework. This can be done by using a mock for the fdk module. Jest allows us to define a mock in the following way :
- create file fdk.js in the folder mocks/@fnproject under the hello function
- implement the mock version of module @fnproject/fdk with a mock version of function handle
- create file func.test.js that uses module @fnproject/fdk and runs tests against func.js
Create the new file for implementing the mock fdk.js:
mkdir /root/hello/__mocks__
mkdir /root/hello/__mocks__/@fnproject
touch /root/hello/__mocks__/@fnproject/fdk.js
Open the new file fdk.js in the IDE. Copy this code snippet to the file.
const handle = function (f) { theFunction = f return f } let theFunction const functionCache = function getFunction() { return theFunction } exports.handle = handle exports.functionCache = functionCache
When the func.js is required, it invokes function handle on the fdk module and passes a function as input parameter. This function is the actual Fn function implementation that handles the input and context objects to the Fn function. In the case of the mock fdk module, the handle function will simply retain the function reference in local variable theFunction.
The test function invokes functionCache to retrieve the handle to this function and subsequently invoke it - just as it would be in case of the normal invocation of the Fn function.
Create the file for the Jest tests for module func:
touch /root/hello/func.test.js
Copy the following snippet to this file:
// simply require func.js registers the function (input, context) with mock fdk const func = require( './func.js' ); const fdk=require('@fnproject/fdk'); const name ="Bob" const input = {"name":name} const context = {"_headers":{"Host":"localhost","Content-Type":"application/json"}} const theFunction = fdk.functionCache() // get the function that was registered in func.js with the (mock) fdk handler test(`Test of func.js for ${name}`, () => { expect(theFunction(input,context).message) .toBe(`Warm greeting to you, dear ${name} and all your loved ones`); }); test(`Test of func.js for Host header in context object`, () => { expect(theFunction(input,context).ctx._headers.Host) .toBe(`localhost`); });
The story for this test: the test loads the object to test - func.js - and the mock for the Fn FDK. The require of func.js causes the call in func.js to fdk.handle() to take place; this loads the reference to function object defined in func.js in the functionCache. The test gets the reference to the function in the local variable theFunction. Two tests are defined:
- when the function is invoked with an object that contains a name property, does the response object contain a message property that has the specified value?
- when the function is invoked with a second object - context - does the response contain a ctx object
Run the test using
npm test
This should report favorably on the test of module existingNodeApp and the two tests for module func, the real function implementation - but still without the runtime interference of Fn.
A different type of test could forego the Node implementation and only focus on the HTTP interaction - including the Fn framework and the Container Image. That can be done using a tool such as Newman.
Service Testing with Newman
Newman is an npm module that is used for running Postman test collections from the command line - and therefore in an automated fashion. See Running collections on the command line with Newman for more details on Newman.
Install Newman as Node module:
npm install --save-dev newman
Copy these files to the folder with hello function resources. The first file defines a single request to the Hello Function along with a number of tests. This file - defined as a collection in Postman - relies on an environment variable defined in the file env.json. This second file does not exist yet; it will be created from the file env_temp.json. This file defines the variable with the endpoint for the hello function. The value of this variable is taken from the environment variable $HELLO_FUNCTION_ENDPOINT. We use the envsubst command for this replacement.
cp /root/scenarioResources/postman-hello-collection.json /root/hello
cp /root/scenarioResources/env_temp.json /root/hello
Open file package.json in the editor/IDE. Add this script element in the existing scripts element:
,"test-fn": "newman run /root/hello/postman-hello-collection.json -e /root/hello/env.json"This script is used to run the function test using Newman.
Replace the Hello Function's endpoint in template file env_temp.json and produce file env.json from the template env_temp.json with the replaced value:
envsubst < env_temp.json > env.json
You can check whether file env.json now contains the correct function endpoint.
cat env.json
To run the test, you can use
npm run test-fn
this will run the test-fn script as defined in the file package.json that will run Newman with the specified collection postman-hello-collection.json that was copied in from the scenario assets folder. You should now see confirmation of the tests - defined in the Postman collection and executed by Newman (against the - locally - deployed Function - invoked through the local Fn framework).
Check the contents of the file postman-hello-collection.json
- in the IDE or on the terminal:
cat postman-hello-collection.json
The json file contains a single request - to the Hello function - with a body and a header. It also defines four tests that the function's HTTP result should conform with. Note: the language used for defining the test conditions is described here in the Chai BDD Assertion Library. Feel free to edit the test definitions in the file.
"pm.test(\"Function Response Status\", function () {\r",
" pm.response.to.have.status(200);\r",
"});\r",
"pm.test(\"Function response must be valid and have a body\", function () {\r",
" pm.response.to.be.ok;\r",
" pm.response.to.be.withBody;\r",
" pm.response.to.be.json;\r",
"});\r",
"pm.test(\"Function response should have property message that should contain name Tester 1\", function () {\r",
" const jsonData = pm.response.json();\r",
" pm.expect(jsonData).to.have.property('message');\r",
" pm.expect(jsonData.message).to.include('Tester 1');\r",
" });\r",
"pm.test(\"Function response should have property ctx that contains custom header CustomHeader\", function () {\r",
" const jsonData = pm.response.json();\r",
" pm.expect(jsonData.ctx).to.have.property('_headers');\r",
" pm.expect(jsonData.ctx[\"_headers\"]).to.have.deep.property('Custom-Header');\r",
" }); "
Performance Testing
We will now briefly look at performance testing the Fn function, using a simple tool called Apache Bench.
Read this article for a very quick introduction of Apache Bench: https://www.petefreitag.com/item/689.cfm
Let's install Apache Bench:
apt install apache2-utils
and confirm by typing y
A simple test of the hello function - without supplying any input - looks like this:
ab -n 100 -c 10 $HELLO_FUNCTION_ENDPOINT
Here we ask for 100 requests, with a maximum of 10 requests running concurrently.
The tool reports how the response times were distributed.
A slightly more serious test would involve at least real input. Here we write the POST body to a file and then send that file along in all the test requests:
echo -n '{"name":"William Shakespeare"}' > postfile
ab -n 100 -c 10 -T 'application/json' -p postfile $HELLO_FUNCTION_ENDPOINT
It seems that if you run the test again, the results are quite a lot faster. Give it a try.
Step 5 - Passing Configuration Values to a Function
Passing Context Values to a Function
The behavior of a function can depend on the context in which the function is running. Depending on the environment (Dev, Test, Production), location (region), time of week | month | year, business unit of any other characteristic, the functionality may be (slightly) different. We do not want to change and redeploy the function for every change in context. Nor we do we want to pass these context properties in every request to the function. Fortunately, we can make use of the Fn runtime context for functions - that is available to the function in a native way at runtime through environment variables. Values can be set in the context before and at any time after deploying the function that uses these values.
Fn config variables can be set for applications (and apply to all functions in the application) or for specific functions. In addition, Fn automatically generates a number of environment variables for your use in every function.
- Application Config Variables: Variables stored in an application are available to all functions that are deployed to that application.
- Function Config Variables: Variables stored for a function are only available to that function.
- Pre-defined environment variables: By default, a number of environment variables are automatically generated in an Fn Docker image. The next figure show the automatically generated variables.
Add a configuration variable to the hello-app application
fn cf a hello-app welcome_message "Herzlich willkommen"
Check all configuration settings in hello-app
fn ls cf a hello-app
Invoke function hello again:
echo -n '{"name":"Your Own Name"}' | fn invoke hello-app hello --content-type application/json
You will notice that the context parameter that is passed to the function now contains a property welcome_message with the value that was set in the application.
Change the value of the configuration variable
fn cf a hello-app welcome_message "Bonjour mes amis"
Invoke function hello again and check the context parameter:
echo -n '{"name":"Your Own Name"}' | fn invoke hello-app hello --content-type application/json
Configuration values are available as environment variables inside functions. A common way to read their values - anywhere in the Node application that implements the function - is using `process.env["name of variable"].
Open file func.js in the editor and change line 10 to
return {'message': app.doYourThing(name), "special_message":process.env["welcome_message"] ,'ctx':ctx}
At this point, the function makes use - in a very superficial way - of the value set in the configuration variable.
Now we need to redeploy the function with the same command as before:
fn -v deploy --app hello-app --local
Invoke function hello again and check the response for the special_message property:
echo -n '{"name":"Your Own Name"}' | fn invoke hello-app hello --content-type application/json
Change the configuration variable and invoke again (no redeployment necessary):
fn cf a hello-app welcome_message "Welkom vrienden"
echo -n '{"name":"Elisabeth"}' | fn invoke hello-app hello --content-type application/json
echo -n '{"name":"Alexander"}' | fn invoke hello-app hello --content-type application/json
Notice your function immediately picks up and uses the variables. You don’t need to redeploy the function or make any other modifications. The variables are picked up and injected into the Docker instance when the function is invoked.
Setting Variables with Fn YAML Files
In addition to using the CLI to set Fn variables for your RuntimeContext, you can set them in Fn YAML configuration files.
Open file func.yaml in the editor.
Add the following snippet at the bottom of the file:
config: hello-config-value1: Allons enfants de la patrie hello-config-value2: We lopen door de lange lindenlaan
Deploy and invoke the function
fn -v deploy --app hello-app --local
echo -n '{"name":"Your Own Name"}' | fn invoke hello-app hello --content-type application/json
Check the contents of the ctx property: it contains the two new configuration variables.
Open file existingNodeApp.js in the editor.
Replace line 2 with this line - that will read environment variable hello-config-value1 and include its value in the function response.
return `Warm greeting to you, dear ${name} and all your loved ones and do not forget: ${process.env['hello-config-value1']}`;
Deploy and invoke the function
fn -v deploy --app hello-app --local
echo -n '{"name":"Your Own Name"}' | fn invoke hello-app hello --content-type application/json
The response contains the message object that contains the value of the configuration variable hello-config-value1
.
Hint:
If you put all of your functions under the same parent directory, you can setup an app.yaml file to hold configuration data. Create an app.yaml file in the parent directory of your two functions. Deploy and invoke your function locally from the parent directory of your functions. The command deploys all functions under the parent directory to the application specified in app.yaml.
Step 6 - Functions in other Languages such as Java
Creating Functions in other Languages
The hello function we have been working with in the previous steps was implemented using Node JS (server side JavaScript). Fn supports many other runtime, such as Go, Python, Java and Ruby. Additionally, you can create a function from just any Docker Container, regardless which (combination of) runtime engines and languages you have used in it.
In this step you will create a function in Java. Feel free to try out the other runtimes as well.
Return to the home directory and create a new function called hello-java with Java as its runtime engine.
cd ~
fn init --runtime java hello-java
Check out the generated directory structure and Java Classes:
ls -R hello-java
Now inspect the generated Java class that handles requests - and can be customized by us.
cd hello-java/src/main/java/com/example/fn
cat HelloFunction.java
Java Class HelloFunction.java was generated as the starting point for this function. You can check out file in the editor.
Warning: if you make changes to the output of the file, ensure that you change the unit test accordingly because when the test fails, the function cannot be built and deployed. The unit test is in the source file hello-java/src/test/java/com/example/fn/HelloFunctionTest.java.
It is not as obvious as in the func.js generated for the Node runtime that an Fn handler is at play. However, also in the case of Java based functions, requests are handled by a generic Fn Java runtime handler before being passed to our own code. Check in func.yaml how the Java Class and method that the generic handler should forward the request to are specified.
Deploy the Java Function hello-java locally, into the app that was created in step 2 of this scenario. You will again see a Docker Container Image being built. Or actually: two images. The first image is the build environment with the full Java JDK, Maven and facilities to run unit tests. The outcome of this first image is a Fat Jar that contains the built application artifact. This is the input for the second container image - that is based on the Java Runtime Environment, a much lighter weight image. The final result of deploying the function is the image based on JRE and with only the Fat Jar created for the function.
cd ~/hello-java
fn -v deploy --app hello-app --local
To invoke the Java function, execute this command:
time fn invoke hello-app hello-java
Note: we have added the time
instruction to get timing for the cold startup time of the function. In step 6, we will use GraalVM powered ahead of time compiled Java applications, that are supposed to have a much faster cold startup time. Please remember the values you are getting for the timing of this command for comparison in step 6.
To verify the cold startup effect, invoke the Java function again:
time fn invoke hello-app hello-java
The real time reported is expected to be much shorter this time - because the container that implements the function is already running and started up.
To send input to the function, use the following command:
echo -n 'Your Own Name' | fn invoke hello-app hello-java --content-type application/json
Again, a friendly, this time personalized, welcome message should be your reward.
Further Explorations
To try out other languages, simply replace java as runtime with go or python in the call to fn init
. For example:
cd ~
fn init --runtime go hello-go
Custom Docker Containers as Function implementation
It is possible to take any Docker Container and use it as the implementation of a function. In that case the runtime is docker. A subsequent step in this scenario demonstrates this compelling feature of Fn.
GraalVM
Project Fn also supports binary executables with GraalVM; there is a special runtime available that takes a Java application and builds it all the way into a container image with just a binary executable. This results in an even smaller image and even faster function warmup and execution. In step 5 of this scenario, you can check out this approach to packaging Java applications.
Step 7 - Bonus: Ahead of Time Function Compilation with GraalVM
Bonus: Ahead of Time Function Compilation with GraalVM
Note: this step is based very heavily on this article on Medium Serverless Functions — Some Like It AOT!
This tutorial walks through how to perform Ahead of Time compilation on an Fn function implemented with Java- to produce a function container image that is very small and has very rapid startup time.
GraalVM
GraalVM is an open source high-performance embeddable polyglot virtual machine that recently sparked a lot of interests in the Java community as it supports Java and other JVM languages such as Groovy, Kotlin, Scala, etc. In addition, GraalVM also supports JavaScript (including the ability to run Node.js applications), Ruby, R, Python and even native languages that have an LLVM backend such as C or C++. GraalVM is incredibly powerful and versatile, it can help in many ways from boosting the performance of Java applications to enabling polyglot applications development that combine different languages in order to to get the best tools and features from different ecosystems. For example, using GraalVM, it is possible to use R for data visualization, Python for machine learning and JavaScript to combine those two functionalities together.
This step will focus on a specific GraalVM capability, i.e. GraalVM Ahead-of-time Compilation (AOT) and more specifically on GraalVM native-image feature with Java functions.
Create a Native Java Function
Return to the home directory. Bootstrap a GraalVM based Java function called hello-java-aot with Java Native (aka GraalVM Ahead of Time compilation) as its runtime engine.
cd ~
fn init --init-image fnproject/fn-java-native-init hello_java_aot
If you compare this to the approach used for generating a "regular" Java function, the key difference is that we instruct the Fn CLI to rely the fnproject/fn-java-native-init Docker init-image (see here for more details on init-image) to generate a boilerplate GraalVM based Java function (instead of relying on the regular java runtime option).
The func.yaml contains some meta-data related to the function (its version, its name, etc.). It is very similar to a regular Java func.yaml, the only difference being the runtime entry. The Java function uses the java runtime while the GraalVM native-image function rely on the default Docker runtime which also explains the presence of a Dockerfile.
Now inspect the generated files and notice the Docker file.
cd hello_java_aot
ls -R
cat Dockerfile
To generate the Docker image of the function, the Fn CLI is relying on the Dockerfile that was generated during the previous init phase. If you inspect this Dockerfile or if you look at the verbose output of the depoyment, you will notice that one of the step is using GraalVM's native-image utility to compile the Java function ahead-of-time into a native executable.
The resulting function does not run on a "regular" Java virtual machine but uses the necessary components like memory management, thread scheduling from a different virtual machine called Substrate VM (SVM). SVM, which is a part of the GraalVM project, is written in Java and is embedded into the generated native executable of the function. Given it is a small native executable, this native function has a faster startup time and a lower runtime memory overhead compared to the same function compiled to Java bytecode running on top of a "regular" JVM.
That native function executable is finally added to a base lightweight image (busybox:glibc) with some related dependencies. This will constitute the function Docker image that the Fn infrastructure will use when the function is invoked.
Deploy and Run the Java Application
Java Class HelloFunction.java was generated as the starting point for this function. You can check out file in the editor.
Warning: if you make changes to the output of the file, ensure that you change the unit test accordingly because when the test fails, the function cannot be built and deployed. The unit test is in the source file hello-java-aot/src/test/java/com/example/fn/HelloFunctionTest.java.
Note: the fact that is application is deployed as native Java application does not mean anything for the way you program the Java code. (well, in all honesty, it does; there are some Java mechanism that are not available in native Java applications - such as dynamic class loading and certain elements of reflection; that is why turning Spring applications into Native Java applications is not a simple challenge).
Deploy the native Java application just as before the regular Java application
fn -v deploy --app hello-app --local
Note: the process of turning the Java application into a natively executable image can take quite long. We perform some heavy lifting once (at compile time) to benefit many times (on every execution) later on.
One major difference we expect to see is in the size of the Docker image prepared for our function. It should be small - much smaller than the container image produced for the regular Java function that contains a full blown Java Runtime environment.
To check and compare the sizes of the Docker images, execute this command:
docker images | grep hello
As you can see, the function image that includes everything required to run, including the operating system and the function native executable, is only weighing around 21 MB! Since all necessary components of a virtual machine (ex. GC) are embedded into the function executable, there is no need to have a separate Java runtime (JRE) in the function Docker image. When a function is invoked through Fn, Fn will instruct Docker to pull and launch the corresponding container image and hence the smaller this container image is, the better it is to reduce the cold-startup time of this function.
To give us an idea, we can quickly measure the cold startup of the function. Execute this command:
time fn invoke hello-app hello_java_aot
We expect to see that the cold startup time of a GraalVM native-image function is improved, comparing to the regular Java function with JIT compilation.
Those numbers will vary depending on the machine you run the tests on but this basic benchmark shows that the cold startup of the GraalVM native-image function is faster. In this particular example, the cold startup of the GraalVM native-image function is probably ~70% of the cold startup time of the same Java function that uses a regular JVM (HotSpot in the case of Fn with Java runtime).
Step 8 - Bonus: Custom Docker Images as Function Implementation
Bonus: Custom Docker Images as Function Implementation
Note: this step is based very heavily on this tutorial on the Fn Project Website Make your own Linux command Function with HotWrap and a Customer Docker Image
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), but you aren’t required to be very Docker-savvy to develop functions with Fn.
What if you want to make a function using Linux command line tools, or a script that does not involve one of the supported Fn languages? Can you use your Docker image as a function? Fortunately the design and implementation of Fn enables you to do exactly that. Let’s build a simple custom function container image to see how it’s done.
The Fn HotWrap tool allows you to create functions using conventional Unix command line tools and a Docker container. The tool provides the FDK contract for any command that can be run on the command line. Once wrapped, event data is passed to your function via STDIN and the output is returned through STDOUT.
Initial Linux Command
Try out this command - that simply reverses text.
echo "Hello World" | rev
Our function implementation is simple: we only need to call the command /bin/rev with the input to the function and produce the result of the rev* command as output.
Initialize Function
We need a directory for our project.
cd ~
mkdir revfunc
and change into the directory:
cd revfunc
In the folder , create a func.yaml file
touch func.yaml
Open this file in the text editor and copy/paste the following as its content:
schema_version: 20180708 name: revfunc version: 0.0.1 runtime: docker triggers: - name: revfunc type: http source: /revfunc
This is a typical func.yaml except that instead of declaring the runtime as a programming language we've specified docker. If you were to type fn build right now you'd get the error:
Fn: Dockerfile does not exist for 'docker' runtime
This is because when you set the runtime type to docker. The fn build
command defers to your Dockerfile to build the function container image–and you haven’t defined one yet!
Create the Dockerfile for the Function
Create a file named Dockerfile
touch Dockerfile
and copy/paste the following as its content:
FROM alpine:latest # Install hotwrap binary in your container COPY --from=fnproject/hotwrap:latest /hotwrap /hotwrap CMD "/bin/rev" ENTRYPOINT ["/hotwrap"]
Here is an explanation of each of the Docker commands.
- FROM alpine:latest - Use the latest version of Alpine Linux as the base image.
- COPY --from=fnproject/hotwrap:latest /hotwrap /hotwrap - Install the HotWrap Fn tool.
- CMD "/bin/rev" - The Linux command to run.
- ENTRYPOINT ["/hotwrap"] - Tells the container to execute the previous command using HotWrap: /hotwrap /bin/rev
Build and Deploy the Function
Once you have your custom Dockerfile you can simply use fn build to build your function. Give it a try:
fn -v build
Just like with a default build, the output is a container image. From this point forward everything is just as it would be for any Fn function. Since you’ve previously started an Fn server, you can deploy it.
fn -v deploy --app hello-app --local --no-bump
List the functions in the hello-app application, to see that now revfunc is a function:
fn list functions hello-app
Pro tip: The Fn cli let's you abbreviate most of the keywords so you can also say fn ls f hello-app
! You should see the same output.
Invoking the Function
With the function deployed let’s invoke it to make sure it’s working as expected.
echo "Hello World" | fn invoke hello-app revfunc
For this command you should see the following output: dlroW olleH
We included an HTTP trigger declaration in the func.yaml so we can also call the function with curl:
curl --data "Hello World" -H "Content-Type: text/plain" -X POST http://localhost:8080/t/hello-app/revfunc
What about this one?
curl --data "Eva, can I see bees in a cave" -H "Content-Type: text/plain" -X POST http://localhost:8080/t/hello-app/revfunc
Conclusion
One of the most powerful features of Fn is the ability to use custom defined Docker container images as functions. This feature makes it possible to customize your function’s runtime environment including letting you use Linux command line tools as your function. And thanks to the Fn CLI's support for Dockerfiles it's the same user experience as when developing any function.
Having completed this step you've successfully built a function using a custom Dockerfile. Congratulations!