A zero-config Terraform module for self-hosting Next.js sites serverless on AWS Lambda.
Some features are still under development, here is a list of features that are currently supported and what we plan to bring with the next releases:
- ✅ Next.js
v9.5+
&v10+
(older Versions might work but are not actively supported) - ✅ Terraform
v0.13+
- ✅ Static, SSG, Lambda and API pages (with dynamic routes)
- ✅ Automatic expiration of old static assets
- ✅ Rewrites & Redirects
- ✅ Image Component & Image Optimization support
- 🚧 Incremental Static Regeneration
- 🚧 AWS CodeDeploy
The Next.js Terraform module is designed as a full stack AWS app. It relies on multiple AWS services and connects them to work as a single application:
-
I.
CloudFrontThis is the main CloudFront distribution which handles all incoming traffic to the Next.js application. Static assets with the prefix
/_next/static/*
(e.g. JavaScript, CSS, images) are identified here and served directly from a static content S3 bucket (II
). Other requests are delegated to the proxy handler Lambda@Edge function (III
). -
II.
S3 bucket for static contentThis bucket contains the pre-rendered static HTML sites from the Next.js build and the static assets (JavaScript, CSS, images, etc.).
-
III.
Lambda@Edge proxy handlerThe proxy handler analyzes the incoming requests and determines from which source a request should be served. Static generated sites are fetched from the S3 bucket (
II
) and dynamic content is served from the Next.js Lambdas (V
). -
The HTTP API Gateway distributes the incoming traffic on the existing Next.js Lambdas (
V
). It uses a cost efficient HTTP API for this. -
V.
Shared Next.js Lambda functionsThese are the Next.js Lambdas which are doing the server-side rendering. They are composed, so a single lambda can serve multiple SSR-pages.
-
Terraform Next.js Image Optimization
The image optimization is triggered by routes with the prefix
/_next/image/*
. It is a serverless task provided by our Terraform Next.js Image Optimization module for AWS. -
Static Content Deployment
This flow is only triggered when a Terraform apply runs to update the application. It consists of a dedicated S3 bucket and a single Lambda function. The bucket is only used by Terraform to upload the static content from the
tf-next build
command as a zip archive. The upload then triggers the Lambda which unzips the content and deploys it to the static content S3 bucket (II
). Static assets from previous deployments are then marked to be expired in a certain amount of days (default 30, configurable viaexpire_static_assets
variable). After the successful deployment a CloudFront invalidation is created to propagate the route changes to every edge location. -
Proxy Config Distribution
This is a second CloudFront distribution that serves a special JSON file that the Proxy (
III
) fetches as configuration (Contains information about routes).
First add our custom builder to your Next.js project. It uses the same builder under the hood as Vercel does:
npm i -D tf-next # npm or
yarn add -D tf-next # yarn
Then you should add a new script to your package.json (Make sure it is not named build
):
{
...
"scripts": {
"dev": "next",
"build": "next build",
"start": "next start",
+ "tf-next": "tf-next build"
}
...
}
tf-next build
runs in a temporary directory and puts its output in a .next-tf
directory in the same directory where your package.json
is.
The output in the .next-tf
directory is all what the Terraform module needs in the next step.
Note: Make sure that the
AWS_ACCESS_KEY_ID
&AWS_SECRET_ACCESS_KEY
environment variables are set when running the Terraform commands. How to create AWS Access Keys?
Adding Terraform to your existing Next.js installation is easy.
Simply create a new main.tf
file in the root of your Next.js project and add the following content:
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
# Main region where the resources should be created in
# (Should be close to the location of your viewers)
provider "aws" {
region = "us-west-2"
}
# Provider used for creating the Lambda@Edge function which must be deployed
# to us-east-1 region (Should not be changed)
provider "aws" {
alias = "global_region"
region = "us-east-1"
}
module "tf_next" {
source = "dealmore/next-js/aws"
providers = {
aws.global_region = aws.global_region
}
}
output "cloudfront_domain_name" {
value = module.tf_next.cloudfront_domain_name
}
To deploy your app to AWS simply run the following commands:
npm run tf-next # Build the Next.js app
yarn tf-next # Same command when using yarn
# Expose your AWS Access Keys to the current terminal session
# Only needed when running Terraform commands
export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE
export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
terraform init # Only needed on the first time running Terraform
terraform plan # (Optional) See what resources Terraform will create
terraform apply # Deploy the Next.js app to your AWS account
> Apply complete!
>
> Outputs:
>
> cloudfront_domain_name = "xxxxxxxxxxxxxx.cloudfront.net"
After the successful deployment your Next.js app is publicly available at the CloudFront subdomain from the cloudfront_domain_name
output.
When using this module together with Terraform Cloud make sure that you also upload the build output from the tf-next
task.
You can create a .terraformignore
in the root of your project and add the following line:
# .terraformignore
+ !**/.next-tf/**
- Complete - Complete example with SSR, API and static pages.
- Static - Example that uses static pages only (No SSR).
- Custom Domain - Demonstrates how to use the module with a custom domain from Route 53.
Name | Version |
---|---|
terraform | >= 0.13 |
aws | >= 3.28.0 |
random | >= 2.3.0 |
Name | Version |
---|---|
aws | >= 3.28.0 |
random | >= 2.3.0 |
Name | Description | Type | Default | Required |
---|---|---|---|---|
cloudfront_cache_key_headers | Header keys that should be used to calculate the cache key in CloudFront. | list(string) |
[ |
no |
cloudfront_custom_behaviors | n/a | list(any) |
null |
no |
cloudfront_geo_restriction | Options to control distribution of content, object with restriction_type and locations. | object({ |
{ |
no |
cloudfront_minimum_protocol_version | Minimum version of the SSL protocol that you want CloudFront to use for HTTPS connections. One of SSLv3, TLSv1, TLSv1_2016, TLSv1.1_2016, TLSv1.2_2018 or TLSv1.2_2019. | string |
"TLSv1.2_2019" |
no |
cloudfront_origins | n/a | list(any) |
null |
no |
cloudfront_price_class | Price class for the CloudFront distributions (main & proxy config). One of PriceClass_All, PriceClass_200, PriceClass_100. | string |
"PriceClass_100" |
no |
cloudfront_viewer_certificate_arn | n/a | string |
null |
no |
create_domain_name_records | Controls whether Route 53 records for the for the domain_names should be created. | bool |
true |
no |
create_image_optimization | Controls whether resources for image optimization support should be created or not. | bool |
true |
no |
debug_use_local_packages | Use locally built packages rather than download them from npm. | bool |
false |
no |
deployment_name | Identifier for the deployment group (alphanumeric characters, underscores, hyphens, slashes, hash signs and dots are allowed). | string |
"tf-next" |
no |
domain_names | Alternative domain names for the CloudFront distribution. | list(string) |
[] |
no |
domain_zone_names | n/a | list(string) |
[] |
no |
expire_static_assets | Number of days after which static assets from previous deployments should be removed from S3. Set to -1 to disable expiration. | number |
30 |
no |
lambda_environment_variables | Map that defines environment variables for the Lambda Functions in Next.js. | map(string) |
{} |
no |
lambda_memory_size | Amount of memory in MB a Lambda Function can use at runtime. Valid value between 128 MB to 10,240 MB, in 1 MB increments. | number |
1024 |
no |
lambda_policy_json | Additional policy document as JSON to attach to the Lambda Function role | string |
null |
no |
lambda_role_permissions_boundary | ARN of IAM policy that scopes aws_iam_role access for the lambda | string |
null |
no |
lambda_runtime | Lambda Function runtime | string |
"nodejs14.x" |
no |
lambda_timeout | Max amount of time a Lambda Function has to return a response in seconds. Should not be more than 30 (Limited by API Gateway). | number |
10 |
no |
next_tf_dir | Relative path to the .next-tf dir. | string |
"./.next-tf" |
no |
tags | Tag metadata to label AWS resources that support tags. | map(string) |
{} |
no |
use_awscli_for_static_upload | Use AWS CLI when uploading static resources to S3 instead of default Bash script. Some cases may fail with 403 Forbidden when using the Bash script. | bool |
false |
no |
Name | Description |
---|---|
cloudfront_domain_name | The domain of the main CloudFront distribution. |
cloudfront_hosted_zone_id | The zone id of the main CloudFront distribution. |
static_upload_bucket_id | n/a |
Under the hood this module uses a lot of Vercel's build pipeline. So issues that exist on Vercel are likely to occur on this project too.
-
Stack deletion (
terraform destroy
) fails on first run (terraform-provider-aws#1721)This is intentional because we cannot delete a Lambda@Edge function (Used by proxy module) in a synchronous way. It can take up to an hour for AWS to unbind a Lambda@Edge function from it's CloudFront distribution even when the distribution is already destroyed.
Workaround:
After running the initial
terraform destroy
command (that failed) wait ~1 hour and run the command again. This time it should run successfully and delete the rest of the stack.
Apache-2.0 - see LICENSE for details.
Note: All sample projects in
examples/*
are licensed as MIT to comply with the official Next.js examples.