{ "version": "https://jsonfeed.org/version/1", "title": "Emmanuel Allen", "home_page_url": "https://www.emmanuelallen.com", "feed_url": "https://www.emmanuelallen.com/rss/feed.json", "description": "Your blog description", "icon": "https://www.emmanuelallen.com/favicon.ico", "author": { "name": "Emmanuel Allen" }, "items": [ { "id": "https://www.emmanuelallen.com/articles/nextjs-static-website-cloudfront-s3", "content_html": "

While there are many ways to use Next.js to run a website I am most interested\nin it’s static site generation capability. I appreciate it’s ability to provide\na modern website experience to users while also removing the need for server\nside code execution.

\n

This guide will show you how to host a statically generated Next.js site on AWS\nCloudfront and S3. Github actions and Terraform will be used to create a CI/CD\npipeline to build and deploy changes to the site.

\n

The versions used for Terraform and the AWS providers are all the latest as of\nJune 16, 2023.

\n

Prerequisites

\n

Before we get started you are going to do some initial setup:

\n\n

End goal for this guide

\n\n

Next.js SSG

\n

First to use Next.js SSG(static site generation) the nextConfig.output needs\nto be set to export and nextConfig.images.unoptimized need to be true. Having\nto turn off image optimization is a limitation of how Next.js Image component\nworks, in the coming week I will write a guide on how to restore image\noptimization functionality.

\n
const nextConfig = {\n  output: 'export',\n  images: { unoptimized: true },\n  ...\n}\n
\n

Terraform

\n

These are all the terra

\n

S3 bucket for Terraform state files

\n

You will need a S3 bucket to hold the Terraform state files. This is the one\npiece of infrastructure that is best to setup manually since it bootstraps\nTerraform. Chose a name for your bucket I have have gone with\nyourdomain-terraform for these examples.

\n

Once created give it the following policy, replace the iam arn and s3 arn to\nuse your account ID and bucket name. You can also change the Principal to\nrestrict access to a specific user or group.

\n
{\n  "Version": "2012-10-17",\n  "Statement": [\n    {\n      "Effect": "Allow",\n      "Principal": {\n        "AWS": "arn:aws:iam::1234567890:root"\n      },\n      "Action": "s3:ListBucket",\n      "Resource": "arn:aws:s3:::yourdomain-terraform"\n    },\n    {\n      "Effect": "Allow",\n      "Principal": {\n        "AWS": "arn:aws:iam::1234567890:root"\n      },\n      "Action": ["s3:GetObject", "s3:PutObject"],\n      "Resource": "arn:aws:s3:::yourdomain-terraform/*"\n    }\n  ]\n}\n
\n

Variables

\n
variable "domain_name" {\n  type        = string\n  description = "The domain name for the website."\n}\n\nvariable "common_tags" {\n  description = "Common tags you want applied to all components."\n}\n
\n

You will need a terraform.tfvars file to set the Terraform variables.

\n
domain_name = "yourdomain.com"\n\ncommon_tags = {\n  Project = "yourdomain"\n}\n
\n

Providers

\n

The region can be changed from us-east-2 to one that is closer to you.

\n
terraform {\n  required_version = "~> 1.5"\n\n  required_providers {\n    aws = {\n      source  = "hashicorp/aws"\n      version = "~> 5.2"\n    }\n  }\n\n  backend "s3" {\n    bucket = "yourdomain-terraform"\n    key    = "prod/terraform.tfstate"\n    region = "us-east-2"\n  }\n}\n\nprovider "aws" {\n  region = "us-east-2"\n}\n
\n

S3 redirect bucket

\n

This buckets job is to redirect all requests from the root domain to the www\nsubdomain.

\n
# S3 bucket for redirecting root domain to www subdomain\nresource "aws_s3_bucket" "root_bucket" {\n  bucket = var.domain_name\n  tags   = var.common_tags\n}\n\nresource "aws_s3_bucket_website_configuration" "root_bucket" {\n  bucket = aws_s3_bucket.root_bucket.id\n  redirect_all_requests_to {\n    host_name = "www.${var.domain_name}"\n    protocol  = "https"\n  }\n}\n
\n

S3 website content buckets

\n

There need to be two buckets because we want to be able to serve pages at both\nhttps://www.yourdomain.com/articles and https://www.yourdomain.com/articles/article_1.

\n

The bucket for all the content not is a subdirectory.

\n
# Policy allowing Cloudfront to get objects from the bucket\ndata "aws_iam_policy_document" "www_bucket" {\n  statement {\n    actions = [\n      "s3:GetObject",\n    ]\n    resources = [\n      "arn:aws:s3:::www.${var.domain_name}/*",\n    ]\n    principals {\n      type = "Service"\n      identifiers = ["cloudfront.amazonaws.com"]\n    }\n    condition {\n      test = "StringEquals"\n      values = [ aws_cloudfront_distribution.www_distribution.arn ]\n      variable = "aws:SourceArn"\n    }\n  }\n}\n\n# S3 bucket for www subdomain\nresource "aws_s3_bucket" "www_bucket" {\n  bucket = "www.${var.domain_name}"\n  tags   = var.common_tags\n}\n\nresource "aws_s3_bucket_policy" "www_bucket" {\n  bucket = aws_s3_bucket.www_bucket.id\n  policy = data.aws_iam_policy_document.www_bucket.json\n}\n\nresource "aws_s3_bucket_ownership_controls" "www_bucket" {\n  bucket = aws_s3_bucket.www_bucket.id\n  rule {\n    object_ownership = "BucketOwnerPreferred"\n  }\n}\n\nresource "aws_s3_bucket_acl" "www_bucket" {\n  depends_on = [aws_s3_bucket_ownership_controls.www_bucket]\n\n  bucket = aws_s3_bucket.www_bucket.id\n  acl    = "private"\n}\n
\n

The bucket for all the content in a subdirectory.

\n
# Policy allowing Cloudfront to get objects from the bucket\ndata "aws_iam_policy_document" "subdirectories_bucket" {\n  statement {\n    actions = [\n      "s3:GetObject",\n    ]\n    resources = [\n      "arn:aws:s3:::www.${var.domain_name}-subdirectories/*",\n    ]\n    principals {\n      type = "Service"\n\n      identifiers = ["cloudfront.amazonaws.com"]\n    }\n    condition {\n      test = "StringEquals"\n      values = [ aws_cloudfront_distribution.www_distribution.arn ]\n      variable = "aws:SourceArn"\n    }\n  }\n}\n\n# S3 bucket for content subdirectories\nresource "aws_s3_bucket" "subdirectories_bucket" {\n  bucket = "www.${var.domain_name}-subdirectories"\n  tags   = var.common_tags\n}\n\nresource "aws_s3_bucket_policy" "subdirectories_bucket" {\n  bucket = aws_s3_bucket.subdirectories_bucket.id\n  policy = data.aws_iam_policy_document.subdirectories_bucket.json\n}\n\nresource "aws_s3_bucket_ownership_controls" "subdirectories_bucket" {\n  bucket = aws_s3_bucket.subdirectories_bucket.id\n  rule {\n    object_ownership = "BucketOwnerPreferred"\n  }\n}\n\nresource "aws_s3_bucket_acl" "subdirectories_bucket" {\n  depends_on = [aws_s3_bucket_ownership_controls.subdirectories_bucket]\n\n  bucket = aws_s3_bucket.subdirectories_bucket.id\n  acl    = "private"\n}\n
\n

Cloudfront redirecting distribution

\n

The arn of you ACM SSL certificate needs to be added manually

\n
# Cloudfront distribution for root domain (redirected to www)\nresource "aws_cloudfront_distribution" "root_distribution" {\n  depends_on = [\n    aws_s3_bucket.root_bucket\n  ]\n\n  origin {\n    domain_name = aws_s3_bucket_website_configuration.root_bucket.website_endpoint\n    origin_id   = "s3-cloudfront"\n\n    custom_origin_config {\n      http_port              = 80\n      https_port             = 443\n      origin_protocol_policy = "http-only"\n      origin_ssl_protocols   = ["TLSv1.2"]\n    }\n  }\n\n  enabled         = true\n  is_ipv6_enabled = true\n\n  aliases = [var.domain_name]\n\n  default_cache_behavior {\n    allowed_methods  = ["GET", "HEAD"]\n    cached_methods   = ["GET", "HEAD"]\n    target_origin_id = "s3-cloudfront"\n\n    # The AWS managed policy CachingOptimizedForUncompressedObjects is used\n    cache_policy_id = "b2884449-e4de-46a7-ac36-70bc7f1ddd6d"\n\n    viewer_protocol_policy = "allow-all"\n  }\n\n  restrictions {\n    geo_restriction {\n      restriction_type = "none"\n    }\n  }\n\n  viewer_certificate {\n    acm_certificate_arn      = "{arn of acm certificate}"\n    ssl_support_method       = "sni-only"\n    minimum_protocol_version = "TLSv1.2_2021"\n  }\n\n  tags = var.common_tags\n}\n
\n

Cloudfront content distribution

\n

The arn of you ACM SSL certificate needs to be added manually

\n
resource "aws_cloudfront_origin_access_control" "s3_access" {\n  name                              = "s3_access_control"\n  origin_access_control_origin_type = "s3"\n  signing_behavior                  = "always"\n  signing_protocol                  = "sigv4"\n}\n\n# Cloudfront distribution for www subdomain (main website)\nresource "aws_cloudfront_distribution" "www_distribution" {\n  depends_on = [\n    aws_s3_bucket.www_bucket,\n    aws_s3_bucket.subdirectories_bucket\n  ]\n\n  origin {\n    domain_name = aws_s3_bucket.www_bucket.bucket_regional_domain_name\n    origin_id   = "s3-cloudfront"\n\n    origin_access_control_id = aws_cloudfront_origin_access_control.s3_access.id\n  }\n\n  origin {\n    domain_name = aws_s3_bucket.subdirectories_bucket.bucket_regional_domain_name\n    origin_id   = "s3-subdirectories"\n\n    origin_access_control_id = aws_cloudfront_origin_access_control.s3_access.id\n  }\n\n  enabled             = true\n  is_ipv6_enabled     = true\n  default_root_object = "index.html"\n\n  aliases = ["www.${var.domain_name}"]\n\n  custom_error_response {\n    error_caching_min_ttl = 0\n    error_code            = 404\n    response_code         = 200\n    response_page_path    = "/404"\n  }\n\n  custom_error_response {\n    error_code            = 403\n    response_code         = 200\n    error_caching_min_ttl = 0\n    response_page_path    = "/404"\n  }\n\n  default_cache_behavior {\n    allowed_methods  = ["GET", "HEAD"]\n    cached_methods   = ["GET", "HEAD"]\n    target_origin_id = "s3-cloudfront"\n\n    # The AWS managed policy CachingOptimized is used\n    cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"\n\n    viewer_protocol_policy = "redirect-to-https"\n    compress               = true\n    # The AWS managed policy SecurityHeadersPolicy is used \n    response_headers_policy_id = "67f7725c-6f97-4210-82d7-5512b31e9d03"\n  }\n\n  ordered_cache_behavior {\n    path_pattern     = "*/*"\n    allowed_methods  = ["GET", "HEAD"]\n    cached_methods   = ["GET", "HEAD"]\n    target_origin_id = "s3-subdirectories"\n\n    # The AWS managed policy CachingOptimized is used\n    cache_policy_id = "658327ea-f89d-4fab-a63d-7e88639e58f6"\n\n    viewer_protocol_policy = "redirect-to-https"\n    compress               = true\n    # The AWS managed policy SecurityHeadersPolicy is used \n    response_headers_policy_id = "67f7725c-6f97-4210-82d7-5512b31e9d03"\n  }\n\n  restrictions {\n    geo_restriction {\n      restriction_type = "none"\n    }\n  }\n\n  viewer_certificate {\n    acm_certificate_arn      = "{arn of acm certificate}"\n    ssl_support_method       = "sni-only"\n    minimum_protocol_version = "TLSv1.2_2021"\n  }\n\n  tags = var.common_tags\n}\n
\n

Route53 DNS routing

\n

Routes to for both IPv4 and IPv6 for both the redirecting distribution and\nthe content distribution.

\n
data "aws_route53_zone" "domain_name" {\n  name         = var.hosted_zone\n  private_zone = false\n}\n\nresource "aws_route53_record" "root_a" {\n  depends_on = [\n    aws_cloudfront_distribution.root_distribution\n  ]\n\n  zone_id = data.aws_route53_zone.domain_name.zone_id\n  name    = var.domain_name\n  type    = "A"\n\n  alias {\n    name    = aws_cloudfront_distribution.root_distribution.domain_name\n    zone_id = aws_cloudfront_distribution.root_distribution.hosted_zone_id\n\n    evaluate_target_health = false\n  }\n}\n\nresource "aws_route53_record" "www_a" {\n  depends_on = [\n    aws_cloudfront_distribution.www_distribution\n  ]\n\n  zone_id = data.aws_route53_zone.domain_name.zone_id\n  name    = "www.${var.domain_name}"\n  type    = "A"\n\n  alias {\n    name    = aws_cloudfront_distribution.www_distribution.domain_name\n    zone_id = aws_cloudfront_distribution.www_distribution.hosted_zone_id\n\n    evaluate_target_health = false\n  }\n}\n\nresource "aws_route53_record" "root_aaaa" {\n  depends_on = [\n    aws_cloudfront_distribution.root_distribution\n  ]\n\n  zone_id = data.aws_route53_zone.domain_name.zone_id\n  name    = var.domain_name\n  type    = "AAAA"\n\n  alias {\n    name    = aws_cloudfront_distribution.root_distribution.domain_name\n    zone_id = aws_cloudfront_distribution.root_distribution.hosted_zone_id\n\n    evaluate_target_health = false\n  }\n}\n\nresource "aws_route53_record" "www_aaaa" {\n  depends_on = [\n    aws_cloudfront_distribution.www_distribution\n  ]\n\n  zone_id = data.aws_route53_zone.domain_name.zone_id\n  name    = "www.${var.domain_name}"\n  type    = "AAAA"\n\n  alias {\n    name    = aws_cloudfront_distribution.www_distribution.domain_name\n    zone_id = aws_cloudfront_distribution.www_distribution.hosted_zone_id\n\n    evaluate_target_health = false\n  }\n}\n
", "url": "https://www.emmanuelallen.com/articles/nextjs-static-website-cloudfront-s3", "title": "How to host a Next js static website on AWS Cloudfront and S3", "summary": "While there are many ways to use Next.js to run a website I am most interested in it’s static site generation capability. I appreciate it’s ability to provide a modern website experience to users while also removing the need for server side code execution.", "date_modified": "2023-06-15T00:00:00.000Z", "author": { "name": "Emmanuel Allen" } } ] }