Event-Driven Infrastructure as Code with OCI Events, Functions and Python

Event-Driven Infrastructure as Code with OCI Events, Functions and Python

I was playing lately with Oracle Cloud Infrastructure (OCI) Events Service and Functions and while searching through resources already available, I stumbled upon this great video about OCI Functions and I thought that it would be nice to actually create the demo presented in that video.

The demo use case sounds something like this:

When a new Compute instance is provisioned or terminated, an OCI Event rule will call an OCI Function that will create or delete a new Object Storage Bucket with the name of the instance in the same compartment.

Let’s build this.

Setup Dynamic-Group & Policies

Before going into Events, Functions, and Python, we should set up the environment first.

Create a Dynamic-Group for OCI Functions

I have a dynamic-group that I use for both my Fn development environment (which is on an OCI Compute instance) and for giving rights to my Functions in OCI.

Check this post if you want to see how to set up your development environment for OCI Functions.

Under Identity – Dynamic Groups click on Create Dynamic Group:

There are two rules here:

  1. The first rule is for my development environment – so the instance from where I deploy my Functions has access
    1. I specify the Instance ID but also the Compartment ID if I want to use another instance from the same compartment
  2. The second rule is for the Functions
    1. I specify the resource type fnfunc and the compartment where I will deploy the Function

Create Policies

Under Identity – Policies click on Create Policy:

Statement 1 – Will give access to Function as a Service to read the docker repositories in my tenancy

Statement 2 – Will give access to Function as a Service to access network resources in my compartment

Statement 3 – Will give access to my Fn Development Instance and my deployed Functions to manage all resources in my compartment. Of course, you can fine grain the policies depending on your use case.

Make sure you also have the right policies for the FaaS – check this page to create the policy you need.

Create a new Application

Under Developer Services – Functions click on Create Application:

I have deployed my app in a public subnet, inside my VCN and I will send the logs to my papertrail account – I like to use papertrail for logging, especially in a development phase of my apps.

Create Function

From your development environment, create a new python function – I am doing this from my OCI Compute instance:

ubuntu@fn-dev:~$ fn init --runtime python create-bucket-for-compute
Creating function at: ./create-bucket-for-compute
Function boilerplate generated.
func.yaml created.

The newly created function should look like this:

ubuntu@fn-dev:~$ cd create-bucket-for-compute/
ubuntu@fn-dev:~/create-bucket-for-compute$ ll
total 20
drwxr-xr-x  2 ubuntu ubuntu 4096 Apr 18 16:34 ./
drwxr-xr-x 10 ubuntu ubuntu 4096 Apr 18 16:34 ../
-rw-r--r--  1 ubuntu ubuntu  574 Apr 18 16:34 func.py
-rw-r--r--  1 ubuntu ubuntu  154 Apr 18 16:34 func.yaml
-rw-r--r--  1 ubuntu ubuntu    3 Apr 18 16:34 requirements.txt

Deploy & Test

We’ll just deploy and test the hello-world function for now and we’ll come back a bit later on this to actually write the code that will create or delete a bucket based on a Compute event.

From inside the function folder, deploy to the application created from the OCI Console – mine was create-bucket-app.

ubuntu@fn-dev:~/create-bucket-for-compute$ fn -v deploy --app create-bucket-app

Let’s try to invoke the function to see that it works:

ubuntu@fn-dev:~/create-bucket-for-compute$ fn invoke create-bucket-app create-bucket-for-compute
{"message": "Hello World"}

Great – now that this works, we should set up the event first and then write the complete function based on the CloudEvent.

Create Event Rule

Let’s create an event that will trigger the whole process.

Under Application Integration – Events Services click on Create Rule:

I am choosing to trigger this event when:

  • A new instance is created – Instance – Lanch End
  • An instance is terminated – Instance – Terminate End
  • I want only to trigger the event only for instances inside my compartment – Ionut

The Action is to trigger the function – create-bucket-for-compute – that we just deployed earlier.

The python function

Now, let’s head back to the development environment and let’s write the actual script that will do all the work.

I will be using the OCI Python SDK to create and delete the buckets, so I have to add it to the requirements.txt so it will be installed on the docker container that will hold the function:

ubuntu@fn-dev:~/create-bucket-for-compute$ cat requirements.txt
fdk
oci==2.12.4

This is the latest stable version at the time of the article.

Authentication

To execute API calls against OCI, you have to authenticate. I will use Instance Principals signer for that. You can read more about that in this article.

# authenticate via Instance Principals
signer = oci.auth.signers.get_resource_principals_signer()

Read the Cloud Event

When called by the event service, a Cloud Event type of message will be sent and you can read it like this:

# get the info from the event service
body = json.loads(data.getvalue())

logging.getLogger().info("Body is: " + str(body))

Get the event type

We will use the same function for both create and delete, so we’d have to know whether the instance was created or deleted:

# see if instance was created or terminated
action_type = body["eventType"]
        
logging.getLogger().info("action_type is: " + str(action_type))
        
# create or delete bucket based on event type
if action_type == "com.oraclecloud.computeapi.launchinstance.end":
    create_bucket()
elif action_type == "com.oraclecloud.computeapi.terminateinstance.end":
   delete_bucket()

You can find all the event types in here.

Initiate the Object Storage Client

We will initiate the Object Storage Client so we can create and/or delete Object Storage Buckets:

# initiate the object storage client
object_storage_client = oci.object_storage.ObjectStorageClient({}, signer=signer)

We pass the signer for authentication purposes. You can find out more about how to use the OCI Python SDK here.

Create Bucket

We will create a bucket in the same compartment as the instance and with the same name as the instance:

# get the namespace (tenancy name)
namespace = object_storage_client.get_namespace().data
logging.getLogger().info("Namespace is: "+ str(namespace))
        
# create the bucket
create_bucket_response = object_storage_client.create_bucket(
    namespace,
    oci.object_storage.models.CreateBucketDetails(
        name=resource_name,
        compartment_id=compartment_id,
        public_access_type='ObjectRead',
        storage_tier='Standard',
         object_events_enabled=True
     )
)

The compartment id and the resource name will come from the Cloud Event.

Delete Bucket

To delete a bucket, you need the namespace and its name:

# get the namespace (tenancy name)
namespace = object_storage_client.get_namespace().data
        
# delete the bucket
object_storage_client.delete_bucket(namespace, resource_name)

Complete solution

Let’s put everything together in the function:

import io
import json
import logging

from fdk import response
import oci


def handler(ctx, data: io.BytesIO=None):
    try:
        # authenticate via Instance Principals
        signer = oci.auth.signers.get_resource_principals_signer()
        
        # get the info from the event service
        body = json.loads(data.getvalue())

        logging.getLogger().info("Body is: " + str(body))
        
        # get compartment id and instance name
        compartment_id = body["data"]["compartmentId"]
        resource_name = body["data"]["resourceName"]
                      
        # see if instance was created or terminated
        action_type = body["eventType"]
        
        logging.getLogger().info("action_type is: " + str(action_type))
        
        # initiate the object storage client
        object_storage_client = oci.object_storage.ObjectStorageClient({}, signer=signer)
        
        # create or delete bucket based on event type
        if action_type == "com.oraclecloud.computeapi.launchinstance.end":
            create_bucket(object_storage_client, compartment_id, resource_name)
        elif action_type == "com.oraclecloud.computeapi.terminateinstance.end":
            delete_bucket(object_storage_client, compartment_id, resource_name)
        
    except (Exception, ValueError) as ex:
        logging.getLogger().error("There was an error: " + str(ex))

    return response.Response(
        ctx, response_data=json.dumps(
            {"message": "The function executed successfully"}),
        headers={"Content-Type": "application/json"}
    )


def create_bucket(object_storage_client, compartment_id, resource_name):
    logging.getLogger().info("Create bucket")
    try:
        # get the namespace (tenancy name)
        namespace = object_storage_client.get_namespace().data
        logging.getLogger().info("Namespace is: "+ str(namespace))
        
        # create the bucket
        create_bucket_response = object_storage_client.create_bucket(
            namespace,
            oci.object_storage.models.CreateBucketDetails(
                name=resource_name,
                compartment_id=compartment_id,
                public_access_type='ObjectRead',
                storage_tier='Standard',
                object_events_enabled=True
            )
        )
    except Exception as ex:
        logging.getLogger().error("There was an error creating the bucket: " + str(ex))
        
    logging.getLogger().info(f"Bucket {resource_name} was created")


def delete_bucket(object_storage_client, compartment_id, resource_name):
    logging.getLogger().info("Delete bucket")
    try:
        # get the namespace (tenancy name)
        namespace = object_storage_client.get_namespace().data
        
        # delete the bucket
        object_storage_client.delete_bucket(namespace, resource_name)
        
    except Exception as ex:
        logging.getLogger().error("There was an error deleting the bucket: " + str(ex))

    logging.getLogger().info(f"Bucket {resource_name} was deleted")




and deploy it:

ubuntu@fn-dev:~/create-bucket-for-compute$ fn -v deploy --app create-bucket-app

Test the solution

All that’s left now is to test the solution.

First of all, let’s create a new Compute instance in our compartment and wait for it to be provisioned:

Once it was provisioned – the Event Rule created earlier will be triggered (you can check the logs under Application Integration – Events Service – Your Rule – Logs) and that will call our Function.

The Function will create, in the same compartment, a bucket with the same name as the instance:

Now, terminate the instance and the bucket will be automatically deleted.

Conclusions

The combination of OCI Events and OCI Functions can be really useful when trying to automate your tasks or when trying to build Infrastructure as Code.

You can find the whole function on my Github.

Resources

Setup development environment for OCI Functions

How to use the OCI Python SDK to make API Calls

Access OCI resources from within a Function

OCI Python SDK Documentation

OCI Events description

OCI Functions video

Cloud Events

Solution on Github

Ionut Adrian Vladu

I enjoy building python scripts for…everything! I am a Cloud enthusiast and I like to keep up with technology. When I'm not behind a computer, I like taking photos -- Visit My 500px profile -- or sit back and enjoy Formula 1 race weekends. Currently, working as a Tech Cloud Specialist @ Oracle
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments