From c4ede50c37d2ce35e3f0a7b7bf3237e20ad47455 Mon Sep 17 00:00:00 2001 From: archiev4 Date: Wed, 11 Feb 2026 19:50:12 +0530 Subject: [PATCH] Lambda DDB Tenant Isolation commit-1 --- lambda-ddb-tenant-isolation/README.md | 127 ++++ .../architecture/architecture.png | Bin 0 -> 20190 bytes .../example-pattern.json | 55 ++ .../isolated_lambda_function.zip | Bin 0 -> 1611 bytes lambda-ddb-tenant-isolation/main.tf | 681 ++++++++++++++++++ .../standard_lambda_function.zip | Bin 0 -> 1602 bytes 6 files changed, 863 insertions(+) create mode 100644 lambda-ddb-tenant-isolation/README.md create mode 100644 lambda-ddb-tenant-isolation/architecture/architecture.png create mode 100644 lambda-ddb-tenant-isolation/example-pattern.json create mode 100644 lambda-ddb-tenant-isolation/isolated_lambda_function.zip create mode 100644 lambda-ddb-tenant-isolation/main.tf create mode 100644 lambda-ddb-tenant-isolation/standard_lambda_function.zip diff --git a/lambda-ddb-tenant-isolation/README.md b/lambda-ddb-tenant-isolation/README.md new file mode 100644 index 0000000000..86a5eea637 --- /dev/null +++ b/lambda-ddb-tenant-isolation/README.md @@ -0,0 +1,127 @@ +# Multi-tenant API with Amazon API Gateway and AWS Lambda Tenant Isolation + +![architecture](architecture/architecture.png) + +This pattern implements a serverless multi-tenant counter API using Amazon API Gateway, AWS Lambda and Amazon DynamoDB. It deploys two parallel endpoints, one without tenant isolation and one with full per-tenant isolation, to demonstrate how shared state leads to cross-tenant data leakage in multi-tenant applications. + +When a request hits the standard endpoint, the Lambda function increments a single shared counter in DynamoDB, exposing activity across all tenants. The isolated endpoint requires a tenant ID header, which maps to a dedicated Lambda execution environment and a tenant-specific DynamoDB row. Each tenant gets an independent counter, ensuring complete data separation. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/lambda-ddb-tenant-isolation + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [Terraform](https://learn.hashicorp.cxom/tutorials/terraform/install-cli?in=terraform/aws-get-started) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to the pattern directory: + ``` + cd lambda-ddb-tenant-isolation + ``` +1. From the command line, initialize terraform to downloads and installs the providers defined in the configuration: + ``` + terraform init + ``` +1. From the command line, apply the configuration in the main.tf file: + ``` + terraform apply -auto-approve + ``` +1. During the prompts + #var.aws_region + - Enter a value: {enter the region for deployment} + + #var.prefix + - Enter a value: {enter any prefix to associate with resources} + +1. Note the outputs from the Terraform deployment process. These contain the resource names and/or ARNs which are used for testing. + +## Testing + +Use [curl](https://curl.se/) to send a HTTP GET request to the API. + +1. Make a GET request to the Standard API endpoint using the following cURL command: +``` +curl -H "x-tenant-id: TENANT_ID" "STANDARD_API_ENDPOINT" +``` +Note: Replace the `TENANT_ID` with a unique Tenant ID of your choice and `STANDARD_API_ENDPOINT` with the generated `standard_multi_tenant_api_endpoint_url` from Terraform (refer to the Terraform Outputs section) + +For ex, +``` +curl -H "x-tenant-id: Tenant-1" "https://1234abcde.execute-api.us-east-1.amazonaws.com/dev/standard" +``` + +The response would be, +``` +{"counter": 1, "tenant_id": "Tenant-1", "isolation_enabled": false, "message": "This function does NOT provide tenant isolation and every tenant reads and writes the same DynamoDB row. Incremented counter is shared across all the tenants"} +``` + +Test this for another Tenant ID. For ex, +``` +curl -H "x-tenant-id: Tenant-2" "https://1234abcde.execute-api.us-east-1.amazonaws.com/dev/standard" +``` + +The response would be, +``` +{"counter": 2, "tenant_id": "Tenant-2", "isolation_enabled": false, "message": "This function does NOT provide tenant isolation and every tenant reads and writes the same DynamoDB row. Incremented counter is shared across all the tenants"} +``` + +1. Now make a GET request to the Isolated API endpoint using the following cURL command: +``` +curl -H "x-tenant-id: TENANT_ID" "ISOLATED_API_ENDPOINT" +``` +Note: Replace the `TENANT_ID` with a unique Tenant ID of your choice and `ISOLATED_API_ENDPOINT` with the generated `isolated_tenant_api_endpoint_url` from Terraform (refer to the Terraform Outputs section) + +For ex, +``` +curl -H "x-tenant-id: Tenant-1" "https://1234abcde.execute-api.us-east-1.amazonaws.com/dev/isolated" +``` + +The response would be, +``` +{"counter": 1, "tenant_id": "Tenant-1", "isolation_enabled": true, "message": "Counter incremented for tenant Tenant-1"} +``` + +Test this for another Tenant ID. For ex, +``` +curl -H "x-tenant-id: Tenant-2" "https://1234abcde.execute-api.us-east-1.amazonaws.com/dev/isolated" +``` + +The response would be, +``` +{"counter": 2, "tenant_id": "Tenant-2", "isolation_enabled": true, "message": "Counter incremented for tenant Tenant-2"} +``` + +## Cleanup + +1. Change directory to the pattern directory: + ``` + cd serverless-patterns/lambda-ddb-tenant-isolation + ``` + +1. Delete all created resources + ``` + terraform destroy -auto-approve + ``` + +1. During the prompts: + ``` + Enter all details as entered during creation. + ``` + +1. Confirm all created resources has been deleted + ``` + terraform show + ``` +---- +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 \ No newline at end of file diff --git a/lambda-ddb-tenant-isolation/architecture/architecture.png b/lambda-ddb-tenant-isolation/architecture/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..227756cacff4138427b743b6cec3af7cdd9aa15c GIT binary patch literal 20190 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 literal 0 HcmV?d00001 diff --git a/lambda-ddb-tenant-isolation/example-pattern.json b/lambda-ddb-tenant-isolation/example-pattern.json new file mode 100644 index 0000000000..7a4df9d74a --- /dev/null +++ b/lambda-ddb-tenant-isolation/example-pattern.json @@ -0,0 +1,55 @@ +{ + "title": "Multi-tenant API with Amazon API Gateway and AWS Lambda Tenant Isolation", + "description": "This pattern implements a serverless multi-tenant API using Amazon API Gateway, AWS Lambda and Amazon DynamoDB to demonstrate tenant isolation.", + "language": "Python", + "level": "200", + "framework": "Terraform", + "introBox": { + "headline": "How it works", + "text": [ + "This solution works by exposing two API Gateway endpoints, /standard and /isolated, each backed by a separate Lambda function. When a request hits the /standard endpoint, the Lambda function increments a single shared counter row in DynamoDB, meaning all tenants read and write the same value. When a request hits the /isolated endpoint with an x-tenant-id header, API Gateway maps the header to the Lambda execution context, ensuring a dedicated execution environment per tenant, and the Lambda function increments a tenant-specific counter row in DynamoDB, keeping each tenant's data completely separate." + + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/lambda-ddb-tenant-isolation", + "templateURL": "serverless-patterns/lambda-ddb-tenant-isolation", + "projectFolder": "lambda-ddb-tenant-isolation", + "templateFile": "main.tf" + } + }, + "resources": { + "bullets": [ + { + "text": "Lambda Tenant Isolation", + "link": "https://docs.aws.amazon.com/lambda/latest/dg/tenant-isolation.html" + } + ] + }, + "deploy": { + "text": [ + "terraform init" + "terraform apply" + ] + }, + "testing": { + "text": [ + "See the GitHub repo for detailed testing instructions." + ] + }, + "cleanup": { + "text": [ + "terraform destroy" + "terraform show" + ] + }, + "authors": [ + { + "name": "Archana V", + "image": "https://media.licdn.com/dms/image/v2/D5603AQGhkVtEhllFEw/profile-displayphoto-shrink_200_200/B56ZZH3LL6H0AY-/0/1744962369852?e=1772668800&v=beta&t=y0t7bsQKJwy5McO395hDmW7QPu_K-a1WKXeTA0-ecno", + "bio": "Solutions Architect at AWS", + "linkedin": "archanavenkat" + } + ] +} diff --git a/lambda-ddb-tenant-isolation/isolated_lambda_function.zip b/lambda-ddb-tenant-isolation/isolated_lambda_function.zip new file mode 100644 index 0000000000000000000000000000000000000000..a8967f95b263b9299fc7dd963cd0bc3250362580 GIT binary patch literal 1611 zcmV-R2DJH5O9KQH000080E}EpTy(25ZnFgd0KX3a02=@R0BLh?Y+-a|WM6DyZDM3$ zUuJb~V{~b6ZZ2?ntykS{+cp%w*HfG^DmTc{ffn2zWw*_t7AXV0jG*f7ajq=IWU*S$di_#=ea|FN}eFIS?Vnq~0X@`B@y z0!#D*#53lvWX*M6GPtR!L{`QWP{>%8r&a))v$Qs;1Q+QNwL`_El>|Nz;q}?*JZW6K zhDN<%!mzSl2PMFS1gHj$Ze>(c4A}xD7R2mqqMN7-YzzGG&r%0!fJm*SN zj#zP##!&b$`=4aGmK+jh03ANx-|qu?Ni#TS|I|!rg0Cn1mKJ%2r1r?5rli^=)eCV* z&-qk@YtqS?6e&|^$PKia^l}W`PMMyGYyi&?V`o&)%oFLa$*{JY!V1Yu>#E@}c3q5L zjo-W*lipVP3MFo#>{xH4CZ!dNK;Ehj9o`6Vhw+PpdYU&;`8|%$z$r)6xY(@g>Kgdn zA&}ojG5MUrwJY;`zW<{C;E*(~!5BRmnwG#a@{npux)$DdOj$N~)S&9hbVim%@P-!) zK=ZsGSb@?t9K7BT-$}s^eERqS|1d&`l3TXwN?`l3t8mvT9PwBDwF=w1!;E9Dsn#-d z(lI&hEoN#gIO|yIQr}Xn=fM?1lZH0OD`lABbyqrxeo`LyAIqat6Z@Dwl19>Anyi`Z zJ+dTVh^188XpChGZUWE@M_h5KL*i6z{*mCwh z7gFDgI{2{NsOcLk_1_?@Uey!BTGrUoCJYYtaOOQaM*D!{b~Q*L#YRq3931K?l`+(! z%u_S3T%Hh2sbqGDeRVxNv08BISGWS14a064QC;D1Vo9!9xgO3}Y;kzssh`0rCCham zFRX~sypm=<2!2T3V&B+FQAa*a5Ge_p$UnWHleuM!!y%DUHh#^*SKB+Yu z(~>E)V<$xeh#Svj`M-RB(4w}CRv+@3#%)y&;9!IO++xackc+e9VLUoc#-n$nJ7oNm zUlGtbu0tQsOVX-VEvLrgrW4Y-g&M&vD7X_2F68Er!0o?uea;Y=PF? zua9eQz`%2?`}}xfH)z_GDM11Ri=mBW%{Is|LyF!oyquCFzd(6Jqeq?H^nQh*PH>k& z2cNdSH=M!UN7w2I_cm1W^Ja~crpGC~yMI{MN2WbvFN_zJ;U=`I3bRL|TR)laipP4v z5iW&n&u~gn7$z?EQggOJya6O)Qi0?~Y?Reyr9yMkd*EsFEz7y0?tQZ5mQ(|JCYHf4pg0p$WQoGArsnz!7l6)ICh8!FAhE|cLwZ!4s ze$_Zp4cw9x@z!rh6OHKUgG0KTYY(uxj?=D`70EWNeko3$gwYN@AHn9DynA9DMT)M& zo!WL!{q0^ZSKYGFyZjGOO928N0~7!N00;n#TuNMYt21u11polQ4*&og0000000001 z0001_fdBvi0BLh?Y+-a|WM6DyZDM3$UuJb~V{~b6ZZ2?nP)h{{000000RRC2NdN!< J>;(V-004gO1S9|e literal 0 HcmV?d00001 diff --git a/lambda-ddb-tenant-isolation/main.tf b/lambda-ddb-tenant-isolation/main.tf new file mode 100644 index 0000000000..ed81dcb009 --- /dev/null +++ b/lambda-ddb-tenant-isolation/main.tf @@ -0,0 +1,681 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.31.0" + } + } +} + +provider "aws" { + region = var.aws_region +} + +# ────────────────────────────────────────────── +# Variables — Terraform will prompt for prefix and region +# ────────────────────────────────────────────── + +variable "aws_region" { + description = "AWS region for resources (e.g. us-east-1, us-west-2)" + type = string + + validation { + condition = can(regex("^[a-z]{2}-[a-z]+-[0-9]+$", var.aws_region)) + error_message = "Must be a valid AWS region (e.g. us-east-1, eu-west-2)." + } +} + +variable "prefix" { + description = "Unique prefix for all resource names — avoids collisions (e.g. your initials or team name)" + type = string + + validation { + condition = can(regex("^[a-z0-9][a-z0-9\\-]{1,20}$", var.prefix)) + error_message = "Prefix must be 2-21 lowercase alphanumeric characters or hyphens, starting with a letter or number." + } +} + +variable "environment" { + description = "Environment name for resource naming" + type = string + default = "dev" + validation { + condition = contains(["dev", "staging", "prod"], var.environment) + error_message = "Environment must be dev, staging, or prod." + } +} + +# ────────────────────────────────────────────── +# Locals — single place that builds the name prefix +# ────────────────────────────────────────────── + +locals { + name_prefix = "${var.prefix}-tenant-iso" +} + +# Data sources +data "aws_caller_identity" "current" {} +data "aws_region" "current" {} + +# ────────────────────────────────────────────── +# DynamoDB Tables +# ────────────────────────────────────────────── + +resource "aws_dynamodb_table" "shared_counter_table" { + name = "${local.name_prefix}-shared-counters" + billing_mode = "PAY_PER_REQUEST" + hash_key = "pk" + + attribute { + name = "pk" + type = "S" + } + + tags = { + Environment = var.environment + Prefix = var.prefix + Purpose = "Shared counter - demonstrates lack of tenant isolation" + } +} + +resource "aws_dynamodb_table" "isolated_counter_table" { + name = "${local.name_prefix}-isolated-counters" + billing_mode = "PAY_PER_REQUEST" + hash_key = "tenant_id" + + attribute { + name = "tenant_id" + type = "S" + } + + tags = { + Environment = var.environment + Prefix = var.prefix + Purpose = "Per-tenant counters with tenant isolation" + } +} + +# ────────────────────────────────────────────── +# IAM +# ────────────────────────────────────────────── + +resource "aws_iam_role" "lambda_execution_role" { + name = "${local.name_prefix}-lambda-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { + Service = "lambda.amazonaws.com" + } + Action = "sts:AssumeRole" + } + ] + }) + + tags = { + Environment = var.environment + Prefix = var.prefix + } +} + +resource "aws_iam_role_policy_attachment" "lambda_basic_execution" { + role = aws_iam_role.lambda_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" +} + +resource "aws_iam_role_policy" "cloudwatch_logs_policy" { + name = "${local.name_prefix}-cw-logs" + role = aws_iam_role.lambda_execution_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ] + Resource = "arn:aws:logs:${data.aws_region.current.id}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/${local.name_prefix}-*" + } + ] + }) +} + +resource "aws_iam_role_policy" "dynamodb_policy" { + name = "${local.name_prefix}-dynamodb" + role = aws_iam_role.lambda_execution_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Sid = "AllowSharedTable" + Effect = "Allow" + Action = [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem" + ] + Resource = aws_dynamodb_table.shared_counter_table.arn + }, + { + Sid = "AllowIsolatedTable" + Effect = "Allow" + Action = [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem" + ] + Resource = aws_dynamodb_table.isolated_counter_table.arn + } + ] + }) +} + +# ────────────────────────────────────────────── +# CloudWatch Log Groups +# ────────────────────────────────────────────── + +resource "aws_cloudwatch_log_group" "counter_standard_log_group" { + name = "/aws/lambda/${local.name_prefix}-counter-standard" + retention_in_days = 14 + + tags = { + Environment = var.environment + Prefix = var.prefix + } +} + +resource "aws_cloudwatch_log_group" "counter_isolated_log_group" { + name = "/aws/lambda/${local.name_prefix}-counter-isolated" + retention_in_days = 14 + + tags = { + Environment = var.environment + Prefix = var.prefix + } +} + +# ────────────────────────────────────────────── +# Lambda Functions +# ────────────────────────────────────────────── + +resource "aws_lambda_function" "counter_standard_function" { + filename = "standard_lambda_function.zip" + function_name = "${local.name_prefix}-counter-standard" + role = aws_iam_role.lambda_execution_role.arn + handler = "standard_lambda_function.lambda_handler" + runtime = "python3.14" + timeout = 30 + memory_size = 128 + description = "[${var.prefix}] Lambda without tenant isolation — shared DynamoDB counter" + + environment { + variables = { + LOG_LEVEL = "INFO" + TABLE_NAME = aws_dynamodb_table.shared_counter_table.name + } + } + + tags = { + Environment = var.environment + Prefix = var.prefix + } + + depends_on = [ + aws_cloudwatch_log_group.counter_standard_log_group, + aws_iam_role_policy_attachment.lambda_basic_execution, + aws_iam_role_policy.dynamodb_policy + ] +} + +resource "aws_lambda_function" "counter_isolated_function" { + filename = "isolated_lambda_function.zip" + function_name = "${local.name_prefix}-counter-isolated" + role = aws_iam_role.lambda_execution_role.arn + handler = "isolated_lambda_function.lambda_handler" + runtime = "python3.14" + timeout = 30 + memory_size = 128 + description = "[${var.prefix}] Lambda with tenant isolation — per-tenant DynamoDB counter" + + tenancy_config { + tenant_isolation_mode = "PER_TENANT" + } + + environment { + variables = { + LOG_LEVEL = "INFO" + TABLE_NAME = aws_dynamodb_table.isolated_counter_table.name + } + } + + tags = { + Environment = var.environment + Prefix = var.prefix + } + + depends_on = [ + aws_cloudwatch_log_group.counter_isolated_log_group, + aws_iam_role_policy_attachment.lambda_basic_execution, + aws_iam_role_policy.dynamodb_policy + ] +} + +# ────────────────────────────────────────────── +# API Gateway REST API +# ────────────────────────────────────────────── + +resource "aws_api_gateway_rest_api" "api_gateway" { + name = "${local.name_prefix}-api" + description = "[${var.prefix}] API Gateway for Lambda tenant isolation demonstration" + + endpoint_configuration { + types = ["REGIONAL"] + } + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = "*" + Action = "execute-api:Invoke" + Resource = "*" + } + ] + }) + + tags = { + Environment = var.environment + Prefix = var.prefix + } +} + +# Request Validator +resource "aws_api_gateway_request_validator" "request_validator" { + name = "${local.name_prefix}-request-validator" + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + validate_request_parameters = true + validate_request_body = false +} + +# Error Model +resource "aws_api_gateway_model" "error_model" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + name = "ErrorModel" + content_type = "application/json" + + schema = jsonencode({ + "$schema" = "http://json-schema.org/draft-04/schema#" + title = "Error Schema" + type = "object" + properties = { + error = { + type = "string" + } + message = { + type = "string" + } + statusCode = { + type = "integer" + } + } + }) +} + +# ──────────────── Standard endpoint ──────────────── + +resource "aws_api_gateway_resource" "standard_resource" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + parent_id = aws_api_gateway_rest_api.api_gateway.root_resource_id + path_part = "standard" +} + +resource "aws_api_gateway_method" "standard_method" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.standard_resource.id + http_method = "GET" + authorization = "NONE" + + request_parameters = { + "method.request.header.tenant-id" = false + } +} + +resource "aws_api_gateway_integration" "standard_integration" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.standard_resource.id + http_method = aws_api_gateway_method.standard_method.http_method + integration_http_method = "POST" + type = "AWS_PROXY" + uri = aws_lambda_function.counter_standard_function.invoke_arn + + request_parameters = { + "integration.request.header.X-Amz-Tenant-Id" = "method.request.header.tenant-id" + } +} + +resource "aws_api_gateway_method_response" "standard_response_200" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.standard_resource.id + http_method = aws_api_gateway_method.standard_method.http_method + status_code = "200" + + response_models = { + "application/json" = "Empty" + } +} + +resource "aws_api_gateway_method_response" "standard_response_405" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.standard_resource.id + http_method = aws_api_gateway_method.standard_method.http_method + status_code = "405" + + response_models = { + "application/json" = aws_api_gateway_model.error_model.name + } +} + +resource "aws_api_gateway_method_response" "standard_response_500" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.standard_resource.id + http_method = aws_api_gateway_method.standard_method.http_method + status_code = "500" + + response_models = { + "application/json" = aws_api_gateway_model.error_model.name + } +} + +# Standard OPTIONS (CORS) +resource "aws_api_gateway_method" "standard_options_method" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.standard_resource.id + http_method = "OPTIONS" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "standard_options_integration" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.standard_resource.id + http_method = aws_api_gateway_method.standard_options_method.http_method + type = "MOCK" + + request_templates = { + "application/json" = "{\"statusCode\": 200}" + } +} + +resource "aws_api_gateway_method_response" "standard_options_response_200" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.standard_resource.id + http_method = aws_api_gateway_method.standard_options_method.http_method + status_code = "200" + + response_parameters = { + "method.response.header.Access-Control-Allow-Headers" = true + "method.response.header.Access-Control-Allow-Methods" = true + "method.response.header.Access-Control-Allow-Origin" = true + } +} + +resource "aws_api_gateway_integration_response" "standard_options_integration_response" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.standard_resource.id + http_method = aws_api_gateway_method.standard_options_method.http_method + status_code = aws_api_gateway_method_response.standard_options_response_200.status_code + + response_parameters = { + "method.response.header.Access-Control-Allow-Headers" = "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" + "method.response.header.Access-Control-Allow-Methods" = "'GET,OPTIONS'" + "method.response.header.Access-Control-Allow-Origin" = "'*'" + } + + response_templates = { + "application/json" = "" + } +} + +# ──────────────── Isolated endpoint ──────────────── + +resource "aws_api_gateway_resource" "isolated_resource" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + parent_id = aws_api_gateway_rest_api.api_gateway.root_resource_id + path_part = "isolated" +} + +resource "aws_api_gateway_method" "isolated_method" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.isolated_resource.id + http_method = "GET" + authorization = "NONE" + request_validator_id = aws_api_gateway_request_validator.request_validator.id + + request_parameters = { + "method.request.header.x-tenant-id" = true + } +} + +resource "aws_api_gateway_integration" "isolated_integration" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.isolated_resource.id + http_method = aws_api_gateway_method.isolated_method.http_method + integration_http_method = "POST" + type = "AWS_PROXY" + uri = aws_lambda_function.counter_isolated_function.invoke_arn + + request_parameters = { + "integration.request.header.X-Amz-Tenant-Id" = "method.request.header.x-tenant-id" + } +} + +resource "aws_api_gateway_method_response" "isolated_response_200" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.isolated_resource.id + http_method = aws_api_gateway_method.isolated_method.http_method + status_code = "200" + + response_models = { + "application/json" = "Empty" + } +} + +resource "aws_api_gateway_method_response" "isolated_response_400" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.isolated_resource.id + http_method = aws_api_gateway_method.isolated_method.http_method + status_code = "400" + + response_models = { + "application/json" = aws_api_gateway_model.error_model.name + } +} + +resource "aws_api_gateway_method_response" "isolated_response_405" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.isolated_resource.id + http_method = aws_api_gateway_method.isolated_method.http_method + status_code = "405" + + response_models = { + "application/json" = aws_api_gateway_model.error_model.name + } +} + +resource "aws_api_gateway_method_response" "isolated_response_500" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.isolated_resource.id + http_method = aws_api_gateway_method.isolated_method.http_method + status_code = "500" + + response_models = { + "application/json" = aws_api_gateway_model.error_model.name + } +} + +# Isolated OPTIONS (CORS) +resource "aws_api_gateway_method" "isolated_options_method" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.isolated_resource.id + http_method = "OPTIONS" + authorization = "NONE" +} + +resource "aws_api_gateway_integration" "isolated_options_integration" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.isolated_resource.id + http_method = aws_api_gateway_method.isolated_options_method.http_method + type = "MOCK" + + request_templates = { + "application/json" = "{\"statusCode\": 200}" + } +} + +resource "aws_api_gateway_method_response" "isolated_options_response_200" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.isolated_resource.id + http_method = aws_api_gateway_method.isolated_options_method.http_method + status_code = "200" + + response_parameters = { + "method.response.header.Access-Control-Allow-Headers" = true + "method.response.header.Access-Control-Allow-Methods" = true + "method.response.header.Access-Control-Allow-Origin" = true + } +} + +resource "aws_api_gateway_integration_response" "isolated_options_integration_response" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + resource_id = aws_api_gateway_resource.isolated_resource.id + http_method = aws_api_gateway_method.isolated_options_method.http_method + status_code = aws_api_gateway_method_response.isolated_options_response_200.status_code + + response_parameters = { + "method.response.header.Access-Control-Allow-Headers" = "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,x-tenant-id'" + "method.response.header.Access-Control-Allow-Methods" = "'GET,OPTIONS'" + "method.response.header.Access-Control-Allow-Origin" = "'*'" + } + + response_templates = { + "application/json" = "" + } +} + +# ────────────────────────────────────────────── +# Lambda Permissions for API Gateway +# ────────────────────────────────────────────── + +resource "aws_lambda_permission" "standard_function_permission" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.counter_standard_function.function_name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.api_gateway.execution_arn}/*/*" +} + +resource "aws_lambda_permission" "isolated_function_permission" { + statement_id = "AllowAPIGatewayInvoke" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.counter_isolated_function.function_name + principal = "apigateway.amazonaws.com" + source_arn = "${aws_api_gateway_rest_api.api_gateway.execution_arn}/*/*" +} + +# ────────────────────────────────────────────── +# API Gateway Deployment & Stage +# ────────────────────────────────────────────── + +resource "aws_api_gateway_deployment" "api_gateway_deployment" { + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + + triggers = { + redeployment = sha1(jsonencode([ + aws_api_gateway_resource.standard_resource.id, + aws_api_gateway_method.standard_method.id, + aws_api_gateway_integration.standard_integration.id, + aws_api_gateway_resource.isolated_resource.id, + aws_api_gateway_method.isolated_method.id, + aws_api_gateway_integration.isolated_integration.id, + ])) + } + + lifecycle { + create_before_destroy = true + } + + depends_on = [ + aws_api_gateway_method.standard_method, + aws_api_gateway_integration.standard_integration, + aws_api_gateway_method.standard_options_method, + aws_api_gateway_integration.standard_options_integration, + aws_api_gateway_method.isolated_method, + aws_api_gateway_integration.isolated_integration, + aws_api_gateway_method.isolated_options_method, + aws_api_gateway_integration.isolated_options_integration, + ] +} + +resource "aws_api_gateway_stage" "api_gateway_stage" { + deployment_id = aws_api_gateway_deployment.api_gateway_deployment.id + rest_api_id = aws_api_gateway_rest_api.api_gateway.id + stage_name = var.environment + description = "[${var.prefix}] Stage for ${var.environment} environment" + + tags = { + Environment = var.environment + Prefix = var.prefix + } +} + +# ────────────────────────────────────────────── +# Outputs +# ────────────────────────────────────────────── + +output "prefix_used" { + description = "The prefix used for all resources" + value = var.prefix +} + +output "standard_lambda_function_name" { + description = "Standard Lambda function name" + value = aws_lambda_function.counter_standard_function.function_name +} + +output "isolated_lambda_function_name" { + description = "Isolated Lambda function name" + value = aws_lambda_function.counter_isolated_function.function_name +} + +output "dyanamodb_shared_counter_table_name" { + description = "DynamoDB table for shared counters" + value = aws_dynamodb_table.shared_counter_table.name +} + +output "dynamodb_isolated_counter_table_name" { + description = "DynamoDB table for isolated per-tenant counters" + value = aws_dynamodb_table.isolated_counter_table.name +} + +output "api_gateway_id" { + description = "API Gateway REST API ID" + value = aws_api_gateway_rest_api.api_gateway.id +} + +output "standard_multi_tenant_api_endpoint_url" { + description = "URL for the standard Lambda function endpoint" + value = "${aws_api_gateway_stage.api_gateway_stage.invoke_url}/standard" +} + +output "isolated_tenant_api_endpoint_url" { + description = "URL for the tenant-isolated Lambda function endpoint" + value = "${aws_api_gateway_stage.api_gateway_stage.invoke_url}/isolated" +} diff --git a/lambda-ddb-tenant-isolation/standard_lambda_function.zip b/lambda-ddb-tenant-isolation/standard_lambda_function.zip new file mode 100644 index 0000000000000000000000000000000000000000..7685c585b3f0ee54c2baaa14c2b58d3f13c7ed99 GIT binary patch literal 1602 zcmV-I2EF-EO9KQH0000806Jw#Tqan!JE{c$02>bg02=@R0CRL* zBdH`_5TFmyC+w5#9E!9h+vyKBAweYaJHPyR&WR{1r7ii(C>e?7y-?TJLSBbYWuovp zQ&#;J&9y4sYo;`h`F+MKE0i?EqYewfr5$UnbQGa7uE{~r8een!-mUfeels?(#BX`g z3z*6D?P))XlF^&@<8(SY8H2nsF_*VOEBTSGH|SNAFC{BgJ_C&NFV@_sT4%gR!xrsF zmdy$dkgyadxX@c`0(*NTS#V;&1AtacTY+(r8@?pqo?a5mC6ktz1p^!8%csx8f&iw0 zsJVNuv;wtd999ilUs)Nx{ll&e8>wc>#F<-O>&~jkNsU_JHbW9+d0+4RY zsVeKjihZvKe#L&pEI=h70G9I-oVgbUwJEX?*NXyvZQMzS)ypPB_=q`?3Zoa&K%(Hx zeKK>Ag-nJ=b6bO{lp`^FuU@_!5c-DYiWZ#Z zTpKu3X9(Hcqka)IJUsS^((q>`&_tVxJjH91Hc$}nb$s(08g=(RMf)NL(`^{2?jVx} zH~A)RJRMDw{mD@a*9yvCLm}s?>lQuJDgz<_afk^-49UUsKjC(v@*#O{2IP#{Mf29U zMUI^dxI_mXwe8edTQy2B|CiHu)6)-A+J6!cgjswAxii~?T`{BKv~-p?4zuG?D`B2% zEx{k=*NJFyd7LC?#2XhfWVCCm?cg&gA-|6k`Za@{Eqq?Rd_CA&q)ljK3XwpKPO9tRqL>U(K|0^m(sox-7ziUo@{fPo}HgU!#){?8uQ}0d4ZIy-M#XJ z30~e}ROEU7jL>aONoYCHIW!z0d@##M!PyPF=48h09p^GM(M=lT^NnJ&k+aQf5Z-uU z_|7D_a!h^oAa??;Uu{Dd_I(6vuxsymFazZl3zz?D9l%WGOE|ZOZh;R+HQencUBw89 z?}(SK=8zP?aJf+KErSp@a6oJWX@=eHL!LKhGp}Xl=C54A;Y?2x9IkFf&bMYBm?iS= zgr>naR@J30XNELvi+BmPv%Jgt$JJM$KZ_oRL8C+fS*>kyRQFN7j_uWheh>zEm#Hqm| za^e|s}hJ!@cyuX=b-~V2_hC9I{l&l~ZTmvCUqYgHX;BwCxRDFlWkrqZ_SNXAU zglo`m$2AWB**q1_rR4V&&yYNg)@nU2=~dVNKfE7FQg0elSypSHDUPS^-wLm)VOg31 z%?1rLwLqEy@psOT#ak5Z(zC5)4S9;9KT6N