Kick off async lambda processing from API Gateway

Asynchronous lambda invocation allows for a quick response back (e.g. 202 Accepted) while allowing the lambda to continue running. This would be desired if you wish to kick off a lambda that runs longer then the API GW 30 second timeout, or if the application needs a quick response without the need for direct success for failure of the longer running lambda.

At the time of this post this feature is not natively supported by serverless framework, but it looks trivial to set it up via a resource override. More information at https://github.com/serverless/serverless/issues/4862

Purchase my Serverless book at https://leanpub.com/serverless

VSCode live sharing rules for pair programming

https://visualstudio.microsoft.com/services/live-share/

If you're a fan of pair programming like I am and you haven't tried VSCode's live sharing functionality, then prepare to be delighted and amazed. It's breathtaking. You get multi-cursor support, and seamless sharing of local servers and terminals. Totally blew me away, and we will be using it for sure at Kickass Partners.

Dynamodb ttl expiration is not minute precise

Finally figured out why some of my expired items fail to delete when expected. (bold mine)

DynamoDB typically deletes expired items within 48 hours of expiration. The exact duration within which an item truly gets deleted after expiration is specific to the nature of the workload and the size of the table. Items that have expired and not been deleted will still show up in reads, queries, and scans. These items can still be updated and successful updates to change or remove the expiration attribute will be honored.

Bottom line: you can't rely on DynamoDB's ttl column based expiration to do minute-precise expiration of things in your serverless app. Luckily for my purposes, I don't need minute precision. I do however need hour precision, so I'm planning to write a little scheduled lambda that kicks off every sixty minutes and reaps whatever records have expired but not deleted yet, and delete them manually.


Purchase my Serverless book at https://leanpub.com/serverless

Limitations of Dynamodb KeyConditionExpression

Dynamodb has a query system with a particularly hard learning curve (in my opinion.) Even though I've been using it for years now, I still run into some sticky problems like this one today when I tried to use an IN operator in a query expression. Should be fine, right?

    const key = {
      IndexName: 'by_label_and_status',
      KeyConditionExpression:
        'labelId = :labelId and submissionStatus in (:sent, :opened, :listened)',
      ExpressionAttributeValues: {
        ':labelId': event.pathParameters.labelId,
        ':sent': 'sent',
        ':opened': 'opened',
        ':listened': 'listened',
      },
    };
    const result = await dynamodb.query('submissions', key);

The code above actually fails. Dynamodb is kind enough to report Invalid operator used in KeyConditionExpression: IN, but finding the reason why took me a bit of digging.

It turns out that indeed if you go to the guide for Dynamodb expression syntax, you'll see that the allowed comparators for KeyConditionExpression are limited.

Valid comparisons for the sort key condition are as follows:

sortKeyName = :sortkeyval - true if the sort key value is equal to :sortkeyval.

sortKeyName < :sortkeyval - true if the sort key value is less than :sortkeyval.

sortKeyName <= :sortkeyval - true if the sort key value is less than or equal to :sortkeyval.

sortKeyName > :sortkeyval - true if the sort key value is greater than :sortkeyval.

sortKeyName >= :sortkeyval - true if the sort key value is greater than or equal to :sortkeyval.

sortKeyName BETWEEN :sortkeyval1 AND :sortkeyval2 - true if the sort key value is greater than or equal to :sortkeyval1, and less than or equal to :sortkeyval2.

begins_with ( sortKeyName, :sortkeyval ) - true if the sort key value begins with a particular operand. (You cannot use this function with a sort key that is of type Number.) Note that the function name begins_with is case-sensitive.

Use the ExpressionAttributeValues parameter to replace tokens such as :partitionval and :sortval with actual values at runtime.

Since the system I'm working on is brand new, I was able to get the code example working by changing the values of our statuses. Instead of having completely disparate statuses for sent, clicked, and opened, I changed them to be sent, sent_clicked, and sent_opened. They are semantically correct, since clicked and opened states are also sent. That change allowed us to use begins_with instead of the prohibited IN operator.

    const key = {
      IndexName: 'by_label_and_status',
      KeyConditionExpression:
        'labelId = :labelId and begins_with(submissionStatus, :submissionStatus)',
      ExpressionAttributeValues: {
        ':labelId': event.pathParameters.labelId,
        ':submissionStatus': 'sent',
      },
    };

Purchase my Serverless book at https://leanpub.com/serverless

Careful sending complex objects to lambda callback

Ran into an issue today with the following code. It's the second or third time it's bitten me, and I think this time I finally am learning the general guideline: don't pass complex objects into your lambda success/failure callbacks!

export async function main(event, context, callback) {
  const id = event.pathParameters.id;
  try {
    const result = await startExecution(
      process.env.statemachine_arn, JSON.stringify({ id })
    );
    callback(null, success({ status: true, result }));
  } catch (error) {
    console.log(error.message);
    callback(error, failure({ status: false, message: error.message }));
  }
}

This lambda will fail with Converting circular structure to JSON exception, because the result object returned from the startExecution function is a big complex object that doesn't work with JSON.stringify. I've had the same thing happen to me with the results of axios requests, as well as other AWS service calls.

To fix, don't pass result but instead pass only what you need returned, or simply true.

Practical difference undefined vs null in JS

body = await sendEmail({
      from: 'Demos.Guru <support@demos.guru>',
      replyTo: submission.reviewerEmail || undefined,
      to: user.email,

Today I learned one of the practical differences between null and undefined thanks to my good friend and Kickass Partner Richard Kaufman https://richardkaufman.xyz.

In the code snippet shown, setting replyTo to undefined makes it disappear from the object, whereas setting it to null would leave the property in the object, with its value set to null. Makes perfect sense.

How to properly do SNS in Serverless

Figuring this out has taken a few hours out of my day.

Assumption

Your system is comprised of multiple CloudFormation stacks (aka Serverless Framework services aka microservices)

What not to do

functions:
  dispatcher:
    handler: dispatcher.dispatch
    events:
      - sns: 'demo_submitted'

It is critically important to not create SNS topic subscriptions on the fly as lambda event triggers. The reason is that Serverless framework tries to be helpful by implicitly auto-magically creating resource declarations for those topics, effectively within the boundaries of the consumer service. Unless parts of your microservice are talking amongst themselves using SNS (which would be weird), you usually subscribe to SNS topics that originate in other services, not the one where you are consuming it.

Why not to do it

Let's say you try to subscribe to the same topic with a lambda in another service OR you go down the road of explicitly declaring topics as resources in the services that broadcast to the topics AND you've implicitly created those very same SNS topics already.

Then when you try to deploy you will get an error that looks something like this:

Serverless Error ---------------------------------------
  An error occurred: DemoSubmittedTopic - demo_submitted already exists in stack arn:aws:cloudformation:us-east-1:423:stack/producers-service-dev/5332a.

Resources must be unique and in this case, you already created that SNS topic in the consumer, not where it belongs and is expected to live.

What to do instead

When you subscribe a lambda to an SNS topic, use the ARN notation as described in https://serverless.com/framework/docs/providers/aws/events/sns#using-a-pre-existing-topic

functions:
  dispatcher:
    handler: dispatcher.dispatch
    events:
      - sns:
          arn: arn:xxx

The ARN notation in the subscription prevents the auto-magical undesirable creation of an SNS resource.

Vanity columns in join tables

Today as I was looking at a table in DynamoDB via the AWS console. This table is basically a queue of items for a particular user to look at or dismiss, and right now it only has two key columns: one for the user, and one for the item.

It struck me that it would be worthwhile to add a few extra columns to this table just to make administration and debugging a little easier. Namely, we could add the name of the user and the name of the item. At worst the tradeoff is that the read/write throughput for the table would be a little higher.

Getting Heroku Review Apps to Work

A few years ago I heard about a project called Fourchette, which facilitated setting up one Heroku app per pull request on a project (aka review apps). I remember being all like THAT'S FREAKING BRILLIANT! Then I went back to whatever I was doing and never did anything about it.

Well, this week I finally had the time and inclination to get review apps working on Heroku. The instructions are out there, but they gave me enough trouble that I figured I'd document the gotchas for posterity.

#1. Understand the app.json file, really

We already had a tiny app.json file that we had created in connection with getting Heroku CI to run our test suite. All it had was an environments section that looked like this:

"environments": {
  "test": {
     "env": {
      "DEBUG_MAIL": "true",
      "OK_TO_SEED": "true"
    },
    "addons":[
      "heroku-postgresql:hobby-basic",
      "heroku-redis:hobby-dev"
    ]

When I started trying to get review apps to work, I simply created a pull request, and followed the dashboard instructions for creating review apps, assuming that since we already had an app.json file that it would just work. Nope, not at all.

After much thrashing, what finally got me over the hump was understanding the purpose of app.json from first principles, which didn't happen until I read this description of the Heroku Platform API. App.json originally came about as a way to automate the creation of an entire Heroku project, not just a CI or Review configuration. It predates CI and Review Apps and has been in essence repurposed.

#2. Add all your ENV variables

The concept of ENV variables being inherited from the designated parent app really threw me for a loop at first. I figured that the only ENV variables needed to be declared in the env section of app.json would be the ones I was overriding with a fixed value. Wrong again.

After much trial-and-error, I ended up with a list of all the same ENV variables as my staging environment. Some with fixed values, but most just marked as required.

"env": {
    "AWS_ACCESS_KEY_ID": {
      "required": true
    },
    "AWS_SECRET_ACCESS_KEY": {
      "required": true
    },

This won't make sense if you're thinking that app.json is specifically for setting up Review Apps (see #1 above.)

#3. Understand the lifecycle, especially with regards to add-ons

After everything was mostly working (meaning that I was able to get past the build stage and actually access my web app via the browser) I still kept getting errors related to the Redis server being missing. To make a long story short, not only did I have to add it to the addons section, but I also had to delete the review app altogether and create it again, so that addons would be created. (Addons are not affected by redeployment.)

"addons":[
  "heroku-postgresql:hobby-basic",
  "heroku-redis:hobby-dev",
  "memcachier:dev"
],

In retrospect, I realize that the reason that was totally unclear is that my review apps Postgres add-on was automatically created, even before I added an addons section to app.json. (Initially I thought it was coming from the test environment.)

I still don't know if Postgres is added by default to all review apps, or inherited from the parent app.

4. Post deploy to the rescue

There's at least one thing you want to do once, every time a new review app is created, and that is to load your database schema. You probably want to seed data also.

"scripts": {
  "postdeploy": "OK_TO_SEED=true bundle exec rails db:schema:load db:seed"
}

As an aside, I have learned to put an OK_TO_SEED conditional check around destructive seed operations to help prevent running in production. This is especially important if you run your staging instances in production mode, like you should.

How to setup Heroku Rails app to handle yarn.lock

One of the nicest features of Rails 5 is its integration with Yarn, the latest and greatest package manager for Node.js. Using it means you can install JavaScript dependencies for your app just as easily as you use Bundler to install Ruby gems.

Now one of the biggest problems you face when using any sort of Node package management is that the combinatorial explosion of libraries downloaded in order to do anything of significance.

Given that reality, you really do not want to add node_modules to your project's git repository, no more than you would want to add all the source code of your gems. Instead, you add node_modules to your .gitignore file.

Yarn adds a file to the root of your Rails app called yarn.lock. Today I learned that if you include the Node.js buildpack to your project on Heroku, it will recognize yarn.lock and install any required node modules for you. You just have to make sure that it runs first in the build chain.

heroku buildpacks:add --index 1 heroku/nodejs

Side note: If you use Heroku CI then you'll need to setup your test environment with the extra buildpack also by adding a new section to app.json.

"buildpacks": [
{ "url": "heroku/nodejs" },
{ "url": "heroku/ruby" }
]

Note that the nodejs buildpack expects a test script to be present in package.json. If you don't have one already, just add a dummy directive there. Almost anything will work; I just put an echo statement.

"scripts": {
    "test": "echo 'no tests in js'"
  },

Cloudflare Flexible SSL mode breaks Rails 5 CSRF

Putting this out there since I didn't find anything on StackOverflow or other places concerning this problem, which I'm sure I'm not the first to run into. CloudFlare is great, especially as a way to set-and-forget SSL on your site, along with all the other benefits you get. It acts as a proxy to your app, and if you set its SSL mode to Flexible then you don't have to have an SSL certificate setup on your server. This used to be a big deal when SSL certificates were expensive. (You could argue that since Let's Encrypt and free SSL certificates it's not worth using Flexible mode anymore.)

Anyway, I digress. The point of this TIL is that if you proxy https requests to http endpoint in Rails 5, you'll get the dreaded InvalidAuthenticityToken exception whenever you try to submit any forms. It has nothing to do with the forgery_protection_origin_check before action in ApplicationController.

The dead giveaway that you're having this problem is in your logs. Look for the following two lines near each other.

WARN -- : [c2992f72-f8cc-49a2-bc16-b0d429cdef20] HTTP Origin header (https://www.example.com) didn't match request.base_url (http://www.example.com)  
...
FATAL -- : [c2992f72-f8cc-49a2-bc16-b0d429cdef20] ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken): 
Aug 13 18:08:48 pb2-production app/web.1: F, [2017-08-14T01:08:48.226341 #4] FATAL -- : [c2992f72-f8cc-49a2-bc16-b0d429cdef20]    

The solution is simple. Make sure you have working SSL and HTTPS on Heroku (or wherever you're serving your Rails application.) Turn Cloudflare SSL to Full mode. Problem solved.