DEV Community

Cover image for Deploying Next.js 13 with Amplify CDK
Guillaume Égée for Serverless By Theodo

Posted on • Edited on

Deploying Next.js 13 with Amplify CDK

📜 How to deploy a Next.js app step by step with an Amplify CDK construct, avoiding you all the pains.

🐝 TLDR: an example app has just been added in swarmion, so you can now bootstrap a ready-to-use Next.js project!

Next.js is a popular React framework to deploy web apps with advanced features like Server Side Rendering (SSR), Image optimization or Incremental Static Regeneration (ISR). In my last project, I chose this framework notably to ensure a good SEO.

To deploy the app, I had the requirement to use AWS as a cloud-provider, with a limited budget, so neither Vercel (a safe choice, as it is the company which develops Next.js), nor a containerized solution like the managed AWS ECS Fargate service could satisfy my needs. Then, I started with a serverless hosting thanks to the serverless-next project, which provides a serverless plugin or a CDK construct to deploy a Next.js stack. However, this project is no more well-maintained and new Next.js features (like middlewares, etc.) are not supported.

Here comes AWS Amplify which has just announced on November 17th to support Next.js 12 and 13 🎉! Amplify is an AWS service to build and host full-stack applications. It uses serverless services under the hood. I tested it with @mamadoudicko and @alexandreperni4 and we will explain you how to setup a production-ready app!

Introducing Amplify's AWS CDK

AWS CDK, for AWS Cloud Development Kit, is a multi-languages framework for writing infrastructure as code and deploying it through AWS CloudFormation. Our examples will be written in Typescript, but other languages like Python, Java, C# or Go can be used as well.

To deploy with the AWS CDK, you would first need to declare a cdk.json file at the root of your frontend repository. This allows you to tell the AWS CDK where is the entry point of the CDK app, hosting/bin.ts here:

{
  "app": "pnpm ts-node hosting/bin.ts"
}
Enter fullscreen mode Exit fullscreen mode

You need to install these dev dependencies:

pnpm add -D aws-cdk-lib
pnpm add -D @aws-cdk/aws-amplify-alpha
pnpm add -D constructs
Enter fullscreen mode Exit fullscreen mode

You will also need some configuration files to bootstrap the app:

  • .nvmrc file to define your node version
  • tsconfig.json TypeScript configuration file, with target version set to "es6" or more

In the hosting folder, you then need to add a bin.ts file to declare the cdk app:

import * as cdk from 'aws-cdk-lib';

import { AmplifyStack } from './stack';

const app = new cdk.App();

new AmplifyStack(app, 'NextJsSampleStack', {
  description: 'Cloudformation stack containing the Amplify configuration',
});
Enter fullscreen mode Exit fullscreen mode

Nothing fancy at this point, the interesting part is in the ./stack.ts configuration file:

import { App } from '@aws-cdk/aws-amplify-alpha';
import { aws_iam, CfnOutput, Stack, StackProps } from 'aws-cdk-lib';
import { BuildSpec } from 'aws-cdk-lib/aws-codebuild';
import { Construct } from 'constructs';

export class AmplifyStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);

    // Define Amplify app
    const amplifyApp = new App(this, 'AmplifyAppResource', {
      appName: 'NextJS app',
      description: 'My NextJS APP deployed with Amplify',

      // ⬇️ configuration items to be defined ⬇️
      role,
      sourceCodeProvider,
      buildSpec,
      autoBranchCreation,
      autoBranchDeletion,
      environmentVariables,
      // ⬆️ end of configuration ⬆️
    });

    // Attach your main branch and define the branch settings (see below)
    const mainBranch = amplifyApp.addBranch('main', {
      autoBuild: false, // set to true to automatically build the app on new pushes
      stage: 'PRODUCTION',
    });

    new CfnOutput(this, 'appId', {
      value: amplifyApp.appId,
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Let’s go through what we need for the different variables within the configuration object.

📖 Step-by-step guide

Define a role to Amplify (role)

This is needed to add a custom role that will be assumed by the Amplify resource.

import { ManagedPolicy, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';

const role = new Role(this, 'AmplifyRoleWebApp', {
  assumedBy: new ServicePrincipal('amplify.amazonaws.com'),
  description: 'Custom role permitting resources creation from Amplify',
  managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('AdministratorAccess-Amplify')],
});
Enter fullscreen mode Exit fullscreen mode

Connection to your Github repository (sourceCodeProvider)

The sourceCodeProvider configuration allows Amplify to access the source code of your application. To connect a Github repository, you can use the declaration below:

import { GitHubSourceCodeProvider } from '@aws-cdk/aws-amplify-alpha/lib/source-code-providers';
import { SecretValue } from 'aws-cdk-lib';

const sourceCodeProvider = new GitHubSourceCodeProvider({
  // GitHub token should be saved in a secure place, we recommend AWS Secret Manager:
  oauthToken: SecretValue.secretsManager('GITHUB_TOKEN_KEY'), // replace GITHUB_TOKEN_KEY by the name of the Secrets Manager resource storing your GitHub token
  owner: '<user name of the GitHub repository owner>',
  repository: '<name of the Github repository>',
});
Enter fullscreen mode Exit fullscreen mode

To get a Github token, go to your Github account develop settings and generate a personal access token. Amplify will need quite high access rights to your repository as it will need to generate ssh keys to clone the repository.

To store the token in AWS, we recommend to use the AWS Secret Manager service. Be careful to choose a plain text secret (in the AWS console select "Store a new secret", "Other type of secret" and finally "Plaintext"). Choose a key name that matches the key used in the GitHubSourceCodeProvider construct (GITHUB_TOKEN_KEY in this example).

Build settings (buildSpec)

Then, you need to define how the app will be built. Below is an example with pnpm package manager:

import { BuildSpec } from 'aws-cdk-lib/aws-codebuild';

import { environmentVariables } from './environmentVariables';

export const buildSpec = BuildSpec.fromObjectToYaml({
  version: '1.0',
  applications: [
    {
      frontend: {
        phases: {
          preBuild: {
            commands: [
              // Install the correct Node version, defined in .nvmrc
              'nvm install',
              'nvm use',
              // Install pnpm
              'corepack enable',
              'corepack prepare pnpm@latest --activate',
              // Avoid memory issues with node
              'export NODE_OPTIONS=--max-old-space-size=8192',
              // Ensure node_modules are correctly included in the build artifacts
              'pnpm install',
            ],
          },
          build: {
            commands: [
              // Allow Next.js to access environment variables
              // See https://docs.aws.amazon.com/amplify/latest/userguide/ssr-environment-variables.html
              `env | grep -E '${Object.keys(environmentVariables).join('|')}' >> .env.production`,
              // Build Next.js app
              'pnpm next build --no-lint',
            ],
          },
        },
        artifacts: {
          baseDirectory: '.next',
          files: ['**/*'],
        },
      },
    },
  ],
});
Enter fullscreen mode Exit fullscreen mode

This example is using pnpm, but you can choose to use any other package manager, such as npm or yarn. A few things to note here :

  • Setting the --max-old-space-size node option is important to prevent out of memory (OOM) errors while building your application when it reaches a certain size.
  • To let your server-side code access your environment variables, you have to include them in a .env.production file. This allows you to put secrets known only from your CI environment in the .env.production file created on the fly.
  • The artifacts section is very important as it configures which files from the build step will be included in the artifacts and thus available at run time.

Auto-deploy when remote branches satisfying a certain name pattern are created

Amplify allow to create a dedicated environment when you push to a branch with a specific name pattern.

import { AutoBranchCreation } from '@aws-cdk/aws-amplify-alpha';

export const autoBranchCreation: AutoBranchCreation = {
  autoBuild: true,
  patterns: ['feature/*'],
  pullRequestPreview: true,
};
Enter fullscreen mode Exit fullscreen mode

You might want to disable the feature in production. For this, simply use undefined instead of this configuration.

💡 Note that this environment can be automatically removed once the branch is deleted if the parameter autoBranchDeletion is set to true.

Configuring environment variables

export const environmentVariables = {
  // https://docs.aws.amazon.com/amplify/latest/userguide/build-settings.html#enable-diff-deploy
  AMPLIFY_DIFF_DEPLOY: 'true',
};
Enter fullscreen mode Exit fullscreen mode

💡 Environment variables can be defined globally or in branch environments.

Setting up a domain

You can add the domain configuration directly in the stack.ts file, to have access to the amplifyApp and mainBranch variables:

const domain = amplifyApp.addDomain('your-domain.com', {
  autoSubdomainCreationPatterns: ['feature/*'],
  enableAutoSubdomain: true,
});
Enter fullscreen mode Exit fullscreen mode

Don't forget to map it with a specific branch by adding the following line:

domain.mapRoot(mainBranch);
Enter fullscreen mode Exit fullscreen mode

💡 If the domain is already defined in AWS Route 53 service, it will be automatically linked to your stack: AWS will add the appropriate DNS records in the hosted zone. Otherwise, you will need to add the records manually in your domain name provider.

Deployment

Almost there! Before being able to detect and deploy Next.js, Amplify needs to be configured with the platform type WEB_COMPUTE. It needs to be done after Amplify app deployment. As there is no option in Cloudformation for now, the best way to do it programmatically is to use a Custom Resource construct:

import { AwsCustomResource, AwsCustomResourcePolicy } from 'aws-cdk-lib/custom-resource';

// Set Amplify platform type to WEB_COMPUTE
new AwsCustomResource(this, 'AmplifySetPlatform', {
  onUpdate: {
    service: 'Amplify',
    action: 'updateApp',
    parameters: {
      appId: amplifyApp.appId,
      platform: 'WEB_COMPUTE',
    },
    physicalResourceId: PhysicalResourceId.of('AmplifyCustomResourceSetPlatform'),
  },
  policy: AwsCustomResourcePolicy.fromSdkCalls({
    resources: [amplifyApp.arn],
  }),
});
Enter fullscreen mode Exit fullscreen mode

💡 This construct is equivalent to a call to AWS SDK, that can also be written with an aws CLI command:

aws amplify update-app --app-id <yourAppID> --platform  WEB_COMPUTE
Enter fullscreen mode Exit fullscreen mode

Finally, to deploy your AWS CDK app, navigate to the folder containing the cdk.json file, then enter the following command:

pnpm cdk bootstrap && pnpm cdk deploy
Enter fullscreen mode Exit fullscreen mode

When the deployment ends successfully, the id of your Amplify app will be printed in your command line interface.

⚠️ This step only deploys an Amplify stack, but it does not build nor deploy the Next.js app yet! To do so, you can either:

  • Set the autoBuild parameter to true in a branch setting to deploy on GitHub pushes (or other source provider)
  • Use a webhook as explained below.

Adding a webhook

In many cases, a dedicated CI/CD platform is used to test and deploy an app. Then, the direct GitHub integration that triggers an Amplify build at each push on a particular branch is no more sufficient. So you may want to trigger a deployment directly in a CD step. Amplify allows to trigger a deployment with webhooks. This is not yet configurable through the CDK but you can use the Amplify CLI:

aws amplify create-webhook --app-id <yourAppID> --branch-name <yourBranchName>
Enter fullscreen mode Exit fullscreen mode

https://awscli.amazonaws.com/v2/documentation/api/latest/reference/amplify/create-webhook.html

In your CD, add curl [webhook url].

💡 Alternatively, you can use a Custom Resource construct to create your webhook:

// Create a webhook to use in your proper CI/CD
const webhookCustomResource = new AwsCustomResource(this, 'AmplifyWebhook', {
  onUpdate: {
    service: 'Amplify',
    action: 'createWebhook',
    parameters: {
      appId: amplifyApp.appId,
      branchName: 'main',
    },
    physicalResourceId: PhysicalResourceId.of('AmplifyCustomResourceWebhook'),
  },
  policy: AwsCustomResourcePolicy.fromSdkCalls({
    resources: [`${amplifyApp.arn}/webhooks/*`],
  }),
});

// Outputs the secret deployment webhook
webhookCustomResource.getResponseField('webhook.webhookUrl');
Enter fullscreen mode Exit fullscreen mode

Other interesting features

Other configurations are available: custom response headers, custom rewrites and redirects or even basic authentication to prevent access to any test environment!

📚 Caveats with a monorepo

In a monorepo, additional configuration is required:

  • Add appRoot property in the build settings, with the frontend directory path.
  • Ensure node_modules are correctly included in the build. In the context of a monorepo, most package managers will put the node_modules at the root of your repository. A nice workaround, with pnpm, is to set the virtual store directory inside the frontend directory when installing the project: pnpm install --virtual-store-dir [your frontend directory]/node_modules/.pnpm

See swarmion full implementation example.

✨ Conclusion

If you plan to go on a monorepo architecture, you can already use Swarmion to create high scalable serverless application including Next.js latest version by simply running: pnpm create swarmion-app and choosing the Next.js option.

Swarmion will pre-configure all required settings so that your app will smoothly be deployed to amplify.

To complete, there are other Next.js deployment solutions which may be interesting to explore like:

  • Vercel (Next.js core team): probably the most user friendly infrastructure solution, if you can afford it on your project.
  • serverless-nextjs: very interesting initiative but seems no longer maintained unfortunately. The resources declared in this library are very similar to the one deployed by Amplify under the hood, which probably made it the cheapest solution around for a while.
  • OpenNext project which uses the NetJSite SST construct. A promising project, but still a work in progress. This one focuses on the connector between the Next.js framework and the infrastructure, but will not provide the infrastructure as code template.
  • JetBridge cdk-nextjs construct, another proposal to deploy Next.js with a construct.

Thanks a lot to @mamadoudicko, @alexandreperni4 and @Antoine Apollis who co-wrote this article and tested the setup process in this repository! We also relied a lot on this AWS blog article to build this guide.

Top comments (1)

Collapse
 
reacthunter0324 profile image
React Hunter

Nice article
thanks