Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ This example repo shows how to run terraform on GitHub Actions and automatically

Please note: You are unable to view the change in Overmind as it is a change tied to our personal account.

# Serverless Demo Environment

This repository now also includes a fully self-contained Terraform demo that provisions an interconnected but low-cost AWS architecture:

- Two-AZ VPC with VPC endpoints (S3, DynamoDB, SSM) so the stack avoids NAT gateways.
- S3 buckets for the SPA, uploads, and processed assets; DynamoDB tables plus Aurora Serverless v2 for structured querying.
- Lambda-powered HTTP API with a custom Lambda authorizer, presigned upload helper, and an SNS → Step Functions asset pipeline that fans out through EventBridge to Lambda, Slack, and SQS.
- CloudFront distribution (with WAF and custom domain) that fronts the static site and proxies `/api/*` traffic to API Gateway.
- Operational guardrails such as CloudWatch alarms, AWS Budgets, and an auto-stopped t4g.nano bastion reachable through SSM.

You can toggle the entire demo via the `enable_demo` variable and customize domains, budgets, or CIDR restrictions in `variables.tf`. Run `terraform init && terraform plan` to inspect the environment, and deploy with `terraform apply` when ready.
# Forking this repo

If you would like to use this repo as an example. Follow these steps:
Expand Down
19 changes: 19 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,22 @@ module "scenarios" {

example_env = var.example_env
}

module "serverless_demo" {
count = var.enable_demo ? 1 : 0

source = "./modules/demo"

providers = {
aws.us_east_1 = aws.us_east_1
}

project_name = var.project_name
allowed_uploader_cidr_blocks = var.allowed_uploader_cidr_blocks
slack_webhook_url = var.slack_webhook_url
default_tags = var.default_tags
aurora_min_acus = var.aurora_min_acus
aurora_max_acus = var.aurora_max_acus
budget_monthly_limit = var.budget_monthly_limit
bastion_key_name = var.bastion_key_name
}
88 changes: 88 additions & 0 deletions modules/demo/api.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
resource "aws_apigatewayv2_api" "recipes" {
name = "${local.name_prefix}-api"
protocol_type = "HTTP"
tags = local.tags
}

resource "aws_apigatewayv2_integration" "recipes" {
api_id = aws_apigatewayv2_api.recipes.id
integration_type = "AWS_PROXY"
integration_uri = aws_lambda_function.api.arn
payload_format_version = "2.0"
}

resource "aws_apigatewayv2_integration" "presign" {
api_id = aws_apigatewayv2_api.recipes.id
integration_type = "AWS_PROXY"
integration_uri = aws_lambda_function.presign.arn
payload_format_version = "2.0"
}

resource "aws_apigatewayv2_authorizer" "recipes" {
api_id = aws_apigatewayv2_api.recipes.id
authorizer_type = "REQUEST"
authorizer_uri = aws_lambda_function.authorizer.invoke_arn
authorizer_payload_format_version = "2.0"
identity_sources = ["$request.header.Authorization"]
name = "${local.name_prefix}-authorizer"
}

resource "aws_apigatewayv2_route" "recipes_get" {
api_id = aws_apigatewayv2_api.recipes.id
route_key = "GET /api/recipes"

target = "integrations/${aws_apigatewayv2_integration.recipes.id}"
authorizer_id = aws_apigatewayv2_authorizer.recipes.id
authorization_type = "CUSTOM"
}

resource "aws_apigatewayv2_route" "recipes_post" {
api_id = aws_apigatewayv2_api.recipes.id
route_key = "POST /api/recipes"

target = "integrations/${aws_apigatewayv2_integration.recipes.id}"
authorizer_id = aws_apigatewayv2_authorizer.recipes.id
authorization_type = "CUSTOM"
}

resource "aws_apigatewayv2_route" "presign" {
api_id = aws_apigatewayv2_api.recipes.id
route_key = "POST /api/uploads/presign"

target = "integrations/${aws_apigatewayv2_integration.presign.id}"
authorizer_id = aws_apigatewayv2_authorizer.recipes.id
authorization_type = "CUSTOM"
}

resource "aws_apigatewayv2_stage" "recipes" {
api_id = aws_apigatewayv2_api.recipes.id
name = "$default"
auto_deploy = true

tags = local.tags
}

resource "aws_lambda_permission" "api_invoke" {
statement_id = "AllowAPIGatewayInvoke"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.api.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.recipes.execution_arn}/*/*"
}

resource "aws_lambda_permission" "presign_invoke" {
statement_id = "AllowAPIGatewayPresign"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.presign.function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.recipes.execution_arn}/*/*"
}

resource "aws_lambda_permission" "authorizer_invoke" {
statement_id = "AllowAPIAuthorizer"
action = "lambda:InvokeFunction"
function_name = aws_lambda_function.authorizer.function_name
principal = "apigateway.amazonaws.com"
source_arn = aws_apigatewayv2_api.recipes.execution_arn
}

Loading
Loading