CLI build tool for AWS Next.js Terraform module.


Keywords
aws, nextjs, serverless, terraform-module
License
Apache-2.0
Install
npm install tf-next@1.0.0-canary.5

Documentation

Note
The main branch currently contains the atomic deployments alpha preview.
For the lastest stable release, check out the v0.x branch.

Please see our blog post "The road to Atomic Deployments"
or watch the latest release review for more information:


Terraform Next.js module for AWS

CI status

A zero-config Terraform module for self-hosting Next.js sites serverless on AWS Lambda.

Features

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:

Architecture

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:

Architecture overview diagram

Usage

Prerequisites

You should have the following tools installed:

Note: Additionally we assume here that you already have a public Route53 Hosted Zone associated with your AWS account.

This is a requirement in the preview phase of atomic deployments, where each deployment gets a unique subdomain assigned. It will change once atomic deployments become generally available.

Setup the Next.js Terraform module

The Terraform module contains the system that is later used for creating new deployments and managing the aliases (domains) for your Next.js app(s). Creating the Terraform stack is only required on initial setup and creates the global resources (CloudFront distributions, DynamoDB tables, S3 storage) that is used for handling incoming requests to your website.

Create a new main.tf file in an empty folder (or add it to your existing Terraform stack) and add the following content:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.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"
}

###########
# Variables
###########

variable "custom_domain" {
  description = "Your custom domain"
  type        = string
  default     = "example.com"
}

# Assuming that the ZONE of your domain is already available in your AWS account (Route 53)
# https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/AboutHZWorkingWith.html
variable "custom_domain_zone_name" {
  description = "The Route53 zone name of the custom domain"
  type        = string
  default     = "example.com."
}

########
# Locals
########

locals {
  # A wildcard domain(ex: *.example.com) has to be added when using atomic deployments:
  aliases = [var.custom_domain, "*.${var.custom_domain}"]
}

#######################
# Route53 Domain record
#######################

# Get the hosted zone for the custom domain
data "aws_route53_zone" "custom_domain_zone" {
  name = var.custom_domain_zone_name
}

# Create a new record in Route 53 for the domain
resource "aws_route53_record" "cloudfront_alias_domain" {
  for_each = toset(local.aliases)

  zone_id = data.aws_route53_zone.custom_domain_zone.zone_id
  name    = each.key
  type    = "A"

  alias {
    name                   = module.tf_next.cloudfront_domain_name
    zone_id                = module.tf_next.cloudfront_hosted_zone_id
    evaluate_target_health = false
  }
}

##########
# SSL Cert
##########

# Creates a free SSL certificate for CloudFront distribution
# For more options (e.g. multiple domains) see:
# https://registry.terraform.io/modules/terraform-aws-modules/acm/aws/
module "cloudfront_cert" {
  source  = "terraform-aws-modules/acm/aws"
  version = "~> 3.0"

  domain_name               = var.custom_domain
  zone_id                   = data.aws_route53_zone.custom_domain_zone.zone_id
  subject_alternative_names = slice(local.aliases, 1, length(local.aliases))

  wait_for_validation = true

  tags = {
    Name = "CloudFront ${var.custom_domain}"
  }

  # CloudFront works only with certs stored in us-east-1
  providers = {
    aws = aws.global_region
  }
}

##########################
# Terraform Next.js Module
##########################

module "tf_next" {
  source  = "milliHQ/next-js/aws"
  version = "1.0.0-canary.4"

  cloudfront_aliases             = local.aliases
  cloudfront_acm_certificate_arn = module.cloudfront_cert.acm_certificate_arn

  deployment_name = "atomic-deployments"

  enable_multiple_deployments      = true
  multiple_deployments_base_domain = "*.${var.custom_domain}"

  providers = {
    aws.global_region = aws.global_region
  }
}

#########
# Outputs
#########

output "api_endpoint" {
  value = module.tf_next.api_endpoint
}

output "api_endpoint_access_policy_arn" {
  value = module.tf_next.api_endpoint_access_policy_arn
}

To create the resources in your AWS account, run the following commands:

terraform init    # Only needed on the first time running Terraform

terraform plan    # (Optional) See what resources Terraform will create
terraform apply   # Create the resources in your AWS account

> Apply complete!
>
> Outputs:
>
> api_endpoint = "https://<api-id>.execute-api.us-west-2.amazonaws.com"
> api_endpoint_access_policy_arn = "arn:aws:iam::123456789012:policy/access-api"

The api_endpoint is later used by the CLI tool to create new deployments.

With the api_endpoint_access_policy_arn AWS policy you can create new users (and assign that policy) that only can use the CLI tool tf-next but cannot access other resources inside of your AWS account.

After the successful deployment your Next.js app is publicly available at the CloudFront subdomain from the cloudfront_domain_name output.

Deploy a Next.js App

For building and deploying Next.js apps to the system we created a CLI tool called tf-next.

It is a npm package that can be installed with:

npm i -g tf-next@canary

Next, we need to build the Next.js so that it can run in a serverless environment (with AWS Lambda). This is archived by running tf-next build in the same directory where your Next.js app is located (Right where your package.json or next.config.js files are located):

tf-next build

> All serverless functions created in: 20.791ms
> 1752924 total bytes
> Build successful!

Now deploy the Next.js app by running tf-next deploy from the same directory. The deploy command communicates through a secured (and authenticated with your AWS credentials) API with the Terraform module.

To tell the command where to deploy the app, an additional --endpoint flag must be provided, which should use the value from the api_endpoint output from the terraform apply step:

tf-next deploy --endpoint https://<api-id>.execute-api.us-west-2.amazonaws.com

> Available at: https://3edade7a2bf7bb0343699af6b851bbfa.example.com/

The preview deployment can now be accessed by the displayed url.
To make the deployment available from a more readable url, you can use the tf-next alias subcommand:

tf-next alias set my-app.example.com 3edade7a2bf7bb0343699af6b851bbfa.example.com

> Available at: https://my-app.example.com/

For a full list of available commands that can be used with tf-next, check the command reference.

Examples

  • Atomic Deployments
    Each deployment gets a unique url from where it can be previewed.
  • Complete
    Complete example with SSR, API and static pages.
  • Static
    Example that uses static pages only (No SSR).
  • Next Image
    Images are optimized on the fly by AWS Lambda.
  • Existing CloudFront
    Use the module together with an existing CloudFront distribution that can be fully customized.
  • Custom Domain
    Use the module with your own domain from Route 53.

Requirements

Name Version
terraform >= 0.15
aws >= 4.8

Providers

Name Version
aws >= 4.8

Inputs

Name Description Type Default Required
cloudfront_acm_certificate_arn ACM certificate arn for custom_domain string null no
cloudfront_aliases Aliases for custom_domain list(string) [] no
cloudfront_cache_key_headers Header keys that should be used to calculate the cache key in CloudFront. list(string)
[
"Authorization"
]
no
cloudfront_create_distribution Controls whether the main CloudFront distribution should be created. bool true no
cloudfront_external_arn When using an external CloudFront distribution provide its arn. string null no
cloudfront_external_id When using an external CloudFront distribution provide its id. string null no
cloudfront_minimum_protocol_version The 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" no
cloudfront_origin_request_policy Id of a custom request policy that overrides the default policy (AllViewer). Can be custom or managed. string 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_response_headers_policy Id of a response headers policy. Can be custom or managed. Default is empty. string null no
cloudfront_webacl_id An optional webacl2 arn or webacl id to associate with the cloudfront distribution string null 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 (only lowercase alphanumeric characters and hyphens are allowed). string "tf-next" no
enable_multiple_deployments Controls whether it should be possible to run multiple deployments in parallel (requires multiple_deployments_base_domain). bool false no
image_optimization_lambda_memory_size Amount of memory in MB the worker Lambda Function for image optimization can use. Valid value between 128 MB to 10,240 MB, in 1 MB increments. number 2048 no
lambda_attach_policy_json Whether to deploy additional lambda JSON policies. If false, lambda_policy_json will not be attached to the lambda function. (Necessary since policy strings are only known after apply when using Terraforms data.aws_iam_policy_document) bool false no
lambda_attach_to_vpc Set to true if the Lambda functions should be attached to a VPC. Use this setting if VPC resources should be accessed by the Lambda functions. When setting this to true, use vpc_security_group_ids and vpc_subnet_ids to specify the VPC networking. Note that attaching to a VPC would introduce a delay on to cold starts bool false 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
multiple_deployments_base_domain Default wildcard domain where new deployments should be available. Should be in the form of *.example.com. string null no
tags Tag metadata to label AWS resources that support tags. map(string) {} no
tags_s3_bucket Tag metadata to label AWS S3 buckets. Overrides tags with the same name in input variable tags. map(string) {} no
vpc_security_group_ids The list of Security Group IDs to be used by the Lambda functions. lambda_attach_to_vpc should be set to true for these to be applied. list(string) [] no
vpc_subnet_ids The list of VPC subnet IDs to attach the Lambda functions. lambda_attach_to_vpc should be set to true for these to be applied. list(string) [] no

Outputs

Name Description
api_endpoint API endpoint that is used by the CLI.
api_endpoint_access_policy_arn ARN of the policy that grants access to the API endpoint.
cloudfront_custom_error_response Preconfigured custom error response the CloudFront distribution should use.
cloudfront_default_cache_behavior Preconfigured default cache behavior the CloudFront distribution should use.
cloudfront_default_root_object Preconfigured root object the CloudFront distribution should use.
cloudfront_domain_name Domain of the main CloudFront distribution (When created).
cloudfront_hosted_zone_id Zone id of the main CloudFront distribution (When created).
cloudfront_ordered_cache_behaviors Preconfigured ordered cache behaviors the CloudFront distribution should use.
cloudfront_origins Preconfigured origins the CloudFront distribution should use.
upload_bucket_id n/a

Known issues

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.

  • Initial apply fails with error message Error: error creating Lambda Event Source Mapping (#138)

    There is some race condition when the permissions are created for the static deployment Lambda. This should only happen on the first deployment.

    Workaround:

    You should be able to runterraform apply again and the stack creation would proceed without this error.

Contributing

Contributions are welcome!
If you want to improve this module, please take a look at our contributing guidelines to get started.

About

This project is maintained by milliVolt infrastructure.
We build custom infrastructure solutions for any cloud provider.

License

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.