OCI Functions | Part 1 - Invoking Functions

OCI Functions | Part 1 - Invoking Functions

Introduction to OCI Functions

In this first blog post of the series, we will take our first step into OCI (Oracle Cloud Infrastructure) Functions. This is a service available on the Oracle Cloud, OCI Functions allow you to package your code so it can run autonomously within its own container. It's a serverless offering, meaning you won't be bogged down by the complexities of server configurations and maintenance. This service is really flexible and offers lots of benefits, some of which are:

  • Billed only for usage
  • Scalable to flex with your demand
  • Secure to keep your data and applications safe
  • Uses an open source framework enabling your applications and functions to be free from vendor lock in

OCI Functions are based on the https://fnproject.io/. This open source framework guarantees your the flexibility to host your functions as you see fit.

OCI Functions supports many languages including:

  • Python
  • Java
  • Node
  • Go
  • Ruby
  • C#

In this series I will make use Python as my language of choice but everything we do is achievable in other languages.

OCI Function Applications

Before we get into the juicy bits we need to create an Application. The Application is used to group functions under a meaningful name and hosts some useful configuration information. We can create an Application by navigating to Developer Services and then Functions. We can find Applications listed under the heading of Functions.

Use the button to create an application and give it a name.

The application is going to expect to be attached to a Virtual Cloud Network (VCN) and some subnets. If you do not have a VCN, you can create one using the wizard in the VCN section of OCI. For your exploration of the service it is recommended, for simplicity, to create a VCN with internet connectivity and attach the functions to the private subnets that are created. Attaching them to the private subnet will help protect them from being exposed to the internet. If you want the function to be accessible from the internet then you either need to attach it to the public subnets or use another solution that can handle incoming requests from the public internet.

Methods of Creating an OCI Function

Once we have created the new Application we have five ways to interact with functions.

  • Create a pre-built function

Create a function using the CLI

  • Using Cloud Shell
  • Using a local development environment
  • Create a function using the Code Editor
  • Create a function using Terraform
  • Control functions using the Rest API

Over this blog series we will explore the different methods and look at the pros and cons of each approach. For now we will keep it simple and focus on creating a pre-built function and how we can use it. Make sure you subscribe to our blog or follow us on social media to keep informed of new posts!

Creating a Pre-Built Function

Under the menu items Developer Services > Functions navigate to Pre-Built Functions. Here we find the functions that Oracle has developed for us. We can select the function called Object Storage File Zip. Then we can click on Create Function.

OCI is going to help guide us through the provisioning of this pre-built function in our OCI environment.

As you can see by default Oracle is taking the initiative to create the required IAM policies that enable the function to work.

There are some properties that are configurable for now we will keep the default values. The properties are:

  • Memory - the amount of memory available to the function at runtime
  • Timeout - the maximum allowed time for the function can run for
  • Provisioned Concurrency - enables quicker executions of the function at an increased cost
  • PBF_LOG_LEVEL - the amount of detail to include in the functions logs
  • Tagging - label resources to help manage and report on them

After the function is created we can review the summary of activity.

Policy

Lets take a look at the policy that Oracle has created.

As you can see, for this blog, I am working in a compartment called data-engineering-oci-functions. The dynamic group that Oracle created for us has been a set of actions on some objects.

  • Read object storage namespaces
  • Manage objects
  • Manage buckets
  • Read child compartments

If you are new to policies you can read about the https://docs.oracle.com/en-us/iaas/Content/Identity/Concepts/policysyntax.htm, this is a beautiful and easy to read language that OCI uses to define the policy definitions.

Dynamic Group

The dynamic group is a layer of abstraction that enables us to add items dynamically to the group that will inherit the permissions defined in the policy. As you can see above the policy isn’t granting actions directly to the function it is applying them to the dynamic group.

The function becomes a member of the dynamic group and will take on the permissions. This is a nice flexible approach that gives us the capability of adding more objects that need the same permissions at a later date.

Within the definition of a dynamic group we can add Matching Rules, these rules are used to dynamically add objects as members of the group.

Function

Let us take a look at the function. We can see the selections we made during the creation of the function have been applied. A notable addition at this point is the Endpoint, as the name suggests this is the information we need to execute the function.

Great, we have an endpoint to invoke the function. So how we can get it to execute?!?

Executing the Function

We have a number of methods available to execute the function, this include:

  • Fn Project’s command line interface (CLI)
  • OCI’s CLI
  • OCI SDK
  • Signed Request to the Endpoint

Fn Project CLI

The Fn Project CLI can be used to execute the function, we can take advantage of the OCI Cloud Shell which has the Fn CLI already installed and pre-configured. Alternatively, you can install it locally and configure it.

Launch the Cloud Shell from the OCI interface.

Within the shell type in the following commands:

fn invoke data-engineering-oci-functions-application data-engineering-oci-functions-object-storage-zip

Oh no! That didn’t go well, what could be the problem.

Error invoking function. status: 502 message: function failed

As might have guessed this function needs some parameters. We need to tell it the name of the bucket and the objects that we would like to zip.

The parameters required by this function are listed in its documentation which can be found here:

https://docs.oracle.com/en-us/iaas/Content/Functions/Tasks/functions_pbf_catalog_object_storage_file_zip.htm#pbf-object-storage-file-zip

The easiest way to invoke the function using parameters and the Fn CLI is to create a text file containing the JSON payload. For this example, we will save the content (with your values substituted as needed) into a file called payload.json.

{
  "COMPARTMENT_ID": "your-compartment-id",
  "REGION": "uk-london-1",
  "SOURCE_BUCKET": "source-bucket",
  "SOURCE_FILES": "/",
  "TARGET_BUCKET": "destination-bucket",
  "ALLOW_OVERWRITE": true
}

Now we can invoke the function and pass the payload using this command:

cat payload.json | fn invoke data-engineering-oci-functions-application data-engineering-oci-functions-object-storage-zip

All being well we should see confirmation of the execution.

{"startTime":"2023-09-01T21:41:36.018Z","endTime":"2023-09-01T21:41:38.799Z","runTime":"PT2.781S","code":200,"status":"Success","data":{"additionalInformation":{"Message":"Zip File: PBF_ZIP_data-engineering-oci-functions-destination_2023-09-01T21:41:36.028Z.zip uploaded to Bucket: data-engineering-oci-functions-destination"}}}

OCI CLI

The OCI command line interface can be used to execute the function. Once again to demonstrate this we will take advantage of the Cloud Shell. The OCI CLI can also be installed locally and configured if required.

Again I am not a fan of providing lengthy payloads on the command line and OCI CLI in many cases lets us take advantage of providing the parameters from a file. For the command we are interested in using, we can get a sample JSON file to use as a template.

oci fn function invoke --generate-full-command-json-input

This gives us some output that we can translate.

{
  "body": "string",
  "file": "/path/to/file",
  "fnIntent": "httprequest|cloudevent",
  "fnInvokeType": "detached|sync",
  "functionId": "string"
}

To run the exact same command that we used with the Fn CLI we can substitute some values.

{
  "body": "{\"COMPARTMENT_ID\": \"ocid1.compartment.oc1..<id>\",\"REGION\": \"uk-london-1\",\"SOURCE_BUCKET\": \"data-engineering-oci-functions-source\", \"SOURCE_FILES\": \"/\",  \"TARGET_BUCKET\": \"data-engineering-oci-functions-destination\",  \"ALLOW_OVERWRITE\": true }",
  "file": "-",
  "fnIntent": "httprequest",
  "fnInvokeType": "sync",
  "functionId": "ocid1.fnfunc.oc1.uk-london-1.<id>"
}

At this point I’m sure we are both thinking the Fn CLI is a little bit more friendly. Of course the OCI CLI is the Swiss army knife of the Oracle Cloud and is useful beyond working with functions!

OCI SDK

There a number of SDKs that you can use to interact with OCI, here I’m going to show how we can invoke the function using the Python OCI SDK. Once again I’m running the python from the command line of the Cloud Shell to take advantage of the configuration and seamless authentication.

# import the modules we need
from oci import functions as fn
import json, oci

# Load the payload file we created when using the Fn CLI
with open('payload.json', 'r') as ifile:
    payload = json.load(ifile)

# Load the OCI configuration
config = oci.config.from_file()

# Create a Functions Management client to get function details
fn_mgmt_client = fn.FunctionsManagementClient(config)
fn_details = fn_mgmt_client.get_function(function_id='ocid1.fnfunc.oc1.uk-london-1.aaaaa...').data

# Now, set the invoke endpoint based on the function's details
fn_invoke = fn.FunctionsInvokeClient(config)
fn_invoke.base_client.set_region('uk-london-1')
fn_invoke.base_client.endpoint = fn_details.invoke_endpoint

# Invoke the function
response = fn_invoke.invoke_function(
    function_id='ocid1.fnfunc.oc1.uk-london-1.aaaaaaa...',
    invoke_function_body=json.dumps(payload)
)

# Print the response
print(response.data.text)

This is a simple script to showcase how to invoke the function using the Python SDK. For simplicity we reused the file already created to hold the payload for the Fn CLI example. This is a great example of why holding payloads in files is so much better than typing them on the command line. They are persisted and available to reuse, for debugging and for auditing.

After we’ve got the payload we use the OCI module to get the configuration from where the script is running. So we take advantage of the Cloud Shell environment’s integration with OCI.

The next notable part of the script is interrogating the function to get some details. We use the FunctionsManagementClient to look up the function and make the endpoint available within the script.

Finally, we setup a FunctionsInvokeClient which we use to execute the function and then print the response to the terminal.

This script was executed with the simple command:

python start_function.py

Signed HTTP Request

The final method to invoke the function is using a signed HTTP request from your local machine. To perform this action we need to ensure we have setup a private key and a public key. We can do this with:

openssl genpkey -algorithm RSA -out oci_api_key.pem
chmod go-rwx oci_api_key.pem
openssl rsa -pubout -in oci_api_key.pem -out oci_api_key_public.pem

This will store two files in your working directory called:

  • oci_api_key.pem - this is the private key and should be kept secure
  • oci_api_key_public.pem - this is the public key that we upload to Oracle under your Profile and then API Keys

Once uploaded you should configure the OCI CLI with:

oci setup config

Now all we need to do is make a call from our machine to OCI telling it to invoke the function.

oci raw-request --http-method POST \
                --target-uri <invoke-endpoint> \
                --request-body file://path/to/payload.json

In the previous script we didn’t access the invoke endpoint directly we used the SDK to get it programmatically. This time we will use the Cloud Shell to access it.

fn inspect function data-engineering-oci-functions-application data-engineering-oci-functions-object-storage-zip

This gives us something like:

{
        "annotations": {
                "fnproject.io/fn/invokeEndpoint": "https://.../invoke",
                "oracle.com/oci/compartmentId": "ocid1.compartment.oc1..aaa...",
                "oracle.com/oci/imageDigest": ""
        },
        "app_id": "ocid1.fnapp.oc1.uk-london-1.aaaa...",
        "created_at": "2023-09-01T20:58:25.872Z",
        "id": "ocid1.fnfunc.oc1.uk-london-1.aaaa...",
        "memory": 256,
        "name": "data-engineering-oci-functions-object-storage-zip",
        "timeout": 300,
        "updated_at": "2023-09-01T20:58:25.872Z"
}

We want to use the value of fnproject.io/fn/invokeEndpoint as the --target-uri.

oci raw-request --http-method POST \
                --target-uri https://.../invoke \
                --request-body file://payload.json

This method will only work if you decided to attach your application to the public subnets.

In Summary

In this first blog post we took our first step into OCI Functions. We found your functions are organised into Applications. Before we create an Application we need to think about network connectivity and setup a VCN to satisfy our requirements. The functions you create can be based on a number of programming languages and are based on the Fn Project giving you greater freedom for future decisions.

Oracle provide us with a small number of Pre-Built functions that we can deploy into our Applications. These can be used as intended or simply be deployed to understand how functions are managed and invoked. From the deployment of the Pre-Built function we understand that a Function can be added as a member of a Dynamic Group. The Function by proxy of the Dynamic Group can then be secured through a Policy definition. We know that we have some configuration available to us to restrict the execution of the Function, we can control the timeout, memory and provisioned concurrency.

In this first taste of OCI functions we focused on the different methods available to you for invoking the function. In the future blog posts we will delve deeper into the different methods of creating functions and how we can gain insight into the functions. Then finally we will look at some applications within the OCI ecosystem of practical uses for functions.

Please subscribe to the Rittman Mead blog and follow us on social media so we can inform you when the new posts are available!