Deploy Nextjs 13 app to AWS Lambda using AWS Serverless Application Model

nextjs aws lambda

Learn how to deploy your Next.js application to AWS Lambda using the AWS SAM framework and aws lambda web adapter. Simplify your deployment process and leverage the benefits of serverless architecture on AWS.


Next.js, the widely adopted React-based framework developed by Vercel, is a popular choice among companies for their web development needs. In this technical blog, we provide a detailed guide on how to deploy a server-side rendered Nextjs application on AWS Lambda using AWS SAM. AWS Lambda is a serverless service offered by Amazon Web Services that eliminates the need for infrastructure maintenance and can automatically scale depending on traffic. Although Next.js previously supported serverless deployment mode, this functionality was deprecated from version 12 onwards.

In this technical blog, we will be utilizing the AWS Serverless Application Model (SAM) for deployment and will provide a comprehensive walkthrough of each component associated with AWS SAM and its usage. The blog was written for the following versions of applications,

  • Next.js: 13.2
  • Node: 18.14.0
  • Docker: 20.10.8
  • AWS SAM CLI: 1.74.0

NextJs Serverless AWS Lambda Architecture

Next 3 Serverless Lambda Architecture

In this serverless architecture we are going to leverage following AWS Services,

  • AWS Lambda
  • AWS API Gateway
  • AWS S3
  • AWS Cloud Front

The build process of Next.js generates build artifacts within the .next directory. In our system architecture, the files related to server-side rendering (SSR) are intended to be deployed to AWS Lambda and served via AWS API Gateway.

We are going to use AWS Lambda Web Adapter to run the web apps on Lambda.

NextJs Static Files Hosted in AWS S3 Bucket

For our system, the AWS S3 bucket is the designated hosting location for static files such as JS, CSS, and images. These files are served via CloudFront, which enables caching at edge locations and facilitates faster delivery of static content.

When routing traffic, CloudFront directs requests based on the URL. In our setup, any static file with a URL containing _next is directed to the S3 bucket, while all other requests are directed to AWS API Gateway by default.

AWS Lambda Web Adapter: Lambda Layer for running WebApps

In our solution, we utilize the AWS Lambda web adapter written in Rust, which is based on the AWS Lambda Rust Runtime, to run our web application. This adapter is capable of supporting AWS Lambda functions triggered by Amazon API Gateway Rest API, Http API (v2 event format), and Application Load Balancer. The Lambda Adapter serves to convert incoming events to HTTP requests and send them to the web application, then it converts the HTTP response back to a Lambda event response.

You can read more about the aws web lambda adapter in their github.

AWS SAM Configuration for NextJs Serverless deployment

Now, let's take a closer look at the AWS SAM template that we will use to deploy our Next.js application.

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  NextJs Serverless AWS Lambda using SAM CLI

Parameters:
  NextBucketName:
    Type: String
    Description: Bucket name for Next.js static resources
  
# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3
    Tracing: Active
  Api:
    TracingEnabled: True

Resources:
  NextFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: ./
      Handler: run.sh
      Runtime: nodejs18.x
      MemorySize: 512
      Architectures:
        - x86_64
      Environment:
        Variables:
          AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap
          RUST_LOG: info
          PORT: 8080
      Layers:
        - !Sub 'arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:13'
      Events:
        RootPath:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /
            Method: ANY
        AnyPath:
          Type: Api
          Properties:
            Path: /{proxy+}
            Method: ANY
    Metadata:
      BuildMethod: makefile
      
  NextBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Ref NextBucketName
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      LoggingConfiguration:
        DestinationBucketName: !Ref NextLoggingBucket
        LogFilePrefix: s3-access-logs
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: 'AES256'

  NextBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref NextBucket
      PolicyDocument:
        Id: NextBucketPolicy
        Version: 2012-10-17
        Statement:
          - Action:
              - 's3:GetObject'
            Effect: Allow
            Principal:
              CanonicalUser: !GetAtt NextOriginAccessIdentity.S3CanonicalUserId
            Resource: !Join
              - ''
              - - 'arn:aws:s3:::'
                - !Ref NextBucket
                - /*

  NextLoggingBucket:
    Type: 'AWS::S3::Bucket'
    Properties:
      BucketName: !Sub '${NextBucketName}-logs'
      PublicAccessBlockConfiguration:
        BlockPublicAcls : true
        BlockPublicPolicy : true
        IgnorePublicAcls : true
        RestrictPublicBuckets : true
      AccessControl: LogDeliveryWrite
      VersioningConfiguration:
        Status: Enabled
      BucketEncryption:
        ServerSideEncryptionConfiguration:
        - ServerSideEncryptionByDefault:
            SSEAlgorithm: 'AES256'
    DeletionPolicy: Delete

  NextOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: OAI for Next static resources in S3 bucket

  NextDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Origins:
        - Id: nextS3Origin
          DomainName: !GetAtt NextBucket.RegionalDomainName
          S3OriginConfig:
            OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/${NextOriginAccessIdentity}'
        - Id: nextAPIGatewayOrigin
          DomainName: !Sub '${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com'
          OriginPath: '/Prod'
          CustomOriginConfig:
            HTTPSPort: '443'
            OriginProtocolPolicy: https-only
        Enabled: 'true'
        Comment: 'Next.js Distribution'
        HttpVersion: http2
        DefaultRootObject: ''
        DefaultCacheBehavior:
          TargetOriginId: nextAPIGatewayOrigin
          CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad # 
          ForwardedValues:
            QueryString: 'true'
            Cookies:
              Forward: all
          Compress: 'true'
          AllowedMethods:
          - DELETE
          - GET
          - HEAD
          - OPTIONS
          - PATCH
          - POST
          - PUT
          ViewerProtocolPolicy: redirect-to-https
          MaxTTL: '31536000'
        CacheBehaviors:
        - PathPattern: '/_next/static/*'
          TargetOriginId: nextS3Origin
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # 
          AllowedMethods:
          - GET
          - HEAD
          ForwardedValues:
            QueryString: 'false'
            Cookies:
              Forward: none
          Compress: 'true'
          ViewerProtocolPolicy: https-only
        - PathPattern: '/static/*'
          TargetOriginId: nextS3Origin
          CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6 # 
          AllowedMethods:
          - GET
          - HEAD
          ForwardedValues:
            QueryString: 'false'
            Cookies:
              Forward: none
          Compress: 'true'
          ViewerProtocolPolicy: https-only
        PriceClass: PriceClass_100
        ViewerCertificate:
          CloudFrontDefaultCertificate: 'true'
        Logging:
          Bucket: !GetAtt NextLoggingBucket.RegionalDomainName
          Prefix: 'cloudfront-access-logs'

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  NextApi:
    Description: "API Gateway endpoint URL for Prod stage for Next function"
    Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/'
  NextFunction:
    Description: "Next Lambda Function ARN"
    Value: !GetAtt NextFunction.Arn
  NextFunctionIamRole:
    Description: "Implicit IAM Role created for Next function"
    Value: !GetAtt NextFunctionRole.Arn
  NextBucket:
    Description: "S3 bucket for Next static resources"
    Value: !GetAtt NextBucket.Arn
  NextDistribution:
    Description: "CloudFront distribution for Next.js"
    Value: !GetAtt NextDistribution.DomainName

Using the aforementioned SAM template, we will create a total of six AWS resources and are listed below,

  • AWS::Serverless::Function - Created the AWs Lambda where the Next.js application runs
  • AWS::S3::Bucket - S3 bucket created for storing all the static files likes css,js and images
  • AWS::S3::BucketPolicy - S3 bucket policy to allow the access only to cloudfront.
  • AWS::S3::Bucket - S3 bucket to store all the cloudfront logs
  • AWS::CloudFront::CloudFrontOriginAccessIdentity - Cloud front origin access identity to provide the access for static resources in S3 bucket
  • AWS::CloudFront::Distribution - Cloud front distribution for the web app.

Lets review the lambda configuration details to understand it better as it is required for the deployment of next application.

AWS Lambda SAM Configuration

In the Lambda configuration, we utilize a custom runtime and the AWS Lambda Web Adapter Lambda layer to facilitate the execution of our Next.js application.

NextFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: ./
      Handler: run.sh
      Runtime: nodejs18.x
      MemorySize: 512
      Architectures:
        - x86_64
      Environment:
        Variables:
          AWS_LAMBDA_EXEC_WRAPPER: /opt/bootstrap
          RUST_LOG: info
          PORT: 8080
      Layers:
        - !Sub 'arn:aws:lambda:${AWS::Region}:753240598075:layer:LambdaAdapterLayerX86:13'
      Events:
        RootPath:
          Type: Api
          Properties:
            Path: /
            Method: ANY
        AnyPath:
          Type: Api
          Properties:
            Path: /{proxy+}
            Method: ANY
    Metadata:
      BuildMethod: makefile

For our custom runtime, we have selected nodejs18 as the programming language. When the Lambda function is invoked, this runtime executes the function's handler method which is a shell script.

The Metadata section of our configuration includes a makefile that outlines the build method for the custom runtime. During the sam build process, the makefile executes the following steps:

install - This step installs all packages specified in the package.json file. build - This step initiates the Next.js build process to generate artifacts within the .next folder. artifacts - This step copies the contents of the .next folder to the .aws-sam/build folder.

# to install all the packages mentioned in the package.json
install:
    npm install

# Run the Next.js Build Process to generate the artifacts in the `.next` folder
build:
    npm run build

# Last steps to copy the contents of .next folder to artifacts directory under the .aws-sam folder
artifacts:  
    # Copy artifacts for deployment
    cp -r .next/* $(ARTIFACTS_DIR)
    cp run.sh $(ARTIFACTS_DIR)

build-NextFunction: install build artifacts

The AWS Lambda Web Adapter and its version is mentioned in the Layer information. When an the lambda is invoked by the API,this lambda web adapter will launch the application and perform the readiness check on http://localhost:8080/ every 10ms. It will start lambda runtime client after receiving 200 response from the application and forward requests to http://localhost:8080.

Within the Events section of our configuration, we have set the Lambda invocation to be performed by the AWS API Gateway Rest API using the default path. This configuration creates an API Gateway and attaches it as a trigger for the Lambda function.

The handler file for this Lambda function is run.sh, which is a shell script containing the following command to invoke the Next server.js file:

#!/bin/bash

node standalone/server.js

Deploying your Next.js App to AWS

Now, let's review the deployment steps for the Next.js serverless Lambda function. These steps can be found within the deploy.sh shell script.

#!/bin/bash
set -e
# Build the application using sam build
sam build
# Deploy the application to AWS using sam deploy
sam deploy --stack-name next-lambda-ssr --capabilities CAPABILITY_IAM --resolve-s3 --parameter-overrides NextBucketName=bucket-name  --profile profile-name --region us-east-1
# Copy the js, css and other static files to s3 bucket
aws s3 cp .aws-sam/build/NextFunction/static/ s3://bucket-name/_next/static/ --recursive --profile profile-name
# Copy the public folder to s3 bucket as Next build dont include public folder during the build process
aws s3 cp public/static s3://bucket-name/static/ --recursive --profile profile-name

The first step of the deployment process is to run sam build, which generates the Next artifacts. As explained earlier, the sam build command executes the makefile and creates the artifacts within the .aws-sam/build directory.

The second step is to run sam deploy, where we provide the S3 bucket name as a parameter along with the AWS profile, AWS region, and stack name. The parameters --capabilities CAPABILITY_IAM --resolve-s3 are passed to ensure that SAM takes care of the IAM permissions and S3 bucket creation for storing the CloudFormation templates of this stack. Once this step is completed successful,it should create all the services mentioned under the Resources section of the AWS SAM template and upload the build artifacts to Lambda. The output of this step should return cloudfront url, rest api url and the bucket name. You can hit the cloudfront url on browser and see if you are getting the page loaded. Please ensure the user associated with the profile has the sufficent privilege to create all the resources mentioned in the SAM template.

The third and fourth steps involve copying the static files such as CSS, JS, and images to the S3 bucket that was created after running the sam deploy command. The fourth step is necessary because the Next.js build process does not copy the public folder to the .next folder.

Sample Repo for Next.JS Serverless AWS Lambda

You can clone the sample repo for Next.js AWS Lambda implementation from github.

Furthermore, you can explore how to host nextjs static website on an aws s3 and cloudfront in this newly published blog post. Incase you are looking to deploy a nextjs app with the docker,please read our new blog post.