Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support AWS Lambda as target serverless #6173

Closed
iam4x opened this issue Jan 29, 2019 · 26 comments
Closed

Support AWS Lambda as target serverless #6173

iam4x opened this issue Jan 29, 2019 · 26 comments

Comments

@iam4x
Copy link

iam4x commented Jan 29, 2019

Hey 馃憢

Thank you a lot @timneutkens for great #5927 PR, amazing work 馃

I was going through the code of your PR in order to make it work with AWS Lambda and it was pretty straightforward.

I had to "hack" your next-serverless-loader.ts function in order to be able to inject serverless-http module that way:

// next.config.js

const nextServerlessLoader = require("next/dist/build/webpack/loaders/next-serverless-loader");
const { stripIndent } = require("common-tags");

const originalLoader = Object.assign(
  {},
  { default: nextServerlessLoader.default }
);
nextServerlessLoader.default = function() {
  const transformed = originalLoader.default
    .call(this)
    .replace(`export async function render`, `async function nextRender`);


  const result = stripIndent`
    import serverlessHttp from 'serverless-http';${transformed}
    export const render = serverlessHttp(nextRender);
  `;

  return result;
};

module.exports = {
  target: "serverless"
}

This is working great, the output can be directly plugged to an handler 馃弳

functions:
  index:
    handler: .next/serverless/pages/index.render
    events:
      - http: GET /

Here are my questions:

  • Are you willing to support other platforms than now.sh?
  • If yes could we expose an option to auto-import the serverless-http module?
  • If not, should I write a custom plugin to nextjs?

Cheers 鉁岋笍

@timneutkens
Copy link
Member

You shouldn't override the serverless loader, instead write a utility that wraps the build output, the point of the serverless target, as documented is provide the low level rendering API so that it can be wrapped for other platforms. Note that the Next.js serverless build isn't particular to Now 2.0, Now 2.0 wraps the next build output using a @now/next builder. We might introduce a utility / api for handling this wrapping in the future though (just to make things easier). It's perfectly possible to generate AWS lambdas using the current API.

@timneutkens
Copy link
Member

Eg like the example I posted here: #6070 (comment)

@iam4x
Copy link
Author

iam4x commented Jan 29, 2019

Thank you for the fast answer.

Alright, I understand the API design but having to create a file with a wrapper function for every pages is redundant or having one file as handler for everything is going against the way of using lambdas.

On my side, it's a big +1 for an utility / api for handling this wrapping with Next.js directly and I'll be happy to help by working on that. But like you said, I'm not sure this is the scope of Next.js for now.

In the meantime I'll stick to my solution as part of my build phase in less hackish way.

鉁岋笍

@romainquellec
Copy link
Contributor

As a starter point, you can push your PR.

@beheh
Copy link

beheh commented Feb 1, 2019

As a reference point for others, our AWS Lambda deployment via serverless tool (serverless deploy) uses the following lambda handler:

const express = require("express");
const serverless = require("serverless-http");
const isProduction = process.env.NODE_ENV === "production";

// setup Express and hook up Next.js handler
const app = express();

const getPage = page => require(`./.next/serverless/pages/${page}`).render;
app.get("/", getPage("index"));
app.get("/pageA", getPage("pageA"));
app.get("/pageB", getPage("pageB"));

// for local development (serverless offline)
if (!isProduction) {
	// host the static files
	app.use("/_next/static", express.static("./.next/static"));
	app.use("/static", express.static("./static"));
}

// 404 handler
app.get("*", require("./.next/serverless/pages/_error").render);

// export the wrapped handler for the Lambda runtime
exports.handler = serverless(app);

We then copy the static files to S3 (using assetPrefix in our next.config.js) and explicitly exclude the static directory from the bundle. In fact, we can also mark all dependencies apart from express and serverless-http as development dependencies (which will be excluded automatically from the Lambda zip), as the page runtimes in .next/serverless files are fully self-contained.

I'm aware that the page runtimes can also be deployed on a per route basis as separate functions, but we're a bit concerned to do that right now due to us having had bad experiences with Lambda coldstarts in the past.

Nonetheless it would be great if someone built an automagic tool specifically for AWS Lambda. That could be a tool that automatically loads the page runtimes from the filesystem (like the script above), or automatically configures serverless to add appropriate routes to each page runtime wrapped up with an HTTP server each. I'm not sure whether lambda really should be an explicit target for Next.js.

@timneutkens

This comment has been minimized.

@beheh
Copy link

beheh commented Feb 1, 2019

Good point. A single "heavy" Lambda is kept warm more easily though (e.g. through Cloudwatch Events from a Schedule Expressions). But I do see the issue if traffic scales up and we need more Lambdas around than we keep warm.

Edit: But I do get that, if cold starts are not an issue because the Lambda bootstraps within tens to hundreds of milliseconds, keeping warm is not a priority at all.

@krajowski
Copy link

krajowski commented Feb 10, 2019

@beheh thanks for the example above!
how would you build dynamic routes in this case?
tried the following but does not work:

const pages = ls('pages')
pages.forEach(page => {
  const pageName = path.parse(page).name
  app.get(`/${pageName}`, require(`./.next/serverless/pages/${pageName}`).render)
})

thanks!

UPD: and here is the answer, with subfolders support:

app.get('/', require('./.next/serverless/pages/index').render)
app.get('*', (req, res) => {
  const { pathname } = parse(req.url, true)
  try {
    require(`./.next/serverless/pages${pathname}`).render(req, res)
  } catch (err) {
    require('./.next/serverless/pages/_error').render(req, res)
  }
})

@danielcondemarin
Copy link

danielcondemarin commented Feb 24, 2019

Hi 馃憢 I've been working on a plugin (https://github.com/danielcondemarin/serverless-nextjs-plugin) for the serverless framework that targets nextjs 8 serverless mode. It adds the compat layer between the AWS Lambdas and the Nextjs page handlers at build time and takes care of uploading the static assets to S3. Seems promising so far, but is still a WIP and haven't been able to test it in anger.
Is this something the folks in this thread using the serverless framework would be interested at? Would be good to get some feedback.

@krajowski
Copy link

Hi @danielcondemarin ! There is definitely a niche for plugin like this!

I can give it a try over the coming days or weekend and provide a more meaningful feedback, but two things come to my mind as I looked through it:

  • dynamically build routes, i.e. by looping through build folder or similar like in my example above
  • support for run-time env vars (hopefully to be supported by nextjs natively)

WDYT?

@danielcondemarin
Copy link

danielcondemarin commented Feb 25, 2019

@krajowski Thanks for your feedback!
The dynamically built routes is something that should not be too difficult to add. This could be an opt-in feature in the plugin to let it create the serverless functions for you at build time based on the pages under .next/serverless/pages/*. A feature proposal on the repo would be great, then we can polish the details!
The runtime env. vars not sure how would that work, AFAIK nextjs doesn't provide any out of the box way to do this, and people use dotenv instead? Also, in the serverless.yml env. support is already quite powerful.

@timneutkens
Copy link
Member

The runtime env. vars not sure how would that work, AFAIK nextjs doesn't provide any out of the box way to do this, and people use dotenv instead? Also, in the serverless.yml env. support is already quite powerful.

You'll want to use build time env in next.config.js instead. Changes to the environment should be considered reason to rebuild.

@danielcondemarin
Copy link

The runtime env. vars not sure how would that work, AFAIK nextjs doesn't provide any out of the box way to do this, and people use dotenv instead? Also, in the serverless.yml env. support is already quite powerful.

You'll want to use build time env in next.config.js instead. Changes to the environment should be considered reason to rebuild.

Neat, I wasn't aware of this option, found it documented here. That's great because it should mean that the serverless page bundles get the env. bundled in. so there is really nothing to do for the plugin I'm working on. Will test this later 馃憤

@timneutkens
Copy link
Member

Exactly!

@danielcondemarin
Copy link

danielcondemarin commented Mar 1, 2019

@krajowski I started looking into dynamically deploying the pages via the serverless plugin, tracking the progress here

@timneutkens
Copy link
Member

@danielcondemarin could you send me a dm on twitter.com/timneutkens / spectrum.chat/users/timneutkens

@bausk
Copy link

bausk commented Mar 8, 2019

@beheh Thanks for sharing your code example! I'm trying to adopt your approach and wonder how do you not need binding _next/static on production as well. I understand from Next.js 8 documentation that the serverless js pages should be standalone but they still try to XHR all the static content in my case. Am I missing something in my config? Here's my index.js and serverless.yml, my application is dead simple with just one page.

const sls = require('serverless-http');
const binaryMimeTypes = require('./binaryMimeTypes');
const express = require('express');
const app = express();

const getPage = page => require(`./.next/serverless/pages/${page}`).render;
app.get("/", getPage("index"));

// Doesn't work when deployed without these 2 lines
app.use("/_next/static", express.static("./.next/static"));
app.use("/static", express.static("./static"));

// 404 handler
app.get("*", require("./.next/serverless/pages/_error").render);

module.exports.server = sls(app, {
  binary: binaryMimeTypes
});
service: ternovka-frontend

provider:
  name: aws
  runtime: nodejs8.10
  stage: ${self:custom.secrets.NODE_ENV}
  region: us-east-1
  environment: ${file(secrets.json)}

functions:
  server:
    handler: index.server
    events:
      - http: ANY /
      - http: ANY /{proxy+}

plugins:
  - serverless-apigw-binary
  - serverless-offline

package:
  exclude:
    - .venv/**
    - .idea/**
    - .git/**
    - node_modules/**/.cache/**
    - '**.test.js'
    - .ebextensions/**
    - .elasticbeanstalk/**
    - .vscode/**
    - .secrets/**
    - secrets.json
    - secrets.dev.json
    - /*.key
    - /*.pem
    - /*.cer

custom:
  secrets: ${file(secrets.json)}
  apigwBinary:
    types:
      - '*/*'

@beheh
Copy link

beheh commented Mar 9, 2019

@bausk Take a look at https://github.com/zeit/next.js/#cdn-support-with-asset-prefix! We intentionally exclude them from the Lambda zip and push the static assets to S3 instead.

@bausk
Copy link

bausk commented Mar 9, 2019

Excellent, thanks a lot!

@Vadorequest
Copy link
Contributor

Vadorequest commented Jul 18, 2019

I've been working with Next on AWS Lambda for more than a year, and the lack of official example/support is painful indeed.

It's really not easy to properly setup next on AWS, and it's even harder to have a local/dev env that properly simulates AWS Lambda.

Also, AWS Lambda is -by far- the most used (compared to GCP, Azure, and others), and it would really make sense for the Next.js community to target this system, as it is one of the cheapest yet most reliable out there. Also, it fits particularly well with the Next 8+ "serverless" approach, but needs guidance to do it right.

The most advanced way I've seen so far is https://github.com/danielcondemarin/serverless-nextjs-plugin. I had made my own AWS Lambda + Next 5 + Express back then https://github.com/Vadorequest/serverless-with-next and I'm still running this in production (and looking for an up-to-date replacement, having a hard time to upgrade myself, see #7823)

There have been numerous open source attempts to make AWS Lambda work with Next over the past year, I can't count how many work-in-progress repositories I've come across, such as https://github.com/justinwhall/nextjs-serverless-aws

The recent Next 9 update brings a lot of much welcomed capabilities/possibilities that are currently in discussion, see serverless-nextjs/serverless-next.js#101 and serverless-nextjs/serverless-next.js#105.

So far, I've seen two very different ways to go "serverless" with Next:

  1. The "express" way, by having one big AWS Lambda that handles all incoming requests, and then split them between Next or something else. That's what I used in https://github.com/Vadorequest/serverless-with-next and this approach allowed me to have a local env very similar to AWS Lambda. But it's much heavier and cold starts are expensive (~5sec), but also much easier to warmup since there is only one endpoint to warm it all.
  2. The "serverless" way, recently introduced by Next 8 and its "serverless" mode, where each page is a different lambda, loading only what's necessary, keeping all endpoints very lightweight and reducing cold starts. It's also what's used by https://github.com/danielcondemarin/serverless-nextjs-plugin

Honestly, I don't know what's the best approach here. Both have their pros/cons.
I like the express way, because it makes it so much easier to use middlewares and it's a huge time saver. I haven't seen anything similar to middlewares using the "serverless" way so far.
It's much heavier, but also easier to warmup. And I don't know how much faster does the "serverless" way is for cold starts. Are they really so insignificant that we can ignore them? Not quite sure about that. What I know is that if we need to warmup in "serverless" mode, it'll be much more difficult/expensive to do so.

Also, I don't know how many existing apps are out there using the "express" way, and it's gonna be hard to migrate apps from "express" to "serverless", as we must find a proper replacement for all middlewares, and other things.

So, the point of all this talking is to clarify that there are -at least- 2 ways to use Next.js on AWS, and it hasn't been proved yet that one way is "better" than the other. Also, there is a lot of efforts made by the community to make Next and AWS Lambda play together, but very few successful ones due to the complexity around those.

It'd very much be a game changer if the Next.js core team would tackle this issue and provide a reliable way of making Next working on AWS Lambda.

@beheh
Copy link

beheh commented Jul 18, 2019

I've been working with Next on AWS Lambda for more than a year, and the lack of official example/support is painful indeed.

As far as I know the official way of deploying Next.js is via Now, Zeit's cloud platform. Their platform is serverless as of Now 2.0 and can deploy Next.js and other platforms to both AWS and GCP. The tooling used there is the "next" now-builder (docs) which already generates all the final lambdas and asset artifacts. Presumably there's some production-grade tooling to manage all of that in the respective cloud (AWS/GCP), including setting up S3 buckets and API Gateway for AWS- but that probably belongs to the more proprietary parts of the Now platform (and I don't think that part is open source).

With that in mind it seems unlikely that we'll see an official tool to directly push the next build output to AWS where it completely bypasses the Now platform, outside of maybe a community contribution. I don't think that's unreasonable at all, but we probably shouldn't hold our breath waiting for an official tool but should rather get to building and contributing one as members of the community.

You can probably take that now-builder above and write a script to orchestrate the final deployment around it. I'm guessing most of the work is getting the full lifecycle sorted out, and based on the experience at my company the API Gateway has always been very painful to work with, even through tooling like serverless as soon as you want to do something like writing a ton of custom routes.

@timneutkens
Copy link
Member

timneutkens commented Jan 2, 2020

Updated this reply as it got mis-interpreted.

You can read more about deployment here: https://nextjs.org/docs/deployment

There's a bunch of community plugins to handle serverless deployments with AWS:
#6173 (comment)

The serverless target gives a deployment target independent solution so that it can be used to create solutions for every possible hosting provider. Not just ZEIT, AWS or Google Cloud functions.

You can write a solution on top of the serverless target like the community plugins.

@bensie
Copy link

bensie commented Feb 7, 2020

@timneutkens That's a disappointing reply. Totally understand recommending your product for many use cases, but dismissing self-hosting on other serverless platforms because of proprietary optimizations ignores many valid reasons for self-hosting - internal sites on a private network or behind a VPN, sites deployed as part of infra-as-code, etc. I'd love to see Zeit reconsider this position.

@timneutkens
Copy link
Member

timneutkens commented Feb 8, 2020

A community plugin exists for hosting using serverless framework: #6173 (comment)

The serverless target allows you to host anywhere you want and isn't limited to AWS Lambda as it accepts Node.js req and res.

However a Next.js app since Next.js 9 is no longer "just a server" as said in my earlier reply. You can't get away with only using AWS Lambda, you also have to serve static pages. When #9524 lands it'll become even more complicated as you'll have to know how to route a page to a static file and lambdas for the same route.

Deployment to self-hosted container or server solutions is just next build and then next start: https://nextjs.org/docs/deployment#self-hosting

@vincent-herlemont
Copy link

BTW, I make a small webpack plugin https://github.com/vincent-herlemont/next-aws-lambda-webpack-plugin for get around this issue and allow us to use Next.JS with native AWS solution deployment like SAM or Cloudfomation.

@balazsorban44
Copy link
Member

This issue has been automatically locked due to no recent activity. If you are running into a similar issue, please create a new issue with the steps to reproduce. Thank you.

@vercel vercel locked as resolved and limited conversation to collaborators Jan 28, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests