Turner Houghton

Turner Houghton

Head in the AWS Cloud

How to set up Amplify Hosting for NextJS 13+ using the AWS CDK

By: Turner HoughtonTurner Houghton
Published on April 11, 2023, last updated on April 13, 2023 at 7:25 AM

Amplify is one of the most weirdly defined services I've come across in my years working with AWS. Amplify is not only the name they chose to give to a bunch of frontend libraries that help you connect to AWS services, but they also decided to use that same name to define a hosting platform that can connect to your repositories to build and deploy frontend applications. This article is about the latter.

For my own projects I have developed a "CDK-first" mentality. Meaning that my first goal with approaching any service is to learn how to define it using the AWS CDK so that my infrastructure is more easy to manage.

Why Amplify Hosting?

Before Amplify hosting, setting up the infrastructure and deployment pipelines for SPAs (single page applications) was incredibly tedious. You would have to provision an S3 bucket, give it public access, and then, if you wanted a custom domain name, configure a CloudFront distribution to use that bucket as an origin and set up Route53 config to point to that distribution. Not only that, but you would need to set up a way to deploy your built SPAs into that bucket and have a strategy in place for invalidating the existing CloudFront distribution cache so that your users would see the latest version. With Amplify Hosting, this becomes a whole lot simpler.

Amplify Hosting only requires that you have a repository to start getting set up, and it deploys everything for you - S3 bucket, CloudFront distribution, Lambda@Edge functions or whatever else your frontend application might need. There's also no need to define your own build pipelines or workflows since Amplify Hosting will automatically set up webhooks to deploy your app whenever it detects changes to the main branch or any feature branches you choose to configure.

Why NextJS?

This is more of a personal choice, but my criteria for a robust frontend framework in 2023 is one where Server-Side-Rendering (SSR) is baked in and the community has a decent level of activity. NextJS met both of those criteria for me, but Amplify Hosting supports a number of different frontend frameworks including Gatsby, Vue, Angular and vanilla React. If you decide to use a different framework you can still follow along for the most part, you will just have to modify your build config to work with the framework you choose.

Let's get started

If you search for "AWS CDK Amplify Hosting" on Google you might come across this CDK construct. We are going to use it to help us define our frontend infrastructure, however since a lot of the functionality we are going to be using was introduced in October of 2022 there are pieces we will have to do ourselves by modifying the underlying CloudFormation template resources.

Step 1: Project Initialization

This assumes you already have the AWS CDK installed and valid credentials on your machine.

In a new directory, run

mkdir frontend-infra cd frontend-infra && cdk init app --language typescript

This will create a new CDK project for you using Typescript, which we will use to define a stack that sets up Amplify Hosting.

In your new project, run

npm install @aws-cdk/aws-amplify-alpha

This will give us a higher level construct to work with to define our Amplify App that we will make a few tweaks to.

Step 2: Define your stack

Next we are going to define a new construct. I put mine in lib/constructs/NextJSAmplifyApp.ts but feel free to put yours anywhere.

// lib/constructs/NextJSAmplifyApp.ts import { Construct } from "constructs"; import * as amplify from "@aws-cdk/aws-amplify-alpha"; import * as iam from "aws-cdk-lib/aws-iam"; import { SecretValue } from "aws-cdk-lib"; import { CfnApp, CfnBranch } from "aws-cdk-lib/aws-amplify"; export interface NextJsAmplifyAppProps { amplifyAppName: string; githubOwner: string; githubRepo: string; // secret should be a reference to a plaintext OAuth token in Secrets Manager oauthSecretName: string; // needed if nextjs app is not located in the root of the project, like in a monorepo // ex. apps/web appRoot?: string; domainName?: string; mainBranchName?: string; environmentVariables?: Record<string, string>; role?: iam.Role; } export class NextJsAmplifyApp extends Construct { public readonly app: amplify.App; public readonly domain?: amplify.Domain; public readonly mainBranch: amplify.Branch; constructor(scope: Construct, id: string, props: NextJsAmplifyAppProps) { super(scope, id); const { amplifyAppName, githubOwner, githubRepo, oauthSecretName, appRoot, domainName, mainBranchName = "main", environmentVariables, role, } = props; this.app = new amplify.App(this, "amplifyApp", { appName: amplifyAppName, role, sourceCodeProvider: new amplify.GitHubSourceCodeProvider({ owner: githubOwner, repository: githubRepo, oauthToken: SecretValue.secretsManager(oauthSecretName), }), environmentVariables: { ...environmentVariables, ...(appRoot && { AMPLIFY_MONOREPO_APP_ROOT: appRoot }), }, // nextjs specific rewrite rule customRules: [ { source: "/<*>", target: "/index.html", status: amplify.RedirectStatus.NOT_FOUND_REWRITE, }, ], }); // amplify CDK module doesn't support setting Framework or Platform yet // keep an eye on this PR to see if it is supported yet: https://github.com/aws/aws-cdk/pull/23818 // in the mean time we need to use L2 construct override to correct this (this.app.node.defaultChild as CfnApp).addPropertyOverride( "Platform", "WEB_COMPUTE" ); this.mainBranch = this.app.addBranch(mainBranchName, { stage: "PRODUCTION", }); // not strictly needed but makes it look as much like Amplify apps created via AWS console (this.mainBranch.node.defaultChild as CfnBranch).addPropertyOverride( "Framework", "Next.js - SSR" ); if (domainName) { this.domain = this.app.addDomain("customDomain", { domainName, }); this.domain.mapRoot(this.mainBranch); } } }

You can see towards the end of our custom resource we have to add a few resource overrides to define the platform and framework. Note that the framework is optional - this is just for display purposes in the AWS Console. The platform however is required and we will need to define this as "WEB_COMPUTE" in order to deploy NextJS applications version 12 or later.

Next, let's use this custom construct in our stack. Go to your main stack file (mine is in lib/frontend-infra-stack.ts and go ahead and initialize our construct:

import * as cdk from "aws-cdk-lib"; import { Construct } from "constructs"; import { NextJsAmplifyApp } from "./constructs/NextJSAmplifyApp"; import * as iam from "aws-cdk-lib/aws-iam"; import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; export interface FrontendStackProps extends cdk.StackProps { githubConfig: { owner: string; repo: string; oauthSecretName: string; } webAppDomainName: string; } export class FrontendStack extends cdk.Stack { constructor(scope: Construct, id: string, props: FrontendStackProps) { super(scope, id, props); const { githubConfig, webAppDomainName, } = props; // default role does not have cloudwatch logs write permissions // this might be overkill but this role gives permissions to write to cloudwatch so ¯\_(ツ)_/¯ const amplifyRole = new iam.Role(this, "AmplifyRoleWebApp", { assumedBy: new iam.ServicePrincipal("amplify.amazonaws.com"), description: "Custom role permitting resources creation from Amplify", managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( "AdministratorAccess-Amplify" ), ], }); new NextJsAmplifyApp(this, "webApp", { amplifyAppName: "portfolio-site", role: amplifyRole, githubOwner: githubConfig.owner, githubRepo: githubConfig.repo, oauthSecretName: githubConfig.oauthSecretName, appRoot: "apps/web", domainName: webAppDomainName!, environmentVariables: { // insert environment variables here }, }); } }

Step 3: Create Github access token

NOTE: This step assumes you are using Github for your repository hosting. The last step is getting your Github OAuth token and uploading it to AWS Secrets Manager.

First, follow these steps to generate a personal access token.

Next, copy that access token value and go to the AWS Secrets Manager console and create a new secret. Enter the secret in as plaintext, it should looks something like this:

github_pat_ABCDEFGHIJLKMNOPQRSTUVWXYZ...

Finish creating your secret, then copy your secret name for the next step.

Step 4: Define stack inputs

For easier deployments locally I tend to define all of my stack inputs using environment variables, but you can do whatever you want.

// bin/frontend-infra.ts #!/usr/bin/env node import "source-map-support/register"; import * as cdk from "aws-cdk-lib"; import { FrontendStack } from "../lib/frontend-stack"; import * as dotenv from "dotenv"; import assert = require("assert"); dotenv.config({ path: ".env.local" }); const app = new cdk.App(); assert(process.env.GITHUB_OWNER, "GITHUB_OWNER not defined"); assert(process.env.GITHUB_REPO, "GITHUB_REPO not defined"); assert(process.env.GITHUB_OAUTH_SECRET_NAME, "GITHUB_OAUTH_SECRET_NAME not defined"); assert(process.env.WEB_APP_DOMAIN_NAME, "WEB_APP_DOMAIN_NAME not defined"); new FrontendStack(app, "FrontendStack", { stackName: "nextjs-frontend", githubConfig: { // your github username owner: process.env.GITHUB_OWNER, // your repository name repo: process.env.GITHUB_REPO, // the name of the Secrets Manager secret you created in the previous step oauthSecretName: process.env.GITHUB_OAUTH_SECRET_NAME }, // mywebapp.example.com webAppDomainName: process.env.WEB_APP_DOMAIN_NAME, });

Step 5: Add amplify.yml

We have one last step to ensure our application builds correctly once the source code has been downloaded. Add this file to the root of your frontend repo where your NextJS app lives (not the one that defines the Amplify Hosting.) In my case I have my application defined in a monorepo, but if your project defines the application in the root of the project you can remove the appRoot and postBuild properties.

version: 1 applications: # main website - appRoot: apps/web frontend: phases: preBuild: commands: - echo 'using amplify.yml in project root for app "web"' - yarn install build: commands: - yarn build postBuild: commands: # needed because standalone copies the monorepo structure for some stupid reason # this copies the files back to the root of the "standalone" folder so Amplify can find them # keep an eye on https://github.com/aws-amplify/amplify-hosting/issues/3179 # for possible long-term fix from the Amplify team - cp -r .next/standalone/apps/web/. .next/standalone artifacts: baseDirectory: .next files: - '**/*' cache: paths: - node_modules/**/*

Step 6: Pat yourself on the back!

That's it! You should now be able to deploy your stack by running cdk deploy in the root of your project. You may need to manually trigger your first build from the Amplify AWS Console page, but all subsequent changes to your main branch will be automatically detected and deployed.