Skip to content

Instantly share code, notes, and snippets.

@techministrator
Last active August 28, 2021 18:37
Show Gist options
  • Save techministrator/b63e117554d710e011ff75426d50cdaf to your computer and use it in GitHub Desktop.
Save techministrator/b63e117554d710e011ff75426d50cdaf to your computer and use it in GitHub Desktop.
Terraform creates Single Page App using Private S3 Bucket and CloudFront with Custom Domain Name
# This simple file presents how to provision a HTTPS Custom Domain Single Page App (like React)
# using S3 Private Bucket (not configured as Website) and CloudFront. Following best practice,
# users can only access S3 content # via Custom Domain Name or CloudFront endpoint. Not directly to S3 bucket.
provider "aws" {
region = "ap-southeast-1"
}
## Data
data "aws_region" "current" {}
## Variables
variable "static_web_domain_name" {
description = "Name of this Static Web. This name will be used for S3 Bucket and CloudFront resources"
type = string
default = "quangluong.example.com"
}
variable "acm_certificate_arn" {
description = "ACM Certificate ARN used for Custom Domain Name"
type = string
default = "arn:aws:acm:<region>:<account_id>:certificate/<certificate-id>"
}
## CloudFront OAI
resource "aws_cloudfront_origin_access_identity" "my_spa" {
comment = var.static_web_domain_name
}
## S3 Private Bucket
resource "aws_s3_bucket" "my_spa" {
bucket = var.static_web_domain_name
acl = "private"
server_side_encryption_configuration {
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
# Do not configure Website as it requires the bucket and all objects to be public.
# Since the bucket is private. CloudFront will access S3 content via S3 API Endpoint instead of S3 Website Endpoint.
# Reference: https://docs.aws.amazon.com/AmazonS3/latest/userguide/WebsiteEndpoints.html#WebsiteRestEndpointDiff
}
## S3 Bucket Policy - Must be created After OAI
resource "aws_s3_bucket_policy" "my_spa" {
bucket = aws_s3_bucket.my_spa.id
policy = data.aws_iam_policy_document.allow_cloudfront_oai_access.json
}
data "aws_iam_policy_document" "allow_cloudfront_oai_access" {
statement {
sid = "AllowCloudFrontOaiAccess"
effect = "Allow"
actions = [
"s3:GetObject",
]
resources = [
"${aws_s3_bucket.my_spa.arn}/*"
]
principals {
type = "AWS"
identifiers = [aws_cloudfront_origin_access_identity.my_spa.iam_arn]
}
}
depends_on = [aws_cloudfront_origin_access_identity.my_spa]
}
## S3 Bucket Public Access Block
resource "aws_s3_bucket_public_access_block" "my_spa" {
bucket = aws_s3_bucket.my_spa.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
## CloudFront Distribution
resource "aws_cloudfront_distribution" "my_spa" {
enabled = true
aliases = [var.static_web_domain_name]
comment = var.static_web_domain_name
default_root_object = "index.html"
http_version = "http2"
price_class = "PriceClass_All"
origin {
# Unless your bucket is in us-east-1. Specify exact bucket region is recommended to prevent
# accessing newly-created CloudFront distribution redirects to S3 URL problem.
domain_name = "${var.static_web_domain_name}.s3.${data.aws_region.current.name}.amazonaws.com"
origin_id = "S3-${var.static_web_domain_name}"
origin_path = ""
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.my_spa.cloudfront_access_identity_path
}
}
# Default Cache Behavior for Main Origin
default_cache_behavior {
target_origin_id = "S3-${var.static_web_domain_name}"
allowed_methods = ["GET", "HEAD", "OPTIONS"]
cached_methods = ["GET", "HEAD"]
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
viewer_certificate {
acm_certificate_arn = var.acm_certificate_arn
minimum_protocol_version = "TLSv1.2_2021"
ssl_support_method = "sni-only"
}
# Make sure all requests return something even if no file exists. Because SPAs like React need the index.html page
# for every requests, then things like "not found" pages are handled in the front-end.
custom_error_response {
error_code = 404
response_code = 200
response_page_path = "/index.html"
error_caching_min_ttl = 0
}
# Same as above but since this is Private Bucket. The error sent back to user is 403 Unauthorized instead of 404 Not Found like Public Bucket.
custom_error_response {
error_code = 403
response_code = 200
response_page_path = "/index.html"
error_caching_min_ttl = 0
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment