Lambda functions on AWS are growing very popular today to perform a lot of computing on demand and also to build entire applications based on the serverless architecture. The biggest advantage lambda functions have is that they do not cost when they are idle and they can be invoked from almost anywhere. Also they can scale on demand.

One challenge faced while developing lambda functions is the managing of code that is not part of your  core business logic. Lets suppose you are developing an application that interacts with the database. With every lambda function you also have to add other helper functions for interacting with the database besides your business logic function handler. If we could separate out the code and management for the database interacting layer, our lambda function would become more leaner and easier to manage. 

One more advantage that we would have is that we could share the same code across multiple lambda functions. This also makes our lambda function align to the separation of concerns philosophy and the single responsibility principle.

AWS Lambda layers are used to provide for exactly this purpose. Using Lambda layers, we can create one or more layers of code. And then we reference these layers in our lambda functions. The code in these layers are totally independent of your lambda function. This means that the layer code can be updated as long as the interface between your function and the underlying layer of functions stays consistent. One more thing about the lambda layers is that they are versioned. So in case if the interface between the function and underlying lambda layer needs to change, the underlying layer can change without any worries since your lambda function still references the older version of the underlying lambda layer. A Lambda function can have up to 5 layers. You can also store third party libraries inside a lambda layer. 

Here we are going to do a small demo in NodeJS on the usage of lambda layers. We would be creating two layers here -:

  1. The first layer would be a database layer that would be inserting data into the database. This would not be a real database here though. We would be inserting data into an array. We are creating this layer since we do not want to interact with a real database in this small tutorial.
  2. The second layer would be a DAO layer. A DAO (Data Access Object) layer is an abstraction layer that lies between the business logic and the database. A DAO layer is important so that the business logic does not have to directly deal with the database. It abstracts the database from the business logic. 

And then we would write a lambda function for our business logic that would reference these layers and perform various CRUD operations. 

So lets get started.

The first thing we would do is create a file named databaseLayer.js

 databaseLayer.js

let data = {};
data.cars = [];
data.persons = [];

let itemId = 0;

let dbOperations = {};

dbOperations.insert = (tableName,item) => {
    item.id = itemId++;
    data[tableName].push(item);
    return item;
}

dbOperations.get =  (tableName,name) => {
    let item = data[tableName].find( element => {
        return element.name == name
    }) 
    if (!item) {
        return null;
    } else {
        return item;
    }
}

dbOperations.list = (tableName) => {
    return data[tableName];
}

dbOperations.update = (tableName,updatedItem) => {    
    let index = data[tableName].findIndex( element => {
        return element.id == updatedItem.id
    })
    if (index != -1 ) {
        data[tableName][index] = updatedItem;
        return data[tableName][index];
    } else {
        return null;
    }
}

dbOperations.delete = (tableName,id) => {
    let index = data[tableName].findIndex( element => {
        return element.id == id
    });
    if (index != -1) {
        let item = data[tableName][index]
        data[tableName].splice(index,1)
        return item;
    } else {
        return null;
    }
}

module.exports = dbOperations;

In this file, we have created a simple database that inserts and removes data from an array. It expects the tablename and other details.

For example, the insert function expects the tablename and the object to insert. In reality we would not be having this layer. This would be our actual database.

For now we have created two tables, one is a persons table and the other is a car table.

The next file that we would be creating is the personsDao.js. This layer is the abstraction layer between the database and the business logic for inserting a person object into the database. In reality this layer would deal directly with the database rather than the above database layer.

personsDao.js

var db = require('./databaseLayer');

var personsDao = {};

var tableName = 'persons';

personsDao.insertPerson = (personObject) => {
    return db.insert(tableName, personObject);
}

personsDao.getPersonByName = (name) => {
    return db.get(tableName, name)
}

personsDao.getAllPersons = () => {
    return db.list(tableName);
}

personsDao.updatePerson = (personObject) => {
    return db.update(tableName, personObject);
}

personsDao.deletePersonById = (id) => {
    return db.delete(tableName, id);
}

module.exports = personsDao;

We would be creating a similar DAO layer for the car object.

carsDao.js

var db = require('./databaseLayer');

var carsDao = {};

var tableName = 'cars';

carsDao.insertCar = (carObject) => {
    return db.insert(tableName, carObject);
}

carsDao.getCarByName = (name) => {
    return db.get(tableName, name)
}

carsDao.getAllCars = () => {
    return db.list(tableName);
}

carsDao.updateCar = (carObject) => {
    return db.update(tableName, carObject);
}

carsDao.deleteCarById = (id) => {
    return db.delete(tableName, id);
}

module.exports = carsDao;

Now we would create our business function that we would be interacting with the DAO layer.

index.js

var personsDao = require('/opt/personsDao')
var carsDao = require('/opt/carsDao')


exports.handler = async (event, context) => {

    var personObject = {
        name: 'Steve Jobs',
        age: 64,
        city: 'SF'
    }
    
    var carObject = {
        name : 'Corolla',
        brand: 'Toyota',
        price: '65,000'
    }
    
    //insert one person
    var insertedPerson = personsDao.insertPerson(personObject);
    console.log("Inserted Person " + JSON.stringify(insertedPerson));
    
    //insert one car
    var insertedCar = carsDao.insertCar(carObject);
    console.log("Inserted Car " + JSON.stringify(insertedCar));
    
    //get person 
    var fetchedPerson =  personsDao.getPersonByName('Steve Jobs')
    console.log("Fetched Person: " + JSON.stringify(fetchedPerson));
    
    //get car 
    var fetchedCar =  carsDao.getCarByName('Corolla')
    console.log("Fetched Car: " + JSON.stringify(fetchedCar));
    
    //update person
    fetchedPerson.age = 67
    var updatedPerson = personsDao.updatePerson(fetchedPerson);
    console.log("Updated Person: " + JSON.stringify(updatedPerson));
    
    //update car
    fetchedCar.price = '80,000'
    var updateCar = carsDao.updateCar(fetchedCar);
    console.log("Updated Car: " + JSON.stringify(updateCar));
    
    //list people
    var listPeople = personsDao.getAllPersons();
    console.log("List of People: " + JSON.stringify(listPeople));
    
    //list car
    var listCar = carsDao.getAllCars();
    console.log("List of Cars: " + JSON.stringify(listCar));
    
    //delete person
    var deletedPerson = personsDao.deletePersonById(fetchedPerson.id);
    console.log("Deleted Person: " + JSON.stringify(deletedPerson));
    
    //delete car
    var deletedCar = carsDao.deleteCarById(fetchedCar.id);
    console.log("Deleted Car: " + JSON.stringify(deletedCar));
    
}

In the above function we are importing the DAO from the /opt directory. AWS Lambda places all the layers in the opt directory and we can import it from there.

The function performs various operations on the DAO layer like inserting, listing, deleting and removing car and person objects.

Now that we have completed our function and layers lets get ready to start uploading them to AWS Lambda.

Prepare three zip files

  1. databaseLayer.zip – This contains the databaseLayer.js
  2. daoLayer.zip – This contains the personsDao.js and carsDao.js
  3. functionOne.zip – This contains index.js ( our business logic)
zip databaseLayer.zip databaseLayer.js
zip daolayer.zip carsDao.js personsDao.js
zip functionOne.zip index.js

Now we have to create a role that will be attached to the lambda function. This role will allow the lambda to insert logs into cloudwatch. It is necessary to create this role, we cannot proceed without creating and attaching a role to the lambda function.

Create a file containing the trust policy. A trust policy contains information about who is allowed to assume the role.

role-trust-policy.json

{
    "Version": "2012-10-17", 
    "Statement": [
        {
            "Action": "sts:AssumeRole", 
            "Principal": {
                "Service": "lambda.amazonaws.com"
            }, 
            "Effect": "Allow", 
            "Sid": ""
        }
    ]
}

As you can see in the above trust policy, only lambda functions are allowed to assume this role.

Once the role-policy document is created, we create the role in AWS using following command.

 aws iam create-role --role-name lambda_execution_role --assume-role-policy-document file://role-trust-policy.json

Here we have created a role with the name lambda_execution_role with the trust policy. Record the Arn of the role. We will need it later.

After this we attach a policy to the role. This policy will allow the lambda functions to insert logs into cloudwatch.

 aws iam attach-role-policy --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole --role-name lambda_execution_role

The policy whose Arn is provided above is an AWS managed policy. This means that the policy already has been created by AWS and exists.

After creating the IAM role, the next step is to create the different layers.

aws lambda publish-layer-version --layer-name daoLayer --zip-file fileb://daoLayer.zip
aws lambda publish-layer-version --layer-name databaseLayer --zip-file fileb://databaseLayer.zip

If these commands fail, you need to update your aws cli to the latest version.

Running these two commands should return an Arn each. Copy these two arns.

Now moving on we create our lambda function using the following command. Replace the role with your actual role Arn that you had got after creating the role.

aws lambda create-function --function-name functionOne --zip-file fileb://functionOne.zip --handler index.handler --runtime nodejs8.10 --role arn:aws:iam::123456789012:role/lambda_execution_role

After creating our lambda function, we associate the layers with the lambda using the following command. Replace the layer Arn below with your actual layer Arns

aws lambda update-function-configuration --function-name functionOne --layers arn:aws:lambda:eu-west-3:123456789012:layer:daoLayer:1 arn:aws:lambda:eu-west-3:123456789012:layer:databaseLayer:1

Now our function is ready to be invoked.

Go to AWS Lambda console and look for functionOne. Open the function. To trigger the lambda function, create a test event. Click on configure test event and create an event with the name testevent. Let the event be the default existing event. And then click on test. You should see some output in the logs below.

When you click on layers under the function name, you should see the referenced layers and the version that has been referenced.

I hope this tutorial has helped you understand the concept of layers in lambda functions.


Leave a Reply

Your email address will not be published. Required fields are marked *