cdk-nextjs-standalone

repository·main·Indexed 18 days ago

https://github.com/jetbridge/cdk-nextjs

A CDK construct for deploying Next.js applications to AWS using 'standalone' build mode. It provides a production-ready architecture featuring SSR, API support, and static asset hosting via CloudFront, S3, and Lambda. The library includes specialized constructs such as Nextjs for high-level deployment, NextjsBuild for artifact generation, NextjsDistribution for CloudFront configuration, and NextjsDomain for managing Route53 and ACM certificates.

Tokens
44.5K
Snippets
122
Records
179
Agent score
61%

What's inside cdk-nextjs-standalone

  1. Configure weighted and geo-location routing properties

    main

    When using advanced routing in Route 53 via OptionalARecordProps, certain properties are interdependent:

    • setIdentifier: This is a required string (1-128 characters) used to distinguish between different records that share the same DNS name and type. You must set this if you are using either weight or geoLocation.
    • weight: A number between 0 and 255 used for weighted routing. Route 53 calculates the ratio of this weight against the sum of all weights for the same name/type to determine query distribution.
    • geoLocation: Used to return DNS records based on the user's geographical location.
    • ttl: The resource record cache time to live. Defaults to Duration.minutes(30) if not specified.
  2. Handle PNPM Monorepo Symlinks

    main

    If you are using a PNPM Monorepo, be aware that CDK Assets do not natively support symlinks between workspace node_modules and the top-level node_modules.

    To resolve this, the library:

    1. Manually zips the assets to include symlinked files.
    2. Uses nextjs-bucket-deployment.ts logic to unzip and re-zip symlinks within Lambda Custom Resources (specifically for ServerFnBucketDeployment).
  3. Understand the Next.js Code Deployment Flow

    main

    The Nextjs construct automates the deployment of Next.js applications to AWS by orchestrating several specialized sub-constructs. The flow follows a specific order to ensure that environment variable placeholders are correctly resolved before assets are served.

    1. Build Phase (NextjsBuild): Runs npx open-next build in your repository. This generates the .next folder and uses open-next to create handler code for the server, image optimization, revalidation, and warmer functions.
      • Environment Variables: It injects process environment variables, NextjsProps.environment, and Nextjs.nodeEnv.
      • Token Handling: Unresolved CDK tokens (e.g., ${TOKEN[Bucket.Name.1234]}) are replaced with placeholders like {{ BUCKET_NAME }} to be resolved later by a CloudFormation Custom Resource.
    2. Static Assets Phase (NextjsStaticAssets): Creates an S3 bucket and deploys static assets.
      • Placeholder Substitution: Uses a NextjsBucketDeployment (a CloudFormation Custom Resource) to download assets from the CDK Assets bucket, replace placeholders with resolved values, and upload them to the target bucket.
      • Security: Only NEXT_PUBLIC environment variable placeholders are passed to substitutionConfig; private server variables are not included in static assets.
    3. Server Phase (NextjsServer): Deploys the Next.js server code via Lambda.
      • Token Handling: Replaces all (public and private) unresolved tokens within the open-next generated server function code.
      • ISR Support: Automatically adds CACHE_BUCKET_NAME, CACHE_BUCKET_KEY, and CACHE_BUCKET_REGION to support Incremental Static Regeneration.
      • Bundling: Uses esbuild to bundle the Lambda code.
    4. Optimization & Revalidation (NextjsImage & NextjsRevalidation): Deploys Lambda functions using the bundled code from open-next to handle image optimization and revalidation.
    5. Cache Invalidation (NextjsInvalidation): Invalidates the CloudFront Distribution. This step explicitly depends on the completion of NextjsStaticAssets, NextjsServer, and NextjsImage to ensure the cache is only cleared once new assets are fully deployed.
  4. How JSII partial types are generated

    main

    Because of JSII limitations regarding TypeScript's Partial<T> utility type, this project uses @mrgrain/jsii-struct-builder to generate partial versions of AWS CDK interfaces and internal structs.

    These generated types (e.g., for NextjsOverrides) are located in the src/generated-structs folder and are defined within the .projenrc.ts configuration.

  5. How cdk-nextjs-standalone works

    main

    The construct deploys a NextJS static site with server-side rendering (SSR) and API support using AWS Lambda and CloudFront.

    It leverages the NextJS standalone output mode (available in NextJS >= 12.3.0) which uses output tracing to generate a minimal server and static files. The architecture follows this flow:

    1. CloudFront Distribution: Acts as the entry point.
    2. S3 Origin: CloudFront first checks S3 for static files.
    3. Lambda Fallback: If no static file is found, CloudFront falls back to an HTTP origin using a Lambda Function URL to handle SSR, API routes, and routing.
  6. Quickstart: Deploy a NextJS app with cdk-nextjs-standalone

    main

    To deploy a NextJS application using AWS CDK, use the Nextjs construct from cdk-nextjs-standalone. This construct requires the nextjsPath option, which is the relative path from your project root to your NextJS application directory. The deployment uses NextJS's standalone output mode to provide SSR, API support, and static file hosting via CloudFront and S3.

    import { App, Stack, StackProps, CfnOutput } from 'aws-cdk-lib';
    import { Construct } from 'constructs';
    import { Nextjs } from 'cdk-nextjs-standalone';
    
    class WebStack extends Stack {
      constructor(scope: Construct, id: string, props?: StackProps) {
        super(scope, id, props);
        const nextjs = new Nextjs(this, 'Nextjs', {
          nextjsPath: './web', // relative path from your project root to NextJS
        });
        new CfnOutput(this, 'CloudFrontDistributionDomain', {
          value: nextjs.distribution.distributionDomain,
        });
      }
    }
    
    const app = new App();
    new WebStack(app, 'web');
  7. Manage dependencies using Projen

    main

    This project uses Projen for project configuration. Do not manually update package.json or use yarn add to manage dependencies.

    To update dependencies:

    1. Modify the dependencies in .projenrc.ts.
    2. Run yarn projen to apply the changes and regenerate project files.
    yarn projen
  8. Implement a Custom Resource Provider

    main

    When using OptionalCustomResourceProps, you can provide a serviceToken using several methods:

    Use the aws-cdk/custom-resources module to create a robust provider.

    const provider = new customresources.Provider(this, 'ResourceProvider', {
      onEventHandler,
      isCompleteHandler, // optional
    });
    
    new CustomResource(this, 'MyResource', {
      serviceToken: provider.serviceToken,
    });

    2. AWS Lambda Function

    Directly use a Lambda function ARN (not recommended to use raw Lambda functions directly).

    new CustomResource(this, 'MyResource', {
      serviceToken: myFunction.functionArn,
    });

    3. SNS Topic

    Publish lifecycle events to an SNS topic (not recommended to use raw SNS topics directly).

    new CustomResource(this, 'MyResource', {
      serviceToken: myTopic.topicArn,
    });
    // Example using the CDK Provider framework
    import * as customresources from 'aws-cdk-lib/custom-resources';
    import { CustomResource } from 'aws-cdk-lib';
    
    const provider = new customresources.Provider(this, 'ResourceProvider', {
      onEventHandler: myHandler,
    });
    
    new CustomResource(this, 'MyResource', {
      serviceToken: provider.serviceToken,
    });
  9. Configure CloudWatch logging for Edge Functions

    main

    When using OptionalEdgeFunctionProps, you can control how logs are handled.

    Recommended Approach: Instead of using deprecated properties like logRetention or logRemovalPolicy, create a custom LogGroup using aws-cdk-lib/aws-logs and pass it to the logGroup property. This allows you to fully customize properties like retention and removal policies, which is not possible with the auto-created default log group.

    Note on Regional Availability: Providing a user-controlled log group was rolled out to commercial regions on 2023-11-16. Check your region's availability before deploying.

    import * as logs from 'aws-cdk-lib/aws-logs';
    
    // Create a custom log group
    const myLogGroup = new logs.LogGroup(this, 'MyLogGroup', {
      retention: logs.RetentionDays.ONE_WEEK,
    });
    
    // Pass it to the Edge Function props
    // (Assuming the context of the construct using OptionalEdgeFunctionProps)
    const props: OptionalEdgeFunctionProps = {
      logGroup: myLogGroup,
      // ... other props
    };
  10. Deploy an example app manually

    main

    To manually deploy an example application to AWS for testing, follow these steps:

    1. Navigate to the desired example directory (e.g., app-router or any other example folder).
    2. Install the local dependencies: pnpm install
    3. Ensure your AWS credentials are injected into your terminal environment.
    4. Deploy using CDK: cdk deploy
    cd app-router # or any other example
    pnpm install
    # Ensure AWS credentials are set
    cdk deploy
  11. Configure custom CloudWatch LogGroups for Lambda

    main

    By default, Lambda functions use an auto-created log group (/aws/lambda/${this.functionName}) which prevents you from customizing properties like log retention via CDK. To use a fully customizable LogGroup, create a logs.LogGroup instance and pass it to the logGroup property.

    Note: Migrating from the deprecated logRetention property to logGroup will change the log group name. If your code or external tools reference the log group name verbatim, they must be updated to use the name from the new LogGroup construct.

    import * as logs from 'aws-cdk-lib/aws-logs';
    
    // Create a custom log group
    const myLogGroup = new logs.LogGroup(this, 'MyLogGroup', {
      retention: logs.RetentionDays.ONE_WEEK,
    });
    
    // Pass it to the function props
    // (Assuming the context of a construct accepting OptionalFunctionProps)
    const props = {
      logGroup: myLogGroup,
    };
  12. Use NextjsDomain to configure a custom domain

    main

    The NextjsDomain construct simplifies the process of using a custom domain with Nextjs. It manages the interdependencies between a Route53 Hosted Zone, a CloudFront Distribution, and the necessary Route53 Hosted Zone Records.

    Requirements:

    • A Route53 hosted zone must exist within the same AWS account.
    • If you use a different service for domain registration (like Cloudflare), you must still create a Route53 hosted zone and configure DNS delegation.

    Alternative for external DNS: If you cannot use a Route53 hosted zone in the same account, use the overrides.nextjsDistribution.distributionProps property within NextjsProps to customize the distribution.

    import { NextjsDomain } from 'cdk-nextjs-standalone';
    
    // Initializing NextjsDomain
    new NextjsDomain(scope, id, {
      domainName: 'example.com',
      alternateNames: ['www.example.com'],
      certificate: myCertificate, // aws-cdk-lib.aws_certificatemanager.ICertificate
      hostedZone: myHostedZone,   // aws-cdk-lib.aws_route53.IHostedZone
    });