Deploy Serverless Next.js to AWS with Terraform 1.1

Terraform for better or worse is frequently updated with new versions. Many of these introduce incompatibilities with previous versions that require manual rework of Terraform definitions as well as require updating the local or remote Terraform state. I originally developed a solution for deploying Next.js to AWS using Terraform version 0.12 and that has been working well for over a year now. However, recently AWS announced that changes to their API would require an update of the AWS Terraform Provider. While there is an option to patch an older version of the Terraform provider, I figured that may be as good an excuse as any to update the Goldstack Next.js + Bootstrap Template to the latest version of Terraform, which is 1.1 as of this writing.

Next.js is optimised for deployment to the Vercel platform – which provides a fast and easy way to deploy Next.js projects. Apart from that, Next.js can also be deployed using Docker or a virtual machine that can run Node.js. My aim is to deploy Next.js to AWS using Serverless solutions that are low-cost and easy to scale. Thus, I won’t be setting up any EC2 instances or EKS/ECS containers. Instead, I will use the following components:

Next.js on AWS Architecture

This post provides an overview of the Terraform resources required to configure the infrastructure for hosting a Next.js application on AWS.

tldr;

If you simply want to get started developing your own Next.js project to be deployed on Terraform:

Terraform Resources

In order to define the above infrastructure in Terraform, we need the following Terraform resources:

For defining our certificate and setting up the domain name:

  • aws_acm_certificate: To define the certificate we use to allow secure connections via https:// to our website. AWS provides these certificates for free and will renew them automatically as well.
  • aws_route53_record: To define the DNS records for our website. We also use this resource to help with validating the TLS certificate.
  • aws_acm_certificate_validation: To assist with the validation of the TLS certificate

See the configuration for each resource in main.tf.

For configuring our CDN:

See the configuration for each resource in root.tf. We are also configuring a second CloudFront distribution to be used for the forwarding domain (e.g. www.mydomain.com to mydomain.com). This distribution is defined in redirect.tf.

For supporting dynamic Next.js routing:

  • aws_lambda_function: To define a lambda function that will help with Next.js dynamic routes. Find the source code for this function in lambda.ts. This is an edge lambda that will be used by CloudFront.

Find the definition of this resource and other resources required for running the lambda in edge.tf.

Changes from Terraform 0.12 to 1.1

Terraform introduced a number of changes, especially in the version 0.13 and 0.14. For the resources required for this project they were:

  • The way provides are defined has changed in 0.13. They are now defined as:
terraform {
  required_providers {
    archive = {
      source = "hashicorp/archive"
      version = "2.2.0"
    }
    aws = {
      source = "hashicorp/aws"
      version = "3.72.0"
    }
  }
  required_version = ">= 0.13"
}
  • Version 0.14 also introduced a lockfile to keep track of versions of modules used and their transitive dependencies. See .terraform.lock.hcl for the lockfile generated for this project.
  • There were a number of changes to aws_acm_certificate resource in newer versions of the Terraform AWS Provider. Previously the Route 53 resource could be defined as follows:
resource "aws_route53_record" "wildcard_validation" {
  name    = aws_acm_certificate.wildcard_website.domain_validation_options[0].resource_record_name
  type    = aws_acm_certificate.wildcard_website.domain_validation_options[0].resource_record_type
  zone_id = data.aws_route53_zone.main.zone_id
  records = [aws_acm_certificate.wildcard_website.domain_validation_options[0].resource_record_value]
  ttl     = "60"
}

But with the most recent AWS Terraform provider, it needs to be defined as (see Terraform and AWS Wildcard Certificates Validation) :

resource "aws_route53_record" "wildcard_validation" {
  provider = aws.us-east-1
  for_each = {
    for dvo in aws_acm_certificate.wildcard_website.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      record = dvo.resource_record_value
      type   = dvo.resource_record_type
    }
   # Skips the domain if it doesn't contain a wildcard
    if length(regexall("\\*\\..+", dvo.domain_name)) > 0
  }

  allow_overwrite = true
  name            = each.value.name
  type            = each.value.type
  zone_id         = data.aws_route53_zone.main.zone_id
  records         = [each.value.record]
  ttl             = 60
}

Getting Next.js Ready for Deployment to AWS

In order to deploy Next.js to AWS, we need to use the Static HTML Export feature of Next.js. We run next export (see package.json) and upload the directory generated by the export to S3 using the Goldstack Next.js template utilities – this will also take care of packing up the dynamic routing config and deploying the edge lambda used by CloudFront routing.

Relying on the static HTML export of Next.js comes with several trade-offs:

There are many different ways to deploy Next.js to AWS, for instance using AWS Amplify or using EC2. The solution pursued in the example project aims to provide a lightweight, yet flexible solution that makes full use of the capabilities of Terraform while creating low-cost, scalable infrastructure. If you have any questions or ideas how to improve this approach, please be welcome to head over to the Goldstack GitHub project and raise an issue.


Cover image by kreatikar

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s