The Software Development Lifecycle of Lambda Functions - TriNimbus

The Software Development Lifecycle of Lambda Functions

Whether you treat Lambda as the connective tissue for your EC2-centric AWS environment, or are deploying fully-serverless infrastructure, you’ll want to have a good methodology in place for developing, testing, building, and deploying your Lambda functions. Lambda itself is not at all prescriptive in this regard; it ultimately expects you to deliver a zipped code payload, but everything up until that point is up to you.

Crucially, since Lambda functions are a software product, it is important to apply our usual software engineering principles and practices to their development. We want to be able to develop locally, have good unit test coverage within a modular design, manage internal & external dependencies, create builds, and deploy to multiple environments. In other words, Lambda may have a novel execution model, but it should nevertheless conform to existing engineering processes. Fortunately, this is quite easy to achieve with a set of project structure conventions, and a trusty utility: Unix make.

We’ve put together a sample repo detailing a set of conventions that achieves these objectives.

Project Structure

Rather than a single monolithic file of Lambda code, as the code textbox on the Lambda console suggests, it makes sense to structure the Lambda function as an entry-point with a package:

.
├── Makefile                           # Definition of `make` targets.
├── builds                             # Builds directory.
│   ├── deploy-2016-08-15_16-50.zip
│   └── deploy-2016-08-15_16-54.zip
├── index.py                           # Entry point for the Lambda function.
├── lambda_package                     # Python package `lambda_package`.
│   ├── __init__.py
│   ├── localcontext.py
│   ├── utility.py
├── requirements                       # External dependencies.
│   ├── common.txt
│   ├── dev.txt
│   └── lambda.txt
└── tests                              # Unit tests for the package.
    ├── __init__.py
    └── lambda_package
        ├── __init__.py
        ├── test_localcontext.py
        └── test_utility.py

Local Development

With our make targets in place, we can easily develop the Lambda code against a local Python runtime; this vastly shortens the development feedback loop.

# Install development dependencies (e.g. unit testing framework).
make init

# Invoke the function locally.
make invoke

# Run the unit test suite.
make test

We can even simulate the AWS Lambda context object by passing in a LocalContext object:

def handler(event, context):
    """Entry point for the Lambda function."""
    # Lambda starts here.

if __name__ == '__main__':
    from lambda_package.localcontext import LocalContext
    handler(None, LocalContext())

Finally, if our Lambda function has complex interactions with other AWS resources, we can run it under an IAM profile with identical permissions to the runtime Lambda role. In short, local development of Lambda functions need not be any different than any other class of software that your organization produces.

Build & Deploy

When it’s time to build and deploy, we can turn again to make:

# Gathers runtime dependencies and our custom code into a single zip archive:
# e.g. builds/deploy-2016-08-15_16-50.zip
make build

# Deploys to the specified Lambda ARN:
export ARN=arn:aws:lambda:us-west-2:111111111111:function:my-function-name 
make deploy

With these CI/CD primitives, the possibilities are endless: multiple environment deploys, blue-green deployment, orchestration via a build system such as TravisCI (as shown in the sample repo), etc. With Lambda, you can craft the precise DevOps workflow that suits your organizational and environmental needs, without ever needing to relax the foundational engineering principles you use to build all other software.