/**
 * Common Data Sources
 ***/

data "aws_region" "current" {}

/**
 * Server Function
 ***/

resource "aws_lambda_function" "server" {
  description      = "Server function for the website"
  filename         = var.server_function_config.filename
  function_name    = var.server_function_config.function_name
  handler          = var.server_function_config.handler
  memory_size      = var.server_function_config.memory_size
  role             = aws_iam_role.server.arn
  runtime          = var.server_function_config.runtime
  source_code_hash = filebase64sha256(var.server_function_config.filename)
  timeout          = var.server_function_config.timeout

  dynamic "tracing_config" {
    for_each = var.server_function_config.tracing_enabled ? [1] : []

    content {
      mode = "Active"
    }
  }

  dynamic "vpc_config" {
    for_each = var.vpc_config.enabled ? [1] : []

    content {
      subnet_ids         = var.vpc_config.subnet_ids
      security_group_ids = var.vpc_config.security_group_ids
    }
  }

  environment {
    variables = {
      CACHE_BUCKET_NAME         = aws_s3_bucket.cache_bucket.id
      CACHE_BUCKET_REGION       = data.aws_region.current.name
      CACHE_DYNAMO_TABLE        = "INSERT_CACHE_DYNAMO_TABLE_HERE"
      REVALIDATION_QUEUE_URL    = aws_sqs_queue.revalidation_queue.id
      REVALIDATION_QUEUE_REGION = data.aws_region.current.name
    }
  }

  tags = merge(var.shared_tags, {
    Name = var.server_function_config.function_name
  })
}

resource "aws_iam_role" "server_lambda" {
  name = var.server_function_config.function_name
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "server_lambda" {
  name = var.server_function_config.function_name
  policy = jsondecode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ]
        Effect   = "Allow"
        Resource = "arn:aws:logs:*:*:*"
      },
      {
        Action = [
          "s3:GetObject",
          "s3:PutObject"
        ]
        Effect   = "Allow"
        Resource = "${aws_s3_bucket.cache_bucket.arn}/*"
      },
      {
        Action = [
          "sqs:SendMessage"
        ]
        Effect   = "Allow"
        Resource = aws_sqs_queue.revalidation_queue.arn
      },
      {
        Action = [
          "dynamodb:PutItem",
          "dynamodb:GetItem"
        ]
        Effect   = "Allow"
        Resource = "INSERT CACHE DYNAMO TABLE ARNS"
      }
    ]
  })
  role = aws_iam_role.server_lambda.id
}

/**
 * Image Optimization Function
 ***/

resource "aws_lambda_function" "image_optimization" {
  architectures    = ["arm64"]
  description      = "Image Optimization function for the website"
  filename         = var.image_optimization_function_config.filename
  function_name    = var.image_optimization_function_config.function_name
  handler          = var.image_optimization_function_config.handler
  memory_size      = var.image_optimization_function_config.memory_size
  role             = aws_iam_role.image_optimization.arn
  runtime          = var.image_optimization_function_config.runtime
  source_code_hash = filebase64sha256(var.image_optimization_function_config.filename)
  timeout          = var.image_optimization_function_config.timeout

  dynamic "tracing_config" {
    for_each = var.image_optimization_function_config.tracing_enabled ? [1] : []

    content {
      mode = "Active"
    }
  }

  dynamic "vpc_config" {
    for_each = var.vpc_config.enabled ? [1] : []

    content {
      subnet_ids         = var.vpc_config.subnet_ids
      security_group_ids = var.vpc_config.security_group_ids
    }
  }

  environment {
    variables = {
      BUCKET_NAME = aws_s3_bucket.asset_files_bucket.id
    }
  }

  tags = merge(var.shared_tags, {
    Name = var.image_optimization_function_config.function_name
  })
}

resource "aws_iam_role" "image_optimization" {
  name = var.image_optimization_function_config.function_name
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "image_optimization" {
  name = var.image_optimization_function_config.function_name
  policy = jsondecode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "s3:GetObject"
        ]
        Effect   = "Allow"
        Resource = "${aws_s3_bucket.asset_files_bucket.arn}/*"
      }
    ]
  })
  role = aws_iam_role.image_optimization.id
}

/**
 * Asset Files Bucket
 ***/

resource "aws_s3_bucket" "asset_bucket" {
  bucket = var.asset_bucket_config.bucket_name

  tags = merge(var.shared_tags, {
    Name = var.asset_bucket_config.bucket_name
  })
}

/**
 * Revalidation Function
 ***/

resource "aws_lambda_function" "revalidation_function" {
  description      = "Revalidation function for the website"
  filename         = var.revalidation_function_config.filename
  function_name    = var.revalidation_function_config.function_name
  handler          = var.revalidation_function_config.handler
  memory_size      = var.revalidation_function_config.memory_size
  role             = aws_iam_role.revalidation_function.arn
  runtime          = var.revalidation_function_config.runtime
  source_code_hash = filebase64sha256(var.revalidation_function_config.filename)
  timeout          = var.revalidation_function_config.timeout

  dynamic "tracing_config" {
    for_each = var.revalidation_function_config.tracing_enabled ? [1] : []

    content {
      mode = "Active"
    }
  }

  dynamic "vpc_config" {
    for_each = var.vpc_config.enabled ? [1] : []

    content {
      subnet_ids         = var.vpc_config.subnet_ids
      security_group_ids = var.vpc_config.security_group_ids
    }
  }

  tags = merge(var.shared_tags, {
    Name = var.revalidation_function_config.function_name
  })
}

resource "aws_iam_role" "revalidation_function" {
  name = var.revalidation_function_config.function_name
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "revalidation_function" {
  name = var.revalidation_function_config.function_name
  role = aws_iam_role.revalidation_function.id
  policy = jsondecode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "sqs:ReceiveMessage",
          "sqs:DeleteMessage"
        ],
        Effect   = "Allow",
        Resource = aws_sqs_queue.revalidation_queue.arn
      }
    ]
  })
}

resource "aws_lambda_event_source_mapping" "revalidation_queue" {
  event_source_arn = aws_sqs_queue.revalidation_queue.arn
  function_name    = aws_lambda_function.revalidation_function.function_name
}

/**
 * Revalidation Queue
 ***/

resource "aws_sqs_queue" "revalidation_queue" {
  name       = "revalidation-queue.fifo"
  fifo_queue = true

  tags = merge(var.shared_tags, {
    Name = "revalidation-queue.fifo"
  })
}

/**
 * Revalidation Tag-to-path Mapping DynamoDB Table
 ***/

/**
 * Cache Files Bucket
 ***/

resource "aws_s3_bucket" "cache_bucket" {
  bucket = var.cache_bucket_config.bucket_name

  tags = merge(var.shared_tags, {
    Name = var.cache_bucket_config.bucket_name
  })
}

/**
 * Warmer Function
 ***/

resource "aws_lambda_function" "warmer" {
  description      = "Warmer function for the website"
  filename         = var.warmer_function_config.filename
  function_name    = var.warmer_function_config.function_name
  handler          = var.warmer_function_config.handler
  memory_size      = var.warmer_function_config.memory_size
  role             = aws_iam_role.warmer.arn
  runtime          = var.warmer_function_config.runtime
  source_code_hash = filebase64sha256(var.warmer_function_config.filename)
  timeout          = var.warmer_function_config.timeout

  dynamic "tracing_config" {
    for_each = var.warmer_function_config.tracing_enabled ? [1] : []

    content {
      mode = "Active"
    }
  }

  dynamic "vpc_config" {
    for_each = var.vpc_config.enabled ? [1] : []

    content {
      subnet_ids         = var.vpc_config.subnet_ids
      security_group_ids = var.vpc_config.security_group_ids
    }
  }

  environment {
    variables = {
      FUNCTION_NAME = var.server_function_config.function_name
      CONCURRENCY   = var.warmer_function_config.concurrency
    }
  }

  tags = merge(var.shared_tags, {
    Name = var.warmer_function_config.function_name
  })
}

resource "aws_iam_role" "warmer_lambda" {
  name = var.warmer_function_config.function_name
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "warmer_lambda" {
  name = var.warmer_function_config.function_name
  policy = jsondecode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "lambda:InvokeFunction"
        ]
        Effect   = "Allow"
        Resource = aws_lambda_function.server.arn
      }
    ]
  })
  role = aws_iam_role.warmer_lambda.id
}

/**
 * Warmer Eventbridge Cron
 ***/

About

This is an opinated way to launch a Next.js based website on AWS.

This architecture is based on utilizing OpenNext for generation of the site bundles. Refer to OpenNext for more information on what Next.js features it supports.

Installation

1

Copy and paste all code files above into a folder.

Copy the main.tf, variables.tf, outputs.tf, versions.tf, and README.md into a folder (ex. website).

2

Reference the new module as a local or remote module.

module "website" {
  source = "path-to-copied-website-module"

  server_function_config = {
    filename = "server.zip"
  }

  image_optimization_function_config = {
    filename = "image-optimization.zip"
  }

  revlidation_function_config = {
    filename = "revlidation.zip"
  }

  warmer_function_config = {
    filename = "warmer.zip"
  }
}
3

Install dependencies, prep zip bundles dependencies, and deploy!

Install OpenNext and Terraform or OpenTofu dependencies. Then make the correct zip bundles via OpenNext and deploy.

Usage

The options available to can be seen in the variables.tf file as well as in the Next.js module README.