AWS Lambda support

Description

AWS Lambdas are one of the best serverless options, and they include a very nice free tier.

Requirements

  • The server.js file generated by npx frontity build should be ready to be uploaded to AWS without requiring any extra modification.

  • It should work with the amazon domain (https://ov8mwmh2xa.execute-api.us-east-1.amazonaws.com/latest) as well as a custom domain (https://domain.com).

    This is especially tricky because the URLs of the amazon domain contain a folder that needs to be stripped out:

    • https://....amazonaws.com/latest -> "/".
    • https://....amazonaws.com/latest/some-post -> "/some-post".

    But when people switch to a custom domain, that folder diappears:

    • https://domain.com/ -> "/".
    • https://domain.com/some-post -> "/some-post".
  • It should also work on other services that also use the AWS Lambdas, like Netlify Functions.

  • We should avoid the need for external tools, like the serverless framework. It should work by default.

Nice to have:

  • It would be really nice if we could provide some easy instructions to upload the Lambda after the deployment, maybe using a simple npm CLI like this or this.

Possible solution

We could use this library https://github.com/dougmoscrop/serverless-http but I think I’m more in favor of using the official Express library directly (https://github.com/awslabs/aws-serverless-express) because it has better support. How to use it with Koa is explained in this other library: https://github.com/compwright/aws-serverless-koa/blob/master/index.js

There is more information on the Deploy to AWS Lambda? thread.

Especially, I did some research on how to solve the amazon domain vs custom domain problem in this post.

I have made a small video to explain how the code for the server.js file is generated:

Hi!

The server.js file generated by npx frontity build should be ready to be uploaded to AWS without requiring any extra modification.

@luisherranz that means that the current server handler, what app.callback() returns, needs to stay the same and we need ti provide an extra export, named handler. Am I presuming correctly? Is that handler export what the serverless providers are using?

Exactly. The default export should be a “req/res” function for NodeJS and other serverless services like Google Cloud or Vercel, and the handler export should be an “event/context” function for serverless services like AWS or Netlify.

1 Like

Awesome! :slight_smile: Thank you.

I have a question: what is more efficient, having two server instance, calling server() with different options or add the serverless context middleware for the req/res handler as well?

I’m pondering which one it’s more suitable as I think the middleware it’s more straight forward, but adds implicit code, and I am not sure if adding it will pollute the context with data that do not exist.

Implementation Proposal

In order to support the serverless method we need to adjust our server entry point and also define a new method to retrieve the needed default handler and proxy one, just like Netlify and the bunch, expect.

  • Install aws-serverless-express as part of the core package.
  • Import the main package and import it inside core/src/server/index.ts
  • Define the middleware to capture serverless events (Deploy to AWS Lambda?)
// Make sure the porxy headers are forwarded
app.proxy = true;

// This middleware is from this discussion: https://community.frontity.org/t/deploy-to-aws-lambda/814/19?u=cristian.bote
app.use((ctx, next) => {
  ctx.lambdaEvent =
    (ctx.headers["x-apigateway-event"] &&
      JSON.parse(decodeURIComponent(ctx.headers["x-apigateway-event"]))) ||
    {};
  ctx.lambdaContext =
    (ctx.headers["x-apigateway-context"] &&
      JSON.parse(decodeURIComponent(ctx.headers["x-apigateway-context"]))) ||
    {};
  ctx.env = (ctx.lambdaEvent && ctx.lambdaEvent.stageVariables) || process.env;

  // Workaround an inconsistency in APIG. For custom domains, it puts the
  // mapping prefix on the url, but non-custom domain requests do not. Fix it by
  // changing the path to the proxy param which has the correct value always.
  if (ctx.lambdaEvent.pathParameters && ctx.lambdaEvent.pathParameters.proxy) {
    const dummyBase = "zz://zz";
    const url = new URL(ctx.url, dummyBase);
    url.pathname = "/" + ctx.lambdaEvent.pathParameters.proxy;
    ctx.url = url.href.replace(dummyBase, "");
  }
  return next();
});
  • Define a new method inside core/src/server/index.ts to return the serverless handler and the req/res one as well.
const createServerlessHandlers = (args: ServerOptions) => {
  const defaultHandler = server(args);

  // This will be the current `server` callback
  const serverless = awsServerlessExpress.createServer(defaultHandler);

  // This is gonna be the proxy handler for serverless
  const handler = (event, context) =>
    awsServerlessExpress.proxy(serverless, event, context);

  return {
    defaultHandler,
    handler,
  };
};

export default createServerlessHandlers;
  • Modify the generateImportsTemplate function to handle the special server case in core/src/scripts/utils/entry-points.ts. In order to keep the same functionality for the current
if (type === "server") {
  template += [
    `const { defaultHandler, handler } = server({ packages });`,
    `export { handler }`,
    `export default defaultHandler;`,
  ].join("\n");
} else {
  template += `export default ${type}({ packages });\n\n`;
}

Implementation details

With the above changes made, there aren’t gonna be breaking changes for the regular req/res handler, except the .proxy mode which will forward the proxy headers. Other than that, this should be pretty transparent and not break the current way of working.

Let me know your thoughts on the above. Thank you!

I didn’t know about the app.proxy option. Great catch.

It looks perfect to me. Great work @cristian.bote! :slightly_smiling_face:

1 Like

Yup, good proposal @cristian.bote! :clap::clap:

I think anyone of the team would be able to implement the AWS Lambda support just by reading it.

2 Likes