Serve Static Site from Private S3 Bucket
Table of Contents
- 1. Introduction
- 2. The Root Module
- 3. The S3 Module
- 4. The CloudFront Module
- 5. The Route 53 Module
- 6. Conclusion
- 7. Let Go Dynamic
- 8. The Source Code
- 9. The Lambda Func Module
- 10. The API Gateway Module
- 11. The CloudFront Module: Updated
- 12. The Root Module: Updated
- 13. Conclusion: Pt. 2
- 14. References
2025-12-11, Thu
1. Introduction
There are a few options to serve static site from private S3 bucket, one of which is to use CloudFront.
In order to do it manually, we need to
- Create a S3 bucket that blocks all public access, upload site files to it
- Create a CloudFront distribution, along with a new OAC Setting (Origin Access Control). Copy the generated S3 bucket policy
- Go to S3 bucket Permissions tab and apply the copied CloudFront OAC policy
And it's done — Unless you want to access the site with a custom subdomain (with the main domain registered with Route 53 already), then we also need to:
- Request a ACM Certificate for our subdomain name in the
us-east-1region - Add the ACM Certificate validation record into Route 53 and wait for Certificate status becomes "Issued"
- In CloudFront distribution setting, add alternate domain name that points to custom domain, along with SSL certificate that just got issued by ACM
- In Route 53 hosted zone, add another alias A record that routes traffic to the CloudFront distribution
After everything is done properly, we should be able to visit custom-domain-name to access our file.
We could use tool like terraform to automate steps above. These
steps could be splitted into one root module and three submodules,
with a directory structure like this:
- ./
- ./modules/aws-s3-static-website
- ./modules/aws-cloudfront-website
- ./modules/aws-route53-website
They will be explained in detail one by one:
2. The Root Module
The input of root module requires the existing main domain
(i.e. hosted_zone) and custom subdomain name, as shown below
# ref: ./variables.tf
variable "existing_zone_name" {
description = "exsting zone name"
type = string
default = "tangwenfei.org"
}
variable "custom_domain_name" {
description = "custom CNAME record name"
type = string
default = "mec"
}
As for the root entry, it handles ACM Certificate creation and invokes other submodules, e.g.
# ref: ./main.tf
# the current working region
provider "aws" {
region = "us-west-1"
profile = "mec-profile"
}
# the special region for ACM Certificate
provider "aws" {
region = "us-east-1"
profile = "mec-profile"
alias = "us_east_1"
}
locals {
full_domain_name = "${var.custom_domain_name}.${var.existing_zone_name}"
}
# Step 1/3: Request the ACM certificate from us-east-1 region
resource "aws_acm_certificate" "custom_domain_cert" {
provider = aws.us_east_1
domain_name = local.full_domain_name
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
# Step 2/3: Add ACM Cert Validation record in Route 53, done in ./modules/aws-route53-website
# Step 3/3: Wait for the certificat to be validated,
# i.e. Certificate status changed from "Pending Validation" to "Issued"
resource "aws_acm_certificate_validation" "cert_validation" {
provider = aws.us_east_1
certificate_arn = aws_acm_certificate.custom_domain_cert.arn
# theres's typically only one record for each manually created ACM Certificate
validation_record_fqdns = [for fqdn in module.website_route53.cert_record_fqdns : fqdn]
}
module "website_s3_bucket" {
source = "./modules/aws-s3-static-website"
bucket_name = "mec-bucket-2025"
cloudfront_arn = module.website_cloudfront.cloudfront_arn
tags = {
Terraform = "true"
Environment = "dev"
}
}
module "website_cloudfront" {
source = "./modules/aws-cloudfront-website"
oac_name = "cloudfront-oac-for-s3-mec-bucket-2025"
oac_description = "OAC for CloudFront to access S3"
root_object = "index.html"
domain_name = module.website_s3_bucket.regional_domain_name
bucket_id = module.website_s3_bucket.id
custom_full_domain = local.full_domain_name
# This ARN is supposed to be the same with aws_acm_certificate.custom_domain_cert.arn
acm_cert_arn = aws_acm_certificate_validation.cert_validation.certificate_arn
}
module "website_route53" {
source = "./modules/aws-route53-website"
zone_name = var.existing_zone_name
custom_domain_name = var.custom_domain_name
cloudfront_domain_name = module.website_cloudfront.domain_name
cloudfront_hosted_zone_id = module.website_cloudfront.hosted_zone_id
acm_cert_domain_validation_options = aws_acm_certificate.custom_domain_cert.domain_validation_options
}
As for the output of the root module, it contains the s3 bucket name, the CloudFront domain, and our custom subdomain name.
# ref: ./outputs.tf
output "cloudfront_domain_name" {
description = "Domain name of CloudFront"
value = module.website_cloudfront.domain_name
}
output "website_domain_name" {
description = "custom domain name of the static site"
value = "https://${var.custom_domain_name}.${var.existing_zone_name}"
}
Now move on to the submodules, starting with S3.
3. The S3 Module
The S3 module requires bucket name and CloudFront ARN as input, e.g.
# ref: ./modules/aws-s3-static-website/variables.tf
variable "bucket_name" {
description = "Name of the s3 bucket. Must be unique"
type = string
}
# Note: it's the CloudFront ARN that is needed here, not CloudFront OAC ARN
variable "cloudfront_arn" {
description = "the CloudFront ARN"
type = string
}
variable "tags" {
description = "Tags to set on the bucket."
type = map(string)
default = {}
}
The main entry of the S3 module creates a S3 bucket along with proper bucket policy for CloudFront access.
# ref: ./modules/aws-s3-static-website/main.tf
resource "aws_s3_bucket" "s3_bucket" {
bucket = var.bucket_name
force_destroy = true # This line allows destruction of a non-empty bucket
tags = var.tags
}
resource "aws_s3_bucket_website_configuration" "s3_bucket" {
bucket = aws_s3_bucket.s3_bucket.id
index_document {
suffix = "index.html"
}
error_document {
key = "error.html"
}
}
resource "aws_s3_bucket_policy" "s3_bucket" {
bucket = aws_s3_bucket.s3_bucket.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow",
Principal = {
Service = "cloudfront.amazonaws.com"
},
Action = "s3:GetObject",
Resource = "${aws_s3_bucket.s3_bucket.arn}/*",
Condition = {
StringEquals = {
"AwS:SourceArn" = var.cloudfront_arn
}
}
},
]
})
}
The output of this module includes id, name, and
regional_domain_name of the newly created S3 bucket.
# ref: ./modules/aws-s3-static-website/outputs.tf
output "name" {
description = "Name (id) of the bucket"
value = aws_s3_bucket.s3_bucket.id
}
output "regional_domain_name" {
description = "S3 bucket region"
value = aws_s3_bucket.s3_bucket.bucket_regional_domain_name
}
output "id" {
description = "S3 bucket id"
value = aws_s3_bucket.s3_bucket.id
}
4. The CloudFront Module
The CloundFront module has following four required input parameters:
- the S3 bucket regional domain name for content servicing
- our custom domain name that serves as alternate domain name
- the ACM Certificaet ARN for the alternate domain name
along with some other optional parameters:
# ref: ./modules/aws-cloudfront-website/variables.tf
variable "oac_name" {
description = "Name of the CloudFront OAC(Origin Access Control) configuration"
type = string
}
variable "oac_description" {
description = "Description of the CloudFront OAC configuration"
type = string
}
variable "root_object" {
description = "root object for CloudFront distribution"
type = string
default = "index.html"
}
variable "domain_name" {
description = "S3 bucket regional domain name"
type = string
}
variable "bucket_id" {
description = "S3 Bucket Id"
type = string
}
variable "custom_full_domain" {
description = "custom FULL domain name in Route 53, i.e. your own domain to access the site"
type = string
}
variable "acm_cert_arn" {
description = "ACM Certificate ARN"
type = string
}
The main entry of this module creates an OAC rule and CloudFront distribution that accesses S3 bucket, with alias that points to custom domain and proper viewer certificates.
# ref: ./modules/aws-cloudfront-website/main.tf
# Origin Access Control (OAC)
resource "aws_cloudfront_origin_access_control" "s3_oac" {
name = var.oac_name
description = var.oac_description
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
resource "aws_cloudfront_distribution" "s3_distribution" {
origin {
domain_name = var.domain_name
origin_id = "s3-origin-${var.bucket_id}"
origin_access_control_id = aws_cloudfront_origin_access_control.s3_oac.id
}
enabled = true
is_ipv6_enabled = true
comment = "CloudFront distribution for s3 static website"
default_root_object = var.root_object
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = "s3-origin-${var.bucket_id}"
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
compress = true
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
aliases = [
var.custom_full_domain
]
viewer_certificate {
acm_certificate_arn = var.acm_cert_arn
ssl_support_method = "sni-only"
}
tags = {
Environment = "mec-custom-env"
ManagedBy = "Terraform"
}
}
The CloudFront module outputs the following attributes of the distrbution resource:
arndomain_namehosted_zone_id
# ref: ./modules/aws-cloudfront-website/outputs.tf
output "cloudfront_arn" {
description = "the CloudFront ARN for S3 bucket"
value = aws_cloudfront_distribution.s3_distribution.arn
}
output "domain_name" {
description = "The domain name of published CloudFront site"
value = aws_cloudfront_distribution.s3_distribution.domain_name
}
output "hosted_zone_id" {
description = "Hosted Zone Id of CloudFront site"
value = aws_cloudfront_distribution.s3_distribution.hosted_zone_id
}
5. The Route 53 Module
The Route 53 module requires both the custom domain and CloudFront distribution domain, along with the ACM Certificate validation records.
# ref: .//modules/aws-route53-website/variables.tf
variable "zone_name" {
description = "exsting zone name"
type = string
}
variable "custom_domain_name" {
description = "custom CNAME record name"
type = string
}
variable "cloudfront_domain_name" {
description = "CloudFront domain name"
type = string
}
variable "cloudfront_hosted_zone_id" {
description = "CloudFront hosted zone"
type = string
}
variable "acm_cert_domain_validation_options" {
description = "List of domain validation options from the ACM Certificate"
# type = any
type = list(object({
domain_name = string
resource_record_name = string
resource_record_type = string
resource_record_value = string
}))
}
The main entry of the Route 53 module adds two types of records:
- an A type alias record for our custom domain that routes traffic to CloudFront distribution, and
- a validation record for validating the ACM issued certificate
# ref: .//modules/aws-route53-website/main.tf
locals {
full_domain_name = "${var.custom_domain_name}.${var.zone_name}"
}
# retrieve info of existing zone, e.g. tangwenfei.org
data "aws_route53_zone" "existing_hosted_zone" {
name = var.zone_name
}
resource "aws_route53_record" "custom_site_domain" {
zone_id = data.aws_route53_zone.existing_hosted_zone.zone_id
name = local.full_domain_name
# name = var.custom_domain_name
type = "A"
alias {
name = var.cloudfront_domain_name
zone_id = var.cloudfront_hosted_zone_id
evaluate_target_health = false
}
}
# ============================================================
# Add DNS record for the ACM Ceritificate
# Step 1/3: Request ACM Certififcate from us-east-1 region, which is done in the root module
# Step 2/3: Add ACM Certificate Validation record into Route 53
# Step 3/3: Wait for the ACM Certificate to become Issued. Done in the root module
resource "aws_route53_record" "cert_validation_record" {
for_each = {
for dvo in var.acm_cert_domain_validation_options : dvo.domain_name => dvo
}
name = each.value.resource_record_name
records = [each.value.resource_record_value]
type = each.value.resource_record_type
zone_id = data.aws_route53_zone.existing_hosted_zone.zone_id
ttl = 60
}
This module outputs the fully qualified domain name for validation of ACM issued certificate, i.e. checking that its status has been chagned from "Pending Validation" to "Issued".
# ref: .//modules/aws-route53-website/output.tf
output "cert_record_fqdns" {
description = "The Route53 validation records added for ACM Certificate"
value = values(aws_route53_record.cert_validation_record)[*].fqdn
}
6. Conclusion
And that's it. To run the plan and check the result, run the following sample script:
#!/bin/sh # current work directory . terraform init terraform plan --out out/plan.out terraform apply --auto-aprove out/plan.out aws --profile mec-profile s3 cp /path/to/index.html s3://mec-bucket-2025 curl https://mec.tangwenfei.org # Release resource # terraform destroy
"Static" content ends here. Below content talks about dynamic content.
7. Let Go Dynamic
For dynamic content served through RESTful API, it could be achieved through a lot of different ways. Lambda + API Gateweay is the combination that will be used here for demonstration.
Four parts will be talked about here:
- The source code for backend and frontend
- The new Lambda Func module
- The new API Gateway module
- The updated CloudFront module and Root module
8. The Source Code
For backend, it's the source code of Lambda function.
// ref: ./backend/hello.js
// ref: https://developer.hashicorp.com/terraform/tutorials/aws/lambda-api-gateway
// Lambda function code
module.exports.handler = async (event) => {
console.log('Event: ', event);
let responseMessage = 'Hello, World!';
if (event.queryStringParameters && event.queryStringParameters['Name']) {
responseMessage = `Hello, ${event.queryStringParameters['Name']}!`;
}
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message: responseMessage,
}),
}
}
For frontend, it's the updated index.html that calls backend API.
<!-- ref: ./www/index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Static Website</title>
<script type="text/javascript">
const _noop = () => {};
async function getGreetings(name = '', onResponse = showGreetings) {
let url = '/api/hello';
try {
if (name) {
url = `${url}?Name=${name}`;
}
const resp = await fetch(url);
const data = await resp.json(); // parse the response body to JSON
onResponse(data);
} catch(e) {
console.error(e);
onResponse(`Failed to get response: ${e}. See console for more info.`);
}
}
document.addEventListener('DOMContentLoaded', () => {
const responseDiv = document.getElementById('response');
function showGreetings(data) {
responseDiv.innerHTML = data.message;
}
const nameTxt = document.getElementById('name');
const greetingBtn = document.getElementById('greetingBtn');
nameTxt.addEventListener('keyup', (e) => {
if (e.key === 'Enter') {
greetingBtn.click();
}
});
greetingBtn.addEventListener('click', () => {
const name = nameTxt.value
getGreetings(name, showGreetings);
nameTxt.value = '';
nameTxt.focus();
});
getGreetings(undefined, showGreetings);
nameTxt.focus();
});
</script>
</head>
<body>
<p>Nothing to see here</p>
<div id="response"></div>
<div>
<input id="name" type="text"></input>
<button id="greetingBtn">Send Greetings</button>
</div>
<body>
</html>
9. The Lambda Func Module
The Lambda func module defines the Lambda function with source code in S3 bucket.
# ref: ./modules/aws-lambda-func/main.tf
resource "aws_lambda_function" "hello_world" {
function_name = "HelloWorld"
s3_bucket = var.s3_bucket_id
s3_key = var.s3_object_key
runtime = "nodejs20.x"
handler = "hello.handler"
source_code_hash = var.source_code_hash
role = aws_iam_role.lambda_exec.arn
}
resource "aws_cloudwatch_log_group" "hello_world" {
name = "/aws/lambda/${aws_lambda_function.hello_world.function_name}"
retention_in_days = 1
}
resource "aws_iam_role" "lambda_exec" {
name = "serverless-lambda-mec"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Action = "sts:AssumeRole"
Effect = "Allow"
Sid = ""
Principal = {
Service = "lambda.amazonaws.com"
}
}]
})
tags = {
ManagedBy = "Terraform"
}
}
resource "aws_iam_role_policy_attachment" "lambda_policy" {
role = aws_iam_role.lambda_exec.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
The input and output are as expected:
# ref: ./modules/aws-lambda-func/variables.tf
variable "s3_bucket_id" {
description = "s3 bucket id"
type = string
}
variable "s3_object_key" {
description = "s3 object key"
type = string
}
variable "source_code_hash" {
description = "source code hash"
type = string
}
# ref: ./modules/aws-lambda-func/outputs.tf
# ref: https://developer.hashicorp.com/terraform/tutorials/aws/lambda-api-gateway
output "function_name" {
description = "name of the Lambda function"
value = aws_lambda_function.hello_world.function_name
}
output "invoke_arn" {
description = "the invocation arn"
value = aws_lambda_function.hello_world.invoke_arn
}
output "lambda_log_group_arn" {
description = "lambda function log group arn"
value = aws_cloudwatch_log_group.hello_world.arn
}
10. The API Gateway Module
The API Gateway module defines an API gateway and wires it up with exisitng Lambda function.
# ref: ./modules/aws-api-gateway/main.tf
# ref: https://developer.hashicorp.com/terraform/tutorials/aws/lambda-api-gateway
# defines the API Gateway
resource "aws_apigatewayv2_api" "lambda" {
# name = "serverless_lambda_gw_mec"
name = var.api_gateway_name
protocol_type = "HTTP"
}
resource "aws_apigatewayv2_stage" "lambda" {
api_id = aws_apigatewayv2_api.lambda.id
# name = "serverless_lambda_stage_mec"
name = var.api_gateway_stage_name
auto_deploy = true
access_log_settings {
destination_arn = var.lambda_log_group_arn
format = jsonencode({
requestedId = "$context.requestId"
sourceIp = "$context.identity.sourceIp"
requestTime = "$context.requestTime"
protocol = "$context.protocol"
httpMethod = "$context.httpMethod"
resourcePath = "$context.resourcePath"
routeKey = "$context.routeKey"
status = "$context.status"
responseLength = "$context.responseLength"
integrationErrorMessage = "$context.integrationErrorMessage"
})
}
}
resource "aws_apigatewayv2_integration" "hello_world" {
api_id = aws_apigatewayv2_api.lambda.id
integration_uri = var.func_invoke_arn
integration_type = "AWS_PROXY"
integration_method = "POST"
}
resource "aws_apigatewayv2_route" "hello_world" {
api_id = aws_apigatewayv2_api.lambda.id
# Note that the path here should match the path_pattern in CloudFront config
route_key = "GET /${var.api_path_prefix}/hello"
target = "integrations/${aws_apigatewayv2_integration.hello_world.id}"
}
resource "aws_cloudwatch_log_group" "api_gw" {
name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}"
retention_in_days = 1
}
resource "aws_lambda_permission" "api_gw" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = var.func_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*"
}
Input and output mainly concern with Lambda function and the API endpoint.
# ref: ./modules/aws-api-gateway/variables.tf
# ref: https://developer.hashicorp.com/terraform/tutorials/aws/lambda-api-gateway
variable "api_gateway_name" {
description = "api gateway name"
type = string
}
variable "api_gateway_stage_name" {
description = "api gateway stage name"
type = string
}
variable "lambda_log_group_arn" {
description = "Lambda function log group ARN"
type = string
}
variable "func_invoke_arn" {
description = "the Lambda function invoke arn"
type = string
}
variable "func_name" {
description = "Lambda function name"
type = string
}
variable "api_path_prefix" {
description = "API Path prefix (without leading /), e.g. api, api-test, etc."
type = string
}
# ref: ./modules/aws-api-gateway/outputs.tf
# ref: https://developer.hashicorp.com/terraform/tutorials/aws/lambda-api-gateway
output "invoke_url" {
description = "base URL for API gateway stage"
value = aws_apigatewayv2_stage.lambda.invoke_url
}
output "api_id" {
description = "api id"
value = aws_apigatewayv2_api.lambda.id
}
output "stage_name" {
description = "api stage name"
value = aws_apigatewayv2_stage.lambda.name
}
output "api_endpoint" {
description = "the endpoint of the API"
value = aws_apigatewayv2_api.lambda.api_endpoint
}
output "api_stage_name"{
description = "API stage name"
value = aws_apigatewayv2_stage.lambda.name
}
11. The CloudFront Module: Updated
The CloudFront module adds routing config for API endpoints in the aws_cloudfront_distribution block.
# ref: ./modules/aws-cloudfront-website/main.tf
resource "aws_cloudfront_distribution" "s3_distribution" {
... (existing S3 configs are left intact)
origin {
# Extract domain from endpoint by stripping 'https://' prefix
domain_name = replace(var.api_endpoint, "/^https?://([^/]*).*/", "$1")
origin_id = "api-gateway-origin-mec-hello-world-2025"
origin_path = "/${var.api_stage_name}"
custom_origin_config {
http_port = 80
https_port = 443
origin_protocol_policy = "https-only"
origin_ssl_protocols = ["TLSv1.2"]
}
}
ordered_cache_behavior {
# Requests to the API gateway domain goes here
# path_pattern should match the path in aws_apigatewayv2_route.route_key
# path_pattern = "/api/*"
path_pattern = "/${var.api_path_prefix}/*"
target_origin_id = "api-gateway-origin-mec-hello-world-2025"
allowed_methods = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "DELETE", "PATCH"]
cached_methods = ["GET", "HEAD", "OPTIONS"]
viewer_protocol_policy = "redirect-to-https"
# ref: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html#managed-origin-request-policy-all-viewer-except-host-header
# ref: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html
# Use AWS managed policy for API Gateway caching (for API call)
origin_request_policy_id = "b689b0a8-53d0-40ab-baf2-68738e2966ac" # AllViewerExceptHostHeader
cache_policy_id = "4135ea2d-6df8-44a3-9df3-4b5a84be39ad" # CachingDisabled
compress = true
# Typically APIs are not cached, so set TTSs to 0
min_ttl = 0
default_ttl = 0
max_ttl = 0
}
}
New input variables are all also about the API endpoint.
# ref: ./modules/aws-cloudfront-website/variables.tf
...existing S3 configs are left intact
variable "api_endpoint" {
# description = "custom FULL domain name in Route 53 for the API call"
description = "API gateway endpoint"
type = string
}
variable "api_stage_name" {
description = "API Stage Name"
type = string
}
variable "api_path_prefix" {
description = "API Path prefix (without leading /), e.g. api, api-test, etc."
type = string
}
12. The Root Module: Updated
The root module have two changes:
- Invoke the new modules defined for Lambda and API Gateway, and
- Update existing CloudFront block to wire things up
# ref: ./main.tf
# ============================================================
# Update existing blocks
# ============================================================
locals {
...keep existing content intact
full_api_domain_name = "api.${var.custom_domain_name}.${var.existing_zone_name}"
zip_file_name = "hello-world.zip"
api_path_prefix = "api"
}
module "website_cloudfront" {
...keep existing content intact
api_endpoint = module.api_gateway.api_endpoint
api_stage_name = module.api_gateway.api_stage_name
api_path_prefix = local.api_path_prefix
}
# ============================================================
# New blocks
# ============================================================
data "archive_file" "lambda_hello_world" {
type = "zip"
source_dir = "${path.module}/backend"
output_path = "${path.module}/${local.zip_file_name}"
}
resource "aws_s3_object" "lambda_hello_world" {
# bucket = aws_s3_bucket.s3_bucket.id
bucket = module.website_s3_bucket.id
key = local.zip_file_name
source = data.archive_file.lambda_hello_world.output_path
etag = filemd5(data.archive_file.lambda_hello_world.output_path)
}
module "lambda_func" {
source = "./modules/aws-lambda-func"
s3_bucket_id = module.website_s3_bucket.id
# s3_object_key = module.website_s3_bucket.object_key
s3_object_key = local.zip_file_name
source_code_hash = data.archive_file.lambda_hello_world.output_base64sha256
}
module "api_gateway" {
source = "./modules/aws-api-gateway"
api_gateway_name = "mec-hello-world-lambda-gw"
api_gateway_stage_name = "mec-stage"
lambda_log_group_arn = module.lambda_func.lambda_log_group_arn
func_invoke_arn = module.lambda_func.invoke_arn
func_name = module.lambda_func.function_name
api_path_prefix = local.api_path_prefix
}
13. Conclusion: Pt. 2
Update and apply the new Terraform plan:
#!/bin/sh # current work directory . terraform init terraform plan --out out/plan.out terraform apply --auto-aprove out/plan.out curl https://mec.tangwenfei.org/api/hello # Test the site in browser # open -a "Google Chrome" https://mec.tangwenfei.org # Release resource # terraform destroy
Now dynamic content could be requested from https://mec.tangwenfei.org/api/hello.
14. References
acm_certificate: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificateacm_certificate_validation: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate_validations3_bucket: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_buckets3_bucket_website_configuration: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_website_configurations3_bucket_policy: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policycloudfront_oac: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_origin_access_controlcloudfront_distribution: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_distributionroute53_zone: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_zoneroute53_record: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_recordaws_lambda_function: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_functionaws_lambda_permission: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permissionaws_cloudwatch_log_group: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_groupaws_iam_role: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_roleaws_iam_role_policy_attachment: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy_attachmentaws_apigatewayv2_api: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_apiaws_apigatewayv2_stage: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_stageaws_apigatewayv2_integration: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_integrationaws_apigatewayv2_route: https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_route- Managed Origin Request Policies: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-origin-request-policies.html
- Managed Cache Policies: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html