- Published on
Hosting a Next.js blog in AWS
- Authors
- Name
- Walt Niederer
Table of Contents
Building infrastructure is something I have a great deal of experience doing, especially using Terraform (blog post about Terraform coming soon after this one...), so naturally when I decided to finally do this project the only two givens I had about the stack is that it I would use AWS to host it and Terraform to manage the infrastructure. What I didn't anticipate how challenging it would be to make Next.js work in AWS.
Why AWS
I work as a Platform Engineer and have obtained AWS associate certifications for both Solution Architect and SysOps Admin, which is a roundabout way of expressing I enjoy working with and learning about AWS.
AWS is a fantastic service that provides a great deal of flexibility and options for building a solution to whatever problem you may have whether that be hosting a website, building and deploying ML models, or creating a low latency gaming streaming service. In my case I wanted to build a serverless website and ultimately landed on using these services
- Lambda, a Serverless solution for handling any dynamic components and invoking APIs (like the NowPlaying component at the bottom of this page).
- An API Gateway for routing requests to my Lambda.
- Note: In my first blog post I stated I was planning on using an Application Load Balancer, but after more research it was clear that API GW is the more cost effective choice for a low traffic site.
- S3 for storing static assets
- CloudFront as my CDN for handling all incoming traffic and serving static assets.
That being said, unless you are passionate or have an interest in building infrastructure I would not recommend hosting a serverless Next.js site in AWS. While it can be made a trivial task with several "no config" tools to quickly spin up a Next.js built website, there are many caveats when it comes to feature support and performance.
The Challenges
Next.js has made it clear that they want developers to use Vercel as the hosting platform for Next.js apps. Hosting anywhere else requires a deep understanding how Next.js works under the hood and knowledge of which services are needed in the hosting platform to have those critical components work well.
I was first clued into how difficult it'd be after my first attempt to push a test version of my blog to a Lambda when I realized I didn't code a handler; this led me down a rabbit hole that got deeper as I did more research. It even led to me publishing my first npm package and going back and forth debugging both the code for the website and that package. I spent many hours trying to get it to work, but eventually I came to the realization that architecting a solution to the Next.js serverless hosting problem was a project on its own sooo I decided to use terraform module to make it so I could have this blog up in days instead of months. One day, when I have more experience with Next.js I will build my own solution...
How I Learned to Stop Worrying and Love the Terraform Next.js module for AWS
There were several solutions to deploying Next.js I researched and evaluated, but ultimately I chose the Terraform Next.js module for AWS as the tool I wanted to use for deploying walt.dev to AWS. It provided the core services I know a website needs to perform well and other interesting features such as an SQS queue for sending invalidations to the CloudFront. In addition it is mostly feature complete with Next.js and even works with Next.js 12, though not all features new to that version are supported yet. Using the Terraform Next.js Module helped me get the website across the finish line and also taught me more about how Next works at a lower level.
The module definitely simplifies what would normally be a complex set of IaC. For this config, the file that represents all of my infrastructure as code is main.tf
, see below for the code as of version 2.1.1
of my site.
terraform {
backend "s3" {
bucket = "walters-playground-terraform"
key = "PersonalWebsite/prod/terraform.tfstate"
region = "us-west-2"
encrypt = true
}
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
variable "aws_region" {
type = string
default = "us-west-2"
}
variable "env" {
type = string
default = "prod"
}
variable "tag" {
type = string
}
variable "app_name" {
type = string
default = "personal-website"
}
variable "tags" {
type = map(string)
default = {
AppName = "personal-website"
Version = "2.1.1"
}
}
variable "cloudfront_alias" {
type = list(string)
default = ["walt.dev"]
}
provider "aws" {
region = "us-west-2"
}
module "tf_next" {
source = "milliHQ/next-js/aws"
next_tf_dir = var.next_tf_dir
deployment_name = var.app_name
lambda_runtime = "nodejs14.x"
tags = var.tags
cloudfront_minimum_protocol_version = "TLSv1.2_2021"
cloudfront_aliases = var.cloudfront_alias
cloudfront_acm_certificate_arn = module.cloudfront_cert.acm_certificate_arn
providers = {
aws.global_region = aws.global_region
}
}
##### tf_next Vars #####
provider "aws" {
alias = "global_region"
region = "us-east-1"
}
variable "next_tf_dir" {
type = string
default = "../.next-tf/"
}
###########
# Variables
###########
variable "custom_domain" {
description = "Your custom domain"
type = string
default = "walt.dev"
}
variable "custom_domain_zone_name" {
description = "The Route53 zone name of the custom domain"
type = string
default = "walt.dev."
}
###########
# Locals
###########
locals {
aliases = [var.custom_domain]
# If you need a wildcard domain(ex: *.example.com), you can add it like this:
# aliases = [var.custom_domain, "*.${var.custom_domain}"]
}
#######################
# Route53 Domain record
#######################
data "aws_route53_zone" "custom_domain_zone" {
name = var.custom_domain_zone_name
}
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
##########
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))
tags = {
Name = "CloudFront ${var.custom_domain}"
}
providers = {
aws = aws.global_region
}
}
Remaining Issues
Unfortunately, the Terraform Next.js module for AWS doesn't appear to be working perfectly. There's an outstanding issue with one of the SSR Lambdas that I've been working through, essentially, the Lambda cannot find the context for various files that I've confirmed is bundled with the artifact that the Terraform Next.js module builds. From the users' perspective, they will see 500s and 503s when they hover over links on my site. I've been digging into that issue for the past few days and have been preparing some steps to reproduce the issue so I can present it in a GitHub Issue to the developers of the tool if I'm unable to solve it myself.