Front-End Web & Mobile

Deploy a NextJS 13 application to Amplify with the AWS CDK

Modern application development often includes features such as authentication, API setup, and file storage. In a previous post we saw how AWS Amplify and the AWS CDK manage the undifferentiated heavy-lifting of standing up these services.

However, without a hosting platform your customers would never see your product. Fortunately, AWS Amplify Hosting is a platform that manages DNS routing, instant cache invalidation, managing staging environment, CI/CD with Git providers, and more.

While one can certainly click through the console to this up, in this post, we’ll use the AWS CDK to deploy a frontend using the NextJS React framework. In addition, we’ll pass environment variables from the backend to our frontend, so they can make use of them using the Amplify JavaScript libraries.

Application Overview

cdk hosting stack with cognito, dynamodb, cloudfront, s3, and amplify hosting, and appsync

As mentioned, the starter application will contain a backend with managed services that most fullstack application would benefit from:

  • Amazon Cognito: This will provide both user sign in with user pools, as well as setting service authorization via identity pools
  • AWS AppSync: Our managed GraphQL API that offers real-time support via WebSockets
  • Amazon DynamoDB: NoSQL database for persisting data
  • Amazon CloudFront: Asset caching for the files stored in our S3 bucket
  • Amazon Simple Storage Service (S3): File storage. Public images are delivered via CloudFront while protected content is served with a pre-signed URL.

To get started with this repository, clone the CDK Fullstack Kitchen Sink repository and run the following commands while in the project’s directory:

npm install

Once the packages are installed, if not already done so, open up the project in your code editor.

Stack setup

The existing service stacks are located in the bin/cdk-kitchen-sink.ts directory. While each stack has their own implementation details that you can view, by changing the values in this project, consumers are able to easily port the application to fit their own needs.

In our case, we’ll create our AmplifyHostingStack such that it expects certain variables to successfully deploy an existing frontend repo to the Amplify Hosting service:

// bin/cdk-kitchen-sink.ts

//… other stack files

const amplifyHostingStack = new AmplifyHostingStack(app, 'HostingStack', {
  githubOauthTokenName: 'github-token',
  owner: ‘your-user-name',
  repository: 'your-frontend-repo-name’,
  environmentVariables: {
    USERPOOL_ID: authStack.userpool.userPoolId,
    GRAPHQL_URL: apiStack.graphqlURL,
  },
})

While we’ll have to create a file that makes use of this file, we’ll create it in such a way that these values are all that will be required.

It makes sense that to deploy a frontend repository from GitHub to Amplify Hosting, we’ll have to provide the repository name, owner, and a reference to the GitHub Auth Token (more on this below). In addition, we can also pull in values from the previous stacks and populate them as environment variables here.

It’s worth noting that environment variables are encrypted and at rest and will be available to frontend applications after a build via the process.env property.

Understanding our configuration

Now that we understand how we’d like to instantiate our stack, let’s actually create it.

Create a new file called NextjsHostingStack.ts  and add in the following:

import { CfnOutput, SecretValue, Stack, StackProps } from 'aws-cdk-lib'
import { Construct } from 'constructs'
import * as codebuild from 'aws-cdk-lib/aws-codebuild'
import {
  App,
  GitHubSourceCodeProvider,
  RedirectStatus,
} from '@aws-cdk/aws-amplify-alpha'

interface HostingStackProps extends StackProps {
  readonly owner: string
  readonly repository: string
  readonly githubOauthTokenName: string
  readonly environmentVariables?: { [name: string]: string }
}

The snippet above represents the imports we’ll being make use of in this file and defines the props that are allowed to be passed in (note that these match what we defined in the bin directory).

This project makes use of the alpha CDK construct for AWS Amplify. This was installed when running npm installed.

Next we’ll create our stack:

export class AmplifyHostingStack extends Stack {
  constructor(scope: Construct, id: string, props: HostingStackProps) {
    super(scope, id, props)
    const amplifyApp = new App(this, 'AmplifyCDK', {
      appName: 'NextJS app from CDK',
      sourceCodeProvider: new GitHubSourceCodeProvider({
        owner: props.owner,
        repository: props.repository,
        oauthToken: SecretValue.secretsManager(props.githubOauthTokenName),
      }),
      autoBranchDeletion: true,
    })
  }
}

This creates our stack, and makes use of the App construct to connect to the Amplify Hosting service. We’ll add more to the App object, though for now, we’re setting GitHub as the source code provider. This will enable CI/CD on the frontend repo. Note we are also setting autoBranchDeletion to true. This means whenever a connected branch is deleted on GitHub, it will automatically be deleted in Amplify Hosting.

Next, we’ll set a routing rule that essentially says, “if a user goes to a page that is not present, redirect them with a 404 (Not Found) error”. We’ll also allow our environment variables to be passed in.

Add the following:

customRules: [
  {
    source: '/<*>',
    target: ' /index.html',
    status: RedirectStatus.NOT_FOUND_REWRITE,
  },
],
environmentVariables: props.environmentVariables,

Because of all the frontend frameworks out there, and all of their various build processes, we need to tell Amplify how we’d like our project to be built, what artifacts matter, and what should be cached to speed up future deployments.

This can be accomplished by providing a buildspec.yml file to Amplify Hosting. Fortunately, because many developers prefer to not mix YAML with another language, the Amplify construct provides a handy utility that converts an object to YAML format for us.

Still in the configuration object of our app, add the following:

buildSpec: codebuild.BuildSpec.fromObjectToYaml({
  version: 1,
  frontend: {
    phases: {
      preBuild: {
        commands: ['npm ci'],
      },
      build: {
        commands: ['npm run build'],
      },
    },
    artifacts: {
      baseDirectory: '.next',
      files: ['**/*'],
    },
    cache: {
      paths: ['node_modules/**/*'],
    },
  },
})

That’s it for configuration. Now we have a reusable way of deploying a NextJS application.

When deployed, we can tell Amplify Hosting which branch is our Production branch. In addition, we can also log out the appId of our App once it’s deployed. To do this, add the following to the end of our stack file (still within the stack itself):

amplifyApp.addBranch('main', {
  stage: 'PRODUCTION',
})

new CfnOutput(this, 'appId', {
  value: amplifyApp.appId,
})

For a complete view of what this page looks like, refer to this project’s repository final branch

Creating and Storing a GitHub Secret

As mentioned, our application needs permissions to pull our frontend repo and build our project. To create a new token for your GitHub account, click this link.

For the permissions, select the admin:repo_hook permission set. Give your token a name and after clicking the button to create the token, copy the generated token to your clipboard.

We’ll store this secret in AWS Secrets Manager. Feel free to use the AWS CLI to create your secret, however this section will use the AWS Console.

In Secrets Manager, select “Store a new secret” to be presented with the following screen:

AWS Secrets Manager

As seen in the above screenshot, select the option for “Other type of secret” and click the tab to enter the secret as “Plaintext” instead of a Key/value.

On the next screen, give your secret a name that matches what we specified in our HostingStack. In this case, the name should be github-token

Deploy

To test out our solution, create a new NextJS application using the following command:

npx create-next-app simple-nextjs

Once created, store the repository on GitHub and make sure the owner and respository fields match on our backend bin/cdk-kitchen-sink.ts file.

Once set, to deploy our application, run the following command:

npx aws-cdk deploy --all

Note: At the time of this post, we’ll need to run a CLI command to update the platform property of our Amplify Hosting app so that it makes use of the new hosting runtime that enables NextJS 13 features. Once deployed, the appId of our Hosting app should have been printed to the console. Use that value to run the following command in the CLI:

aws amplify update-app –app-id THE_APP_ID –platform WEB_COMPUTE

This post will be updated once that value is present in CloudFormation.

Amplify Hosting screen

Cleanup

To remove the backend resources that were created, run the following command while in our backend repo:

npx aws-cdk destroy --all

In addition, remove the secret key stored in Secrets Manager incurring additional costs.

Conclusion

In this post we created a reusable CDK stack that deploys a NextJS 13 application to Amplify Hosting. This allows frontend applications to remain service consumers while also speeding up deployment times. To support NextJS 13 application, we also configured the platform to use the new WEB_COMPUTE property. This enables faster deployment times, more features and increased reliability. To learn more about Amplify Hosting and the newly updated NextJS features, refer to AWS Amplify Hosting docs page.