DEV Community

Cover image for "Do you do Lambda Spaghetti?"
Gernot Glawe for AWS Community Builders

Posted on • Updated on • Originally published at aws-blog.de

"Do you do Lambda Spaghetti?"

Last week in the AWS slack developer channel once again, somebody was asking: "How can I run a Lambda locally?". Well, that is a valid question, but there is a chance that you only think you need a local Lambda emulator because you do Lambda Spaghetti! Spaghetti code is a phrase for unstructured and difficult-to-maintain source code. I show you an easier way to test Lambdas locally and have some arguments that a local Lambda runtime should only be the very last resort. On top, you get examples in Pasta-Python, Gemelli-Go and Tortellini-Typescript.

The problem of spaghetti Lambda

Overcooking pasta gives you sticky and clumpy pasta. Running Lambda functions as a whole gives you a monolith.

If you write Lambda code that is longer than approximately three logical steps, it should not be coded in a single function! This is monolithic programming on the code level, and the parts should be separated. I show you why:

Monlith Test

Have a look at the diagram of this mini-monolith.

You can only test all functions together, so they are tightly coupled. If you do testing of the whole block (the lambda handler) and get an error, you do not know in which part of the application the error occurs. In Addition, you need the emulation of the Lambda compute resource. This resource could be emulated locally with lambci or sam-cli. But this often involves running a docker container locally, which introduces additional time and consume additional local computing resources.

The first part of the solution is to run Lambda functions without the Lambda resources.

To have a fast and easy test, you directly invoke the code without any Lambda resources/environment.

Lambdaless Python

We have this small Lambda python function:

import json

def lambda_handler(event, context):
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
Enter fullscreen mode Exit fullscreen mode

To call it and get the return values, you just need a tiny script "call.py"

import index

ret = index.lambda_handler("","")
print(ret)
Enter fullscreen mode Exit fullscreen mode

Run it and you get:

python call.py
{'statusCode': 200, 'body': '"Hello from Lambda!"'}
Enter fullscreen mode Exit fullscreen mode

That's all, no containers needed!

That is the same approach as the "serverless framework" does with serverless invoke local.

Lambdaless JavaScript

exports.handler = async (event) => {
    // TODO implement
    const response = {
        statusCode: 200,
        body: JSON.stringify(event.key1),
    };

    console.log(extract.extract(event))

    return response;
};
Enter fullscreen mode Exit fullscreen mode

This is the Lambda handler.

'use strict';

const fs = require('fs');
const lambda = require('./lambda')

let rawdata = fs.readFileSync('event.json');
let event= JSON.parse(rawdata);
lambda.handler(event);
Enter fullscreen mode Exit fullscreen mode

You can call the handler with this piece of code. If you have the incoming event stored as a file locally, you can give it as a parameter directly. More on this later.

The second part of the solution is to test methods from the functions independently

Single Tests

Suppose we have a Lambda function that calculates the Body Mass Index. With a pasta Lambda you would write the calculation directly into your handler.

The better way is to write a small module, which you can test independently:

function bmi(age, weight, height) {
  height_m = height/100;
  bmi = weight / (height_m * height_m);
  bmi = Math.round(bmi);
  return bmi;
}
module.exports = bmi;
Enter fullscreen mode Exit fullscreen mode

bmi.js

In your lambda handler you call this method:

const bmi=require('./bmi');

// Handler
exports.handler = async function(event, context) {

  age = event.age;
  weight= event.weight;
  height= event.height;
  return bmi(age, weight, height);
}
Enter fullscreen mode Exit fullscreen mode

index.js

Now you can call and test this bmi function independently, or even better write a unit test:

const bmi = require('./bmi');

test("Bmi Übergewicht", () => {
  expect(bmi(42,113,188)).toBe(32);
});
Enter fullscreen mode Exit fullscreen mode

This example is done with the jest testing framework.

Python Testing

The samecan be achieved with python, e.g. with pytest

GO Testing

For a detailed intro see go-on-aws

How to get real test data?

To test a method, you have to give some test parameters to the function. The Lambda functions on AWS will be invoked with a JSON event. How to get this data?

Get the json event

The trick is to write a dump function at first and save the data like this:

exports.handler = async (event) => {
    console.log(JSON.stringify(event))
    const response = {
        statusCode: 200,
        body: JSON.stringify('Hello from Lambda '),
    };
    return response;
};
Enter fullscreen mode Exit fullscreen mode

Get testdata from AWS API calls

If it's an AWS call, you can just call the API with the AWS CLI and save the JSON as a file.
E.g. if you want to have the describe instances API call:

aws ec2 describe-instances >event.json
Enter fullscreen mode Exit fullscreen mode

Testlevels

Your test concept should ensure that you test specific functionality without side effects. And that you test under a near-real production environment.

Running Lambda functions as a whole locally on your workstation does none of these.

Test specific functionality

This is just to test the "does it work".

  • Test single functions
  • Start with coded tests, consider using a test framework after some time

Test Lambda as a whole

This is to test "does everything work together" and "does it work in the real environment".

  • Invoke Lambda on AWS
  • Test the Lambda IAM rights also
  • Have production latency and timing
  • Have limits for accessing disc (/tmp space)
  • Have limits for CPU cores
  • Have limits for memory

Summary

To begin non-monolith or non-spaghetti programming, you don't have to start with unit tests and a test framework right away if you want to avoid the learning curve. You just refactor your methods from the lambda handler and call them one by one. This way, you will write better code!

I hope you got some appetizer for doing unit testing instead of running your whole lambda locally! So follow me on Twitter and visit my GO on AWS site if you haven't done already :) .

Feedback & discussion

For discussion please contact me on twitter @megaproaktiv

Learn more GO

Want to know more about using GOLANG on AWS? - Learn GO on AWS: here

Top comments (3)

Collapse
 
dvddpl profile image
Davide de Paolis

this is such a good advice!
Never been a fan of those local emulators.. Lambda is just some node/javascript/typescript ( or whatever language you are using ).

If you structure properly your code, you can test it easily without all the context.
Just have another tiny file that you use as a runner, and thats's it.
If you want to test the real thing, then deploy and test it for real, because the emulator will not give you the real thing and the real behaviour ( coldstarts, containers, scalability etc)

Just a note about mocking/creating a sample event:
depending what your lambda trigger is: DynamoStream, SQS or APIGateway you can generate the event using the CLI and just apply your specific payload.
GenerateEvent with Serverless
GenerateEvent with AWS SAM

Thanks for this article!

Collapse
 
zigrazor profile image
ZigRazor

Great Post!!

Collapse
 
aditmodi profile image
Adit Modi

Great Blog Post πŸ‘ πŸ‘
can I have some spaghetti πŸ˜‹