DEV Community

Aaron Garvey
Aaron Garvey

Posted on

Multi-env Next.js app with AWS Amplify & Serverless

As an indie developer working on many different React applications there are a few things that I find really important, like

  • How fast can I setup my backend resources like databases, and authentication
  • How can I maintain multiple development and production environments for my applications, and
  • How fast can I ship updates out to clients

So when I reach for my development toolkit, for a long period of time AWS Amplify has been a hands down winner for me, allowing rapid multi-environment development of REST and GraphQL API's, database and object storage, authentication management. You name it, chances are Amplify can do it.

But lately there's been another aspect of the applications that I work on that is growing in importance every day.

That is the SEO friendliness, and overall performance of my app. We've all heard about bundling together bloated JS libraries, and the issues that search bots have crawling and indexing our apps. We also know that Next.js has come to the rescue with it's bag full of dynamic server side rendering goodness, automatic image optimization etc etc etc!

So lets solve all my main concerns and build an app with Next.js and AWS Amplify! Best of both worlds right?

Not so fast!

You see, AWS Amplify - although it can build and deploy a Next.js application, it can only do so if we are happy to use only statically generated pages. AWS Amplify just doesn't yet have the capabilities needed to deploy all the components required to work with Next.js' dynamic components. What this means is that out of the box we would have to either accept that when we build our Next.js application with AWS Amplify, we'd either build a static page that doesn't change with all data loaded in at build time, or build a static shell of a page, and continue to do all our data fetching on the client side for dynamic content.

That means no Next.js <Image /> component for automatic image optimization. No getInitialProps() for initial data fetching. No incremental static regeneration of pages, and so on, and so on...

If you ask me, that sounds a lot like going on a holiday but only to stay in the motel room... where's the fun in that!

In order to get the most out of Next.js with all the trimmings we need an alternative. One of which is the Serverless Framework. Serverless Framework offers many great yaml based templates that we can use to provision serverless applications to a cloud provider of your choice, including the Next.js Serverless plugin; a template that allows us to use all the cool stuff from Next.js in our very own AWS account.

That all sounds pretty nice!

But there's a catch!

So the serverless framework is really simple to get up and running. We can simply install the Serverless CLI, add a serverless.yml template to the root of our directory, run npx serverless - and then all the magic happens. The serverless framework builds and deploys our Next.js application out to Cloundfront backed by Lambda@Edge for a nice and simple AWS deployment.

But the Serverless Framework deployments are dependent upon the CLI being able to create a .serverless folder within your project, and having the contents of this folder persisted between builds. This isn't a roadblock for AWS Amplify - but a hurdle, as we don't necessarily want the AWS Amplify build server committing files into our repo after each build.

It also seems really annoying to have to manually deploy the application each time I make an update. It would be nice if instead AWS Amplify could deploy the Serverless components on each commit made to certain branches in my repo, and manage the Serverless components outputs between builds. To add to that as well, it would be even nicer to have multiple Serverless Next.js environments, and have each of them linked to in individual AWS Amplify backend environment.

So for my latest project I thought I'd see how hard it would be to get the best of both worlds and use the Next.js Serverless plugin to manage all the nice things of Next.js, and AWS Amplify to provision my backend resources and control the entire build process for the entire application.

Preface

To keep this brief, I'm going to assume you are familiar with provisioning an AWS Amplify application, or getting started with Next.js. There's plenty of great write up's on how to get started, and I'll provide links to some handy resources at the end if needed.

Lets get building!

Setting up the Serverless Next.js Plugin

Using the Serverless Next.js plugin is nice and simple. We can simply place a serverless.yml file like the one below into our project root, and assuming we have the Serverless CLI toolkit installed, we could run npx serverless to deploy our resources.

# serverless.yml

nextslsamplifyApp:
  component: "@sls-next/serverless-component@{version_here}" 
Enter fullscreen mode Exit fullscreen mode

If we were planning on just deploying single environment then using a single serverless.yml file would be just fine. For multiple environments however, it's easiest to create a separate serverless.yml template per environment we plan on provisioning, and making or environment specific changes within each template.

For this project in particular I plan on having a master branch which is linked to my prod backend resources, and a develop branch linked to all by dev backend resources. To setup the Serverless Next.js plugin to suit these environments, I've created a basic folder structure in the root of my application. At the top level I have an environments folders. Next level down, I have a folder for both the master, and develop branches of my project. Now inside each of these branch folders will contain its own serverless.yml templates.

<root>
- amplify
- environments
   |--master
      |--serverless.yml
   |--develop
      |--serverless.yml
- pages
- public
etc...
Enter fullscreen mode Exit fullscreen mode

The changes between the master and develop templates I'm using are quite minimal, as I am only changing the subdomain used by each environment. So my develop branch will be deployed out to a dev subdomain, and the master branch will be deployed out to a www subdomain. The templates below show the extent of both of the configurations being used.

# master/serverless.yml
nextslsamplifyApp:
 component: "@sls-next/serverless-component@{version_here}"
 inputs:
  domain: ["www", "<your-domain-name>"]
  nextConfigDir: "../../"
Enter fullscreen mode Exit fullscreen mode
# develop/serverless.yml
nextslsamplifyApp:
 component: "@sls-next/serverless-component@{version_here}"
 inputs:
  domain: ["dev", "<your-domain-name>"]
  nextConfigDir: "../../"
Enter fullscreen mode Exit fullscreen mode

One important thing to highlight here is the use of the nextConfigDir in both of the Serverless template files. By default, the Serverless framework expects that our serverless.yml template is located at the root of the project. In the event that we store our serverless.yml template somewhere else, like in our environments/${branch} sub-folder, then we can use the nextConfigDir parameter to inform the Serverless Framework where our project root is in relation to the current template.

Persisting Serverless Build Files

Each time we use the Serverless CLI to build our Serverless components, the framework will produce a .serverless folder next to our serverless.yml template with a group of files referencing the specific deployment details of the build. These files are then later referenced by the Serverless Framework upon subsequent builds to update and add to existing resources. So we need a way to capture these files and persist them somewhere accessible to our AWS Amplify build server.

To address this, we can set up an S3 bucket that will store these resources after each build has been completed. For this project, I've created an S3 bucket and placed inside a couple of folders just like our serverless environments folders, named after each branch within the project.

s3://<your-bucket-name>/master/.serverless/
s3://<your-bucket-name>/develop/.serverless/
Enter fullscreen mode Exit fullscreen mode

Inside each of my branch folders, I've also gone ahead and created an empty .serverless folder, which is where our output files from the Serverless component will be stored, and retrieved from for each build performed.

Prepare AWS Amplify build settings

The last step in our process is to finally configure the build settings used by AWS Amplify for our deployment. To achieve this AWS Amplify allows us to create an amplify.yml build spec file within the root of our project. When we commit the file through to our branches, AWS Amplify will use this to override the default build instructions.

The amplify.yml template allows us to break our build processes down into backend and frontend resources, each with their own respective preBuild, build, and postBuild steps. You can get as advanced as you'd like with the build configuration here, but for my project I aimed to keep it as simple as possible with the final amplify.yml taking on a structure like this.

# amplify.yml
version: 1
backend:
  phases:
    build:
      commands:
        # Provision the relevant AWS Amplify resources like Auth etc.
        # dependent on which branch we are currently building
        - amplifyPush --simple
frontend:
  phases:
    preBuild:
      commands: 
        - npm ci
        # Install the Serverless Framework CLI
        - npm i -g serverless
        # Copy any existing files from a previous Serverless deployment into our working directory
        - aws s3 cp s3://<your-bucket-name>/${AWS_BRANCH}/.serverless ./environments/${AWS_BRANCH}/.serverless/ --recursive
    build:
      commands: 
        # Move into the target Serverless env folder, and deploy the Serverless component 
        - cd ./environments/${AWS_BRANCH} && serverless
    postBuild:
      commands:
        # Copy the updated .serverless folder files and contents out to s3 for referencing in future builds
         - aws s3 cp .serverless/ s3://<your-bucket-name>/${AWS_BRANCH}/.serverless --recursive
  artifacts:
    # IMPORTANT - Please verify your build output directory
    baseDirectory: ./
    files:
      - '**/*'
  cache:
    - node_modules/**/*
Enter fullscreen mode Exit fullscreen mode

Lets walk through these instructions step by step.

First we issue Amplify our backend build instructions. Here I am using the built-in AWS Amplify helper script amplifyPush --simple to automatically provision the correct AWS Amplify backend environment with the associated branch. So assuming I have linked my prod AWS Amplify resources to my master branch, this will ensure I never accidently push out my dev backend resources to my production app frontend.

# amplify.yml
version: 1
backend:
  phases:
    build:
      commands:
        # Provision the relevant AWS Amplify resources like Auth etc.
        # dependent on which branch we are currently building
        - amplifyPush --simple
Enter fullscreen mode Exit fullscreen mode

With the backend taken care of by AWS Amplify, we can then setup a clean environment for building our front end with npm ci, and also install the Serverless CLI tools with npm i -g serverless. Then up we can use the AWS CLI commands to interact with our S3 bucket we created earlier to copy down any existing files from our .serverless folder that may have been generated from previous builds.

# amplify.yml
    preBuild:
      commands: 
        - npm ci
        # Install the Serverless Framework CLI
        - npm i -g serverless
        # Copy any existing files from a previous Serverless deployment into our working directory
        - aws s3 cp s3://<your-bucket-name>/${AWS_BRANCH}/.serverless ./environments/${AWS_BRANCH}/.serverless/ --recursive
Enter fullscreen mode Exit fullscreen mode

You'll see here I'm using one of the default environment variables from AWS Amplify ${AWS_BRANCH}. So depending on which environment AWS Amplify is building, our build file will be updated with the exact name of the branch we are currently working with.

With our files all in sync, we can then kick off the build process. Building out the Serverless component is as simple as a quick cd into our target environment folder, and then calling serverless. Again, we can use the ${AWS_BRANCH} environment variable to make sure we switch into the correct branch for each build.

# amplify.yml
    build:
      commands: 
        # Move into the target Serverless env folder, and deploy the Serverless component 
        - cd ./environments/${AWS_BRANCH} && serverless
Enter fullscreen mode Exit fullscreen mode

Once our build has completed, we then need to collect any output files generated to the local .serverless folder and store them back into S3 for future use.

# amplify.yml
    postBuild:
      commands:
        # Copy the updated .serverless folder files and contents out to s3 for referencing in future builds
         - aws s3 cp .serverless/ s3://<your-s3-bucket>/${AWS_BRANCH}/.serverless --recursive
Enter fullscreen mode Exit fullscreen mode

And finally, handle any specific artifacts to output, or cache any additional files.

  artifacts:
    # IMPORTANT - Please verify your build output directory
    baseDirectory: ./
    files:
      - '**/*'
  cache:
    - node_modules/**/*
Enter fullscreen mode Exit fullscreen mode

With all these pieces now put together and assuming auto-builds have been enabled with AWS Amplify, now any subsequent push to either the develop or master branches should kick off a new build process in AWS Amplify, provisioning out the AWS Amplify backend resources, along with the Serverless Next.js plugin components! Our .serverless resources are being successfully persisted within S3, and ready to be referenced for any future builds.

So despite AWS Amplify not supporting many of Next.js features out of the box yet, with a couple of tweaks to the build process, and a little help from the Serverless Framework, there's no reason we can't have the best of both worlds from Next.js and AWS Amplify!

Additional Resources:

Top comments (9)

Collapse
 
jonathangiardino profile image
jonathangiardino

Great article Aaron!

I am actually looking into this exact stack and setup to build a Shopify app.

How would you integrate DynamoDB and the GraphQL API into this setup (using the Serverless NextJS component from Serverless Framework and Amplify)?

Collapse
 
aarongarvey profile image
Aaron Garvey

Thanks Jonathan!

So for adding graphql and db resources like AppSync and DyanmoDB, you'd use the AWS Amplify CLI tools to provision these resources. I'd suggest taking a look at docs.amplify.aws/lib/graphqlapi/ge...] as a starting point.

Once you have your AWS Amplify resources all setup from the CLI, then deploying the application as described above will automatically build any changes for your backend resources every time you push a commit to one of your targeted branches using AWS Amplifies CI/CD pipeline.

Collapse
 
blkdr_plus profile image
Ubaldo Quintana Hdez

Hello!

First of all... thanks for your article! I was able to configure a project using your article as base and it was easy to follow.

Now I have a "problem". This workflow worked flawlessly until some days ago. In the last deploy the process works just fine but I got a an error at the "Deploy" step in the Amplify console:

"Cannot find any generated SSR resources to deploy."

So the Amplify console says that the deployment fails, but the Serverless deployment went perfectly at the previous step "Build".

Do you have this error? Do you know how to fix it?

Thanks in advance!

Collapse
 
rwalle61 profile image
Richard Waller

Thanks for this article! I'm new to Next.js, Amplify and Serverless. I read that the aws-amplify-serverless-plugin lets you use Serverless to set up Amplify backends (medium.com/@jrheling/using-serverl...).

Do you think a serverless.yml could set up a Next.js app with the Serverless Next.js plugin, and a Amplify backend with the Serverless Amplify plugin, and they could talk to each other? What would be the difference between that and your custom solution?

Collapse
 
aarongarvey profile image
Aaron Garvey

Hey Richard, so I think the main difference between these approaches is a choice between having the amplify CI/CD tools manage the deployment of he backend and front end together, or having serverless framework deploy the backend and front end together, or in my case - having amplify CI/CD deploy the backend upon any commits pushed to my dev and master branches and then triggering the serverless framework to deploy the front end with each build.

I can’t see any reason why you couldn’t setup a serverless template as you’ve described. If you put the right pieces together you would still have your aws amplify resources deployed alongside your front end resources - it would all just be managed by the serverless deployment tools.

I think which way you chose to approach this depends on what tools your most comfortable with, and what stage your project is at. In my case, I leverage a number of tools from AWS Amplify CI/CD pipeline, and was migrating a site that was previously only static pages, and aws amplify was previously deploying everything for me. So it was easier for me to extend the amplify CI/CD build instructions to trigger a serverless front end deployment. But if you already have a NextJS deployment running on serverless framework and want to add on some aws resources at a later stage, then perhaps using the amplify serverless template is better?

Collapse
 
diegomelendez profile image
Diego Melendez

Thanks for the article Aaron,

I have just one doubt, what do you put as the output in baseDirectory in the amplify.yml file

Collapse
 
aarongarvey profile image
Aaron Garvey

Cheers Diego,

Technically in this example there is no real need for the artefacts or any output within the amplify.yml file - as the serverless framework component is actually coordinating the build and deployment of the front end resources. If you had additional static resources however, or were merging items from a larger monorepo type environment that needed to be lifted out to perhaps a public facing s3 bucket, then this is the place to do that.

Collapse
 
diegomelendez profile image
Diego Melendez

Thank you Aaron, my issue was that I forgot to add the domain to the serverless.yml, after I added it it worked perfectly

Great post!!

Collapse
 
thedaviddias profile image
David Dias

Nice and detailed article! Thanks 🙏!