Automating Next.js (App Router) Deployment on Amazon S3 using AWS CDK and CodePipeline

In the previous tutorial, we manually deployed a static Next.js App Router application to Amazon S3 and configured it with a CloudFront distribution. In this part, we'll automate the entire process using AWS CDK (Cloud Development Kit) for infrastructure as code, and set up AWS CodePipeline and CodeBuild for continuous deployment.

This tutorial will deploy all the required resources to host your Next.js app on Amazon S3 with a CloudFront distribution and a CodePipeline to automate the deployment process. You can safely delete all the resources we manually created in part 1.

By the end of this tutorial, you'll have an automated pipeline that builds and deploys your Next.js application to S3 whenever you push changes to your Git repository.

Prerequisites

  1. AWS Account: Ensure you have an AWS account with necessary permissions to create the resources we'll use.
  2. AWS CLI: Install and configure the AWS CLI with your credentials.
  3. AWS CDK: Install the AWS CDK by running npm install -g aws-cdk.
  4. Git Repository: Your Next.js App Router project should be in a Git repository (e.g., GitHub).
  5. Domain and SSL Certificate: A custom domain registered in Route 53 or another DNS provider, and an SSL certificate in AWS Certificate Manager (ACM) for the domain.

Overview

We'll accomplish the following:

  • Use AWS CDK to define and deploy the necessary AWS resources:
    • S3 bucket for hosting the static site
    • CloudFront distribution for CDN and HTTPS support
    • ACM certificate for SSL/TLS
    • Route 53 DNS records (if using Route 53)
  • Set up AWS CodePipeline and CodeBuild to automate the build and deployment process:
    • Automatically trigger on code commits
    • Build the Next.js application
    • Deploy the built assets to the S3 bucket
    • Invalidate the CloudFront cache

Step 1: Initialize the AWS CDK Project

First, we'll set up an AWS CDK project in your Next.js application directory.

1.1 Install AWS CDK

If you haven't installed the AWS CDK yet, install it globally:

npm install -g aws-cdk

1.2 Initialize CDK Project

Navigate to your project directory and initialize a new CDK TypeScript app:

cd your-nextjs-app
mkdir infra && cd infra
cdk init app --language typescript

This command creates a new CDK project in the infra directory with the necessary files and directories.

1.3 Install Required CDK Libraries

Install the AWS CDK libraries we'll need:

npm install @aws-cdk/aws-s3 @aws-cdk/aws-cloudfront @aws-cdk/aws-certificatemanager @aws-cdk/aws-route53 @aws-cdk/aws-route53-targets @aws-cdk/aws-codebuild @aws-cdk/aws-codepipeline @aws-cdk/aws-codepipeline-actions

Step 2: Define the Infrastructure with CDK

We'll now define the AWS resources using CDK.

2.1 Update bin/infra.ts

In the bin directory, update infra.ts to instantiate our stack:

#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { NextJsStaticSiteStack } from '../lib/nextjs-static-site-stack';

const app = new cdk.App();
new NextJsStaticSiteStack(app, 'NextJsStaticSiteStack', {
  env: { account: 'YOUR_ACCOUNT_ID', region: 'YOUR_REGION' },
});

Replace YOUR_ACCOUNT_ID and YOUR_REGION with your AWS account ID and preferred region.

2.2 Create the Stack in lib/nextjs-static-site-stack.ts

Create a new file lib/nextjs-static-site-stack.ts and define your stack:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
  aws_s3 as s3,
  aws_cloudfront as cloudfront,
  aws_certificatemanager as acm,
  aws_route53 as route53,
  aws_route53_targets as targets,
  aws_codebuild as codebuild,
  aws_codepipeline as codepipeline,
  aws_codepipeline_actions as cpactions,
} from 'aws-cdk-lib';

export class NextJsStaticSiteStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const domainName = 'yourdomain.com';
    const siteSubDomain = 'www';

    // Hosted Zone
    const zone = route53.HostedZone.fromLookup(this, 'Zone', { domainName });

    // TLS Certificate
    const certificate = new acm.DnsValidatedCertificate(this, 'SiteCertificate', {
      domainName: `${siteSubDomain}.${domainName}`,
      hostedZone: zone,
      region: 'us-east-1', // CloudFront certificates must be in us-east-1
    });

    // S3 Bucket
    const siteBucket = new s3.Bucket(this, 'SiteBucket', {
      bucketName: `${siteSubDomain}.${domainName}`,
      websiteIndexDocument: 'index.html',
      websiteErrorDocument: 'index.html',
      publicReadAccess: false,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // CloudFront Distribution
    const distribution = new cloudfront.CloudFrontWebDistribution(this, 'SiteDistribution', {
      originConfigs: [
        {
          s3OriginSource: {
            s3BucketSource: siteBucket,
          },
          behaviors: [{ isDefaultBehavior: true }],
        },
      ],
      viewerCertificate: cloudfront.ViewerCertificate.fromAcmCertificate(certificate, {
        aliases: [`${siteSubDomain}.${domainName}`],
        securityPolicy: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021,
        sslMethod: cloudfront.SSLMethod.SNI,
      }),
    });

    // Route53 Alias Record for CloudFront Distribution
    new route53.ARecord(this, 'SiteAliasRecord', {
      recordName: `${siteSubDomain}.${domainName}`,
      target: route53.RecordTarget.fromAlias(new targets.CloudFrontTarget(distribution)),
      zone,
    });

    // CodeBuild Project
    const project = new codebuild.PipelineProject(this, 'BuildProject', {
      buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec.yml'),
      environment: {
        buildImage: codebuild.LinuxBuildImage.STANDARD_7_0,
        environmentVariables: {
          DISTRIBUTION_ID: { value: distribution.distributionId },
          BUCKET_NAME: { value: siteBucket.bucketName },
        },
      },
    });

    // Grant S3 access to CodeBuild
    siteBucket.grantReadWrite(project);

    // Grant CloudFront invalidation permissions to CodeBuild
    project.addToRolePolicy(
      new cdk.aws_iam.PolicyStatement({
        actions: ['cloudfront:CreateInvalidation'],
        resources: [`arn:aws:cloudfront::${this.account}:distribution/${distribution.distributionId}`],
      })
    );

    // Source Artifact
    const sourceOutput = new codepipeline.Artifact();

    // CodePipeline
    new codepipeline.Pipeline(this, 'Pipeline', {
      stages: [
        {
          stageName: 'Source',
          actions: [
            new cpactions.GitHubSourceAction({
              actionName: 'GitHub_Source',
              owner: 'your-github-username',
              repo: 'your-repo-name',
              oauthToken: cdk.SecretValue.secretsManager('github-token'),
              output: sourceOutput,
              branch: 'main', // or your default branch
            }),
          ],
        },
        {
          stageName: 'BuildAndDeploy',
          actions: [
            new cpactions.CodeBuildAction({
              actionName: 'Build',
              project,
              input: sourceOutput,
            }),
          ],
        },
      ],
    });
  }
}

Important Notes:

  • Replace yourdomain.com with your actual domain.
  • Replace your-github-username and your-repo-name with your GitHub username and repository name.
  • Make sure you have stored your GitHub OAuth token in AWS Secrets Manager with the name github-token.
  • The buildspec.yml file will be created in the next step.
  • Ensure the AWS CDK libraries' versions are compatible with each other.

Step 3: Create the Build Specification File

In your project root (not the infra directory), create a buildspec.yml file for CodeBuild:

version: 0.2

phases:
  install:
    commands:
      - echo Installing dependencies...
      - npm install -g pnpm
      - pnpm install
  build:
    commands:
      - echo Building the application...
      - pnpm run build
  post_build:
    commands:
      - echo Deploying to S3...
      - aws s3 sync ./out s3://$BUCKET_NAME --delete
      - echo Invalidating CloudFront cache...
      - aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*"

artifacts:
  files:
    - '**/*'
  base-directory: './'

Explanation:

  • Install Phase: Installs dependencies using pnpm.
  • Build Phase: Runs the build script to generate static files in the out directory.
  • Post-build Phase: Syncs the out directory with the S3 bucket and invalidates the CloudFront cache.

Step 4: Deploy the CDK Stack

4.1 Bootstrap the AWS Environment

Before deploying, you may need to bootstrap your AWS environment:

cdk bootstrap aws://YOUR_ACCOUNT_ID/YOUR_REGION

4.2 Deploy the Stack

Deploy the CDK stack:

cdk deploy

This command will output the resources being created and may ask for confirmation. Type "y" to proceed.

Note: The initial deployment may take several minutes.

Step 5: Set Up GitHub Webhooks (If Using GitHub)

To trigger the pipeline on code commits, you need to set up webhooks.

  • The CDK automatically sets up a webhook if you use the GitHubSourceAction with an OAuth token.
  • Ensure your GitHub repository has the necessary permissions.

Step 6: Test the Pipeline

Commit and push a change to your repository to trigger the pipeline.

git add .
git commit -m "Test pipeline deployment"
git push origin main

Monitor the pipeline execution in the AWS Console under CodePipeline.

Step 7: Verify the Deployment

Once the pipeline execution is complete:

  • Visit https://www.yourdomain.com to see your deployed Next.js application.
  • Any subsequent pushes to the repository will trigger the pipeline and update the site.

Step 8: Clean Up (Optional)

If you want to delete the resources to avoid charges:

cdk destroy

Note: This will delete all resources created by the CDK stack.

Conclusion

You've successfully automated the deployment of your Next.js App Router application using AWS CDK and set up a continuous deployment pipeline with AWS CodePipeline and CodeBuild.

Benefits:

  • Infrastructure as Code: Manage and version your infrastructure alongside your application code.
  • Continuous Deployment: Automatically build and deploy changes, reducing manual intervention.
  • Scalability: Leverage AWS services that scale automatically with your application's needs.

Additional Considerations

  • Environment Variables: If your application requires environment variables, consider using AWS Secrets Manager or SSM Parameter Store.
  • Testing: Incorporate testing stages in your pipeline for better reliability.
  • Monitoring: Set up AWS CloudWatch or other monitoring tools to keep an eye on your pipeline and application health.

Disclaimer: Always ensure that you have the necessary permissions and understand the costs associated with the AWS services used in this tutorial.