Skip to content

exratione/lambda-complex

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Lambda Complex

Lambda Complex is a Node.js framework for applications that run entirely within Lambda, SQS, and other high abstraction layer AWS services. It is well suited to non-realtime content generation and other types of application driven by message queues and which require enforced concurrency limits. Examples include high volume generation of static content from data or other types of workflow initiated in response to messages placed into SQS queues.

In a Nutshell

The high points of Lambda Complex:

  • Assemble applications from Node.js Lambda function implementations. Use any NPM module that exports one or more Lambda function handlers, or write your own.
  • Configure the Lambda Complex application to pass data between Lambda function invocations in potentially complicated, result-dependent ways.
  • Each Lambda function in the application can be linked to an SQS queue, and is then invoked in response to messages sent to that queue.
  • Currently this is the only way to send data to a Lambda Complex application.
  • A Lambda complex application is deployed as a CloudFormation stack.

A Lambda Complex application can be deployed from a developer machine, or from an Amazon Linux EC2 instance if binary NPM modules are required. No server infrastructure beyond that is needed.

The typical Lambda Complex application consists of a few small Node.js packages exposing Lambda function handlers, and a configuration file to define necessary details such as permissions to access AWS resources. When deployed, the Lambda functions interact with other AWS services to, for example, generate content in response to SQS messages, or process a workflow in many small steps.

Why Lambda Complex?

The name? Because it involves AWS Lambda and it is a little bit complicated.

Joining together many Lambda functions to form an application might sound a little like reinventing the wheel, given the existence of AWS Simple Workflow Service and other similar frameworks. Why do this, especially given the work needed to factor any complex application into functions that will run within the time, memory, thread, and other constraints of AWS Lambda?

The answer is that this is largely a way to flee from devops requirements in more traditional server deployments. A Lambda Complex application requires no servers and thus no complicated deployment infrastructure. No provisioning scripts, no upgrade treadmill, no version conflicts: this particular combination of AWS services abstracts away all of that. Just design the application, write the code, write the configuration file, run the deployment script, and you are done and deployed, your application running.

Examples

You should find the following examples informative if looking for details, or help in constructing your own application:

How Does Lambda Complex Compare With JAWS?

JAWS is great. Lambda Complex achieves much the same outcome, which is to say constructing an application that resides entirely in AWS Lambda and related AWS services. The two are different in near every design choice at the detail level, however.

At the present time Lambda Complex is more suited to:

  • Slower, non-time-critical applications, such as static content generation.
  • Applications that make use of existing third party Node.js Lambda functions.
  • Applications accepting data via SQS queues.
  • People who like monitoring application state via SQS queue attributes.
  • People who don't mind a lack of structure in their frameworks.

JAWS is more suited to:

  • Faster applications, such as APIs.
  • Applications that must use non-Node.js Lambda functions.
  • Applications accepting data through the standard Lambda function channels.
  • People who like a little more structure in their frameworks.

Basic Concepts

Lambda Complex overview

Application Configuration File

A Lambda Complex application is defined by a configuration file that specifies the AWS details, the components and their Lambda function packages, the IAM roles to grant access to AWS resources, and other necessary items.

Component

A Lambda Complex component consists of:

  • A Lambda function implementation.
  • An SQS queue to measure concurrency.
  • A method of passing data to the Lambda function data, such as an SQS queue.
  • The component definition in the Lambda Complex application configuration file.

The following types of component exist:

Event From Message

The component is bound to a specific queue, and when invoked will consume a message from that queue to provide the event data passed to the handler.

This type of component has a specified concurrency, and is limited to that number of concurrent invocations when the queue is populated. The coordinator periodically invokes instances of the component Lambda function when the queue has visible messages.

If the component fails in its operation, the message remains in the queue and will be retried. Therefore this type of Lambda function can be written to fail fast and with no intricate error handling.

Event From Invocation

The component is a normal Lambda function, and event data is passed to the handler in the normal way. The component Lambda function is invoked by other component Lambda functions directly, without intervention of the coordinator.

The advantage of this is speed, not having to wait for a queue check, while the disadvantage that this component must be absolutely bulletproof or data and operations will be lost when it fails.

Internal

Internal components are built-in Lambda functions that manage the operation of a Lambda Complex application, such as responding to queues backlogs.

Lambda Complex Code Wraps Provided Lambda Function Implementations

Lambda Complex applications can use arbitrary third party Lambda function handler implementations, so long as they are Node.js and have a package.json file, because during deployment those implementations are wrapped in Lambda Complex code.

When the component Lambda function is invoked in a deployed Lambda Complex application, Lambda Complex code runs first to take care of the necessary details - such as retrieving data from an SQS queue, tracking concurrency, and so forth. Only then does it pass control to the underlying Lambda function handler.

Coordinator

The coordinator is a built-in Lambda Complex Lambda function that checks the state of the running application and invokes Lambda functions in response to circumstances. E.g. on the arrival of new messages in SQS queues.

Invoker

The invoker is a built-in Lambda Complex Lambda function that is used to invoke large numbers of other Lambda functions. API calls take time, and a Lambda function instance is both time-limited and thread-limited. Therefore invoking a very large number of Lambda function instances requires the creation of additional invoker instances to carry out that work.

Concurrency Queues

Each component is associated with an SQS queue used to measure the number of concurrent invocations of the component Lambda function. When the component Lambda function is invoked, a message is posted to the queue. That message is deleted on completion of the Lambda function, or expires after the Lambda function timeout.

Note that queue messages cannot expire more rapidly than 60 seconds, and Lambda functions can have shorter timeouts. This shouldn't be too much of an issue since Lambda Complex reactions to uncaught exceptions to take the necessary actions - it is fairly hard to cause the concurrency message deletion to fail to take place.

ARN Map

During deployment of the CloudFormation stack for a Lambda Complex application, a JSON map of the relevant ARNs for queues and Lambda functions is created and uploaded to S3. This file is necessary for the Lambda Complex framework code to function, and is loaded by every invoked Lambda function before they take action.

Creating a Lambda Complex Application

Design the Application

Break up your application functionality into small chunks that can run in parallel, and which individually cannot last longer than the Lambda time limit (currently 300 seconds). Each of these is a component. Identify the data that passes from component to component, and points at which flow control decisions must be made: route to component A versus component B based on the data.

Not all applications are a good fit for Lambda Complex, and it is worth taking a little time to figure out whether or not this is a good plan for any specific use case.

For each component you will need to write or use a third party Lambda function provided in an NPM package. Plan for a component to accept all of its run-time configuration options, such as an environment specifier, resource names, and so on, via the event passed as an argument to the Lambda function handler.

Create the Application Configuration File

The example configuration file provides a good starting point, as it contains examples of nearly all that is needed to specify much larger applications.

Specify Deployment Details

Specify the necessary deployment details:

{
  name: 'simple',
  version: '0.1.0',
  // Add
  deployId: Math.round((new Date()).getTime() / 1000),

  deployment: {
    region: 'us-east-1',
    s3Bucket: 'lambda-complex',
    s3KeyPrefix: 'applications/',
    // No additional tags.
    tags: {},
    switchoverFunction: function (stackDescription, config, callback) {
      callback();
    }
  },

The most important and potentially most complex item here is the switchoverFunction. This is invoked after deployment of a new version of the application, but prior to deletion of the previous version of the application. It should be used to make any changes needed to switch resources to use the new applications.

In a Lambda Complex application, this means the SQS queues that will be used to introduce data into the application: the new queues will have different identities, and the applications sending to them should be updated.

Configure the Coordinator Behavior

The coordinator configuration is as important as the concurrency property of components when tailoring an application to work within the Lambda limits placed on an AWS account. See the example configuration for an explanation of these properties, and note that it is perfectly possible to set values that are too high for a coordinator invocation to complete without error.

  coordinator: {
    coordinatorConcurrency: 1,
    maxApiConcurrency: 10,
    maxInvocationCount: 20,
    minInterval: 10
  },

Add Component and Role Definitions

For each component identified in the design, create a definition in the components array. Most components will be of the eventFromMessage type and this associated with an SQS queue.

Each component must have an associated role, applied to its Lambda function. Add these to the roles array.

  roles: [
    {
      name: 'default',
      // No extra statements beyond those added by default to access queues.
      statements: []
    }
  ],

  components: [
    {
      name: 'invokedProcessor',
      type: 'eventFromInvocation',
      // Since this defines no routing, this is a dead end: events are delivered
      // here and no further processing results.
      // routing: undefined,
      lambda: {
        npmPackage: path.join(__dirname, 'processor'),
        handler: 'index.handler',
        memorySize: 128,
        timeout: 60,
        role: 'default'
      }
    }
  ],

For most components the routing property is important: it determines which other components accept the output of this component as their input. This can be a component name, array of component names, or a function:

    routing: 'aComponent',

    ...

    routing: ['component1', 'component2'],

    ...

    routing: function (error, data) {
      // This is safe provided that the Lambda function for this component
      // includes the underscore module.
      var _ = require('underscore');

      // Don't send on any data if an error resulted.
      if (error) {
        return [];
      }

      // Otherwise split up or manipulate data and send it to other
      // components as desired.
      return [
        { name: 'componentA', data: _.keys(data) },
        { name: 'componentB', data: _.values(data) }
      ];
    },

Write the Lambda Functions

A Lambda function for use in a Lambda Complex application is written in the normal way. The result it returns to context.succeed or context.done may be routed to one or more other components depending on definitions in the application configuration file.

Thus any suitable NPM module that provides exported handle functions that conform to the Lambda specification can be used. As a reminder, these functions must be of the form:

exports.fn = function (event, context) {
  var result = {
    name: 'value'
  };

  context.succeed(result);
}

A function must invoke the context methods on completion to provide data that can be passed on to other components in the application.

Ensure Compatibility with the Supported Node.js Version

AWS Lambda runs under a specific version of Node.js. Set up development and testing to ensure that your application can function in that environment.

Set up AWS Credentials

For deployment to work, suitable AWS credentials for the account specified in the configuration file must be present. The credentials must at a minimum allow full interaction with CloudFormation stacks, the S3 bucket and key prefix specified in configuration, CloudWatch logs, Lambda functions, SQS queues, and roles.

To make credentials available for Lambda Complex, either create a credentials file in the standard location or set environment variables to hold the key and secret key. Both options are described in the AWS SDK documentation.

Unlike many Node.js AWS modules, Lambda Complex does not accept IAM access keys directly in configuration. It is bad practice to specify access keys in code or configuration for code; you should always add them to the environment in one of the ways noted above.

Working With a Lambda Complex Application

Grunt Tasks

The lambda-complex module exports the following Grunt tasks, which can be loaded via grunt.loadNpmTasks('lambda-complex') in modules that load lambda-complex as a dependency:

  • lambda-complex-build - build a specified application.
  • lambda-complex-deploy - build and deploy a specified application.

The usage of the tasks is illustrated below; both require the --config-path option to be specified.

Deploy the Application

Once the Javascript configuration file is ready, deploy the application via a Grunt task. Note that the config-path option can be set to a relative or absolute path:

grunt lambda-complex-deploy --config-path=/path/to/applicationConfig.js

Deployment uses the cloudformation-deploy NPM module, so you may want to take a look at its documentation. In short, the following occurs:

  • Request stack creation.
  • Wait on the stack status to show show success.
  • Run the provided config.deployment.switchoverFunction.
  • Delete any previous instances of the stack once the switchover is done.

Programmatic Interface for Deployment

Deployment can be managed programmatically:

var lambdaComplex = require('lambda-complex');
var config = require('/path/to/applicationConfig');

lambdaComplex.deploy(config, function (error, results) {
  if (error) {
    console.error(error);
  }

  console.info(results);
});

Binary NPM Module Limitations

Some NPM modules build local binaries on installation. If any of the component Lambda function implementations require binary modules, then the Lambda Complex application must be deployed from an Amazon Linux EC2 instance to ensure that the environments match.

Building the Application Without Deployment

It is possible to build the application locally without deploying it, which can be useful during development. This will result in the creation of the zipped Lambda function packages and CloudFormation template only.

grunt lambda-complex-build --config-path=/path/to/applicationConfig.js

Programmatic Interface for Building

Building without deployment can be managed programmatically:

var lambdaComplex = require('lambda-complex');
var config = require('/path/to/applicationConfig');

lambdaComplex.build(config, function (error, results) {
  if (error) {
    console.error(error);
  }

  console.info(results);
});

Shut Down an Application

Since the application includes self-invoking Lambda functions, the only sure way to shut it down at the present time is to delete the CloudFormation stack. Deleting the ARN map file uploaded to S3 during application deployment should also shut things down, as without the list of ARNs for application resources nothing in Lambda Complex can work.

Update a Deployed Application

To update the deployed application, simply deploy again. A new stack will be created and the old stack destroyed when (a) creation is complete and (b) the config.deployment.switchoverFunction calls back.

If you are using SQS queues to pass data into the application, note that this will require you to update the queue names. This should be accomplished in the switchoverFunction defined in the application configuration, which will be invoked after creation is successful but before deletion of the prior application stack.

Monitor a Running Application

Lambda functions write to CloudWatch Logs and other attributes such as number of invocations and errors can be monitored via the CloudWatch API. The component SQS queues can also be monitored via CloudWatch, and the concurrency SQS queues are especially useful when monitoring application state.

The coordinator SQS concurrency queue should never be empty, for example, as this indicates that the application has failed and is no longer running. Component queues used to introduce data to the application would be backing up at this point as well.

A number of third party services can be used to build monitors and alerts based on these logs and metrics.

A Speculative Roadmap

Lambda Complex remains in an early stage of development. Moving forward the following are intended.

Switch Coordinators to Use Scheduled Events

When scheduled events for Lambda functions can be set up via CloudFormation, switch the coordinator instances to run using this functionality rather than via self-invocation.

Better Support for Switchover of SQS Queues

The process of switching delivery of data to new queues on each new deployment of an existing application is painful, and what happens to the messages in the old queues? They will get dropped as it stands. Helpers to make it easier to build a suitable switchover function would be useful.

Monitoring and Log Streaming

A robust monitoring and log streaming module is needed, built on the CloudWatch APIs. A programmatic interface to how the applications is working (or not working) is a necessary part of a number of further refinements.

More Component Types

Add further component types based on other ways to trigger Lambda functions from AWS resources. E.g. from S3 events or SNS topics.

Application Testbed

Providing the ability to run an application locally.

Stress Testing

Tools to stress test a local or deployed application.

About

Build and deploy Node.js applications constructed from AWS Lambda functions.

Resources

License

Stars

Watchers

Forks

Packages

No packages published