λ 🌉The missing bridge from SQS to Lambda 🎆
Switch branches/tags
Clone or download
jelder Merge pull request #9 from Blissfully/fifo_safety
Force all FIFO queues to use batchSize = 1 until real support arrives
Latest commit 37e62e6 Oct 25, 2018
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.circleci Configure CircleCI Mar 8, 2018
iam
src Force all FIFO queues to use batchSize = 1 until real support arrives Oct 25, 2018
.eslintrc.yml
.gitignore First public release Mar 8, 2018
Dockerfile Use Node 8 Apr 18, 2018
LICENSE.txt Release under MIT license Mar 8, 2018
README.md Update README.md Sep 18, 2018
index.js Force all FIFO queues to use batchSize = 1 until real support arrives Oct 25, 2018
package.json Fix autoformatting Oct 25, 2018
yarn.lock

README.md

SQS Lambda Bridge

Invoke AWS Lambda functions from SQS queues while adhering to strict concurrency limits.

Originally and until recently, there was official way to dispatch Lambda events from SQS. SQS Lambda Bridge provides a way to do this without any dependencies on DynamoDB or other persistence layer. It easily manages a high volume of invocations on the smallest available Fargate task size (less than $15/month). There is no inherent limit on how long invocations can take (even if the 5 minute limit is extended by Amazon). It will never perform more concurrent invocations than you configure, and will stay at that limit as long as there are messages in the queues.

While AWS has since added official support for SQS events, it doesn't support FIFO queues, and isn't ideally suited to the kind of scarce-resource-protection role that necessitated the creation of this project.

Use

Instead of triggering your Lambda function with lambda.invoke, use sqs.sendMessage or sqs.sendMessageBatch. Specify the name of the Lambda function via the message attribute FunctionName. The function payload should be the message body. Both are JSON encoded.

const AWS = require("aws-sdk")
const sqs = new AWS.SQS()

const functionName = "my-function" // Or the fully-qualified ARN of your function
const payload = { widgetId: 1234 }

sqs
  .sendMessage({
    QueueUrl: `https://sqs.us-east-1.amazonaws.com/1234/my-queue`,
    MessageBody: JSON.stringify(payload),
    MessageAttributes: {
      FunctionName: {
        DataType: "String",
        StringValue: functionName,
      },
    },
  })
  .promise()

If your Lambda returns a successful response, the message will be removed from the queue. Otherwise, it will be retried until it expires, subject to any redrive policy on that queue.

A Dockerfile is provided to simplify deployment, but anywhere you can run npm start is sufficient. See Installation for details.

Configuration

Create an SQS queue and add a tag called sqsLambdaBridge. All queues with this tag will be polled continuously. Additional options are supported via tags:

Tag Name Default
sqsLambdaBridge false If this isn't set, the queue is ignored.
concurrency 1 Max 1000 total across all lambdas, but you can ask AWS for more.
batchSize 10 Max is 10. Low values mean better utilization but more API traffic.

Note: The default visibility timeout for these queues must be at least as long as the longest timeout for the functions in that queue. If in doubt, 310 seconds is a reasonable value.

Note: You should configure a dead letter queue, or else invocations which fail will be retried immediately and continue until they succeed or expire from the queue.

Designing your queues

For the highest throughput, you'd want to have only one queue, with lots of workers pulling jobs from it. There are a few general rules about when it makes sense to split your work into different queues.

1. Priority

If all jobs are otherwise equal, but some need to be completed relatively sooner than others, create a dedicated queue for time-critical work and add to it sparingly. You may create separate queues for different classes of customers: free users vs. paying customers, or you may have separate queues for batch vs. interactive uses.

2. Different jobs consume different resources

With Lambda we have amazing scalability, right? But not all functions are stateless. A function may make a connection to RDS, which has a limit of 1000 concurrent connections, or it could hit a 3rd party API that has strict rate limits. Each function should be sent to a queue named for the most scarce resource which it consumes.

Example:

  • Lambda for stateless stuff that is only limited by your account's Lambda concurrency.
  • Aurora for functions which make a connection to the database. Set the concurrency tag to something conservative with respect to your Aurora connection limit (max 1000 probably).
  • Contentful where Contentful is a 3rd party API that you can't hit too often.

3. Purging

There is exactly one O(1) operation in SQS and that is purgeQueue. If there's a class of function invocation that we may need to cancel, dedicate a queue to to it. All SQS messages are immutable. Selectively deleting messages from the queue requires that you first recieve that message, making it invisible to other clients, and incrementing its recieve count, which may move it closer to the dead-letter queue, before you can delete it. In-flight messages are also invisible, so you won't necessarily find everything on the first pass.

4. Queue-level features.

For example, normal queues can't do deduplication or guarantee ordering, and FIFO queues can't do delays on individual messages. If you need mutually exclusive features, make more queues.

Installation

In this tutorial, we'll be deploying to Amazon Fargate, but anywhere you can run npm start should work.

Before we begin, you should have installed and configured the official AWS CLI. You should be able to run e.g. aws sqs list-queues without error. We will also be using the unofficial Fargate CLI. This will automatically use the same credentials as the official CLI. Download that and make sure that the executable file is in your $PATH.

First, create an IAM role which can enumerate your SQS queues and their tags, and invoke your Lambda functions.

aws iam create-role \
  --role-name sqs-lambda-bridge \
  --assume-role-policy-document file://iam/role.json
  
aws iam put-role-policy \
  --role-name sqs-lambda-bridge \
  --policy-name sqs-lambda-bridge \
  --policy-document file://iam/role-policy.json

Next, create a cluster. It takes a few minutes to provision and start the new container.

fargate service create --task-role sqs-lambda-bridge sqs-lambda-bridge

You can view logs in real time.

fargate service logs sqs-lambda-bridge -f

Deploying a new version is also trivial (and also takes a few minutes).

fargate service deploy sqs-lambda-bridge

The service only checks its configuration once, at startup. If you add or remove queues, or change their config tags, you'll need to restart it.

fargate service restart sqs-lambda-bridge

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/Blissfully/sqs-lambda-bridge. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the Contributor Covenant code of conduct.

License

The project is available as open source under the terms of the MIT License.