Multi-env Next.js app with AWS Amplify & Serverless
Tags: react, nextjs, aws, serverless
Published: 2021-02-16 04:47:57

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](https://aws.amazon.com/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](https://www.serverless.com/).  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](https://www.serverless.com/plugins/serverless-nextjs-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.

```yml
# 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.

```yaml
# master/serverless.yml
nextslsamplifyApp:
 component: "@sls-next/serverless-component@{version_here}"
 inputs:
  domain: ["www", "<your-domain-name>"]
  nextConfigDir: "../../"
```

```yaml
# 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.

```yaml
# 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.

```yaml
# 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.

```yaml
# 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.

```yaml
# 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.

```yaml
# 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.

```yaml
  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:
- [Getting Started - AWS Amplify](https://docs.amplify.aws/start/q/integration/react?sc_icampaign=react-start&sc_ichannel=choose-integration)
- [Getting Started - Next.js](https://nextjs.org/docs/getting-started)
- [Multiple Environments - AWS Amplify](https://docs.amplify.aws/cli/teams/overview)(https://docs.amplify.aws/start/q/integration/react?sc_icampaign=react-start&sc_ichannel=choose-integration)
- [Serverless Next.js plugin](https://www.serverless.com/plugins/serverless-nextjs-plugin)