Multi-env Next.js app with AWS Amplify & Serverless
Aaron Garvey
Posted on February 16, 2021
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}"
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...
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: "../../"
# develop/serverless.yml
nextslsamplifyApp:
component: "@sls-next/serverless-component@{version_here}"
inputs:
domain: ["dev", "<your-domain-name>"]
nextConfigDir: "../../"
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/
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/**/*
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
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
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
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
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/**/*
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:
Posted on February 16, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.