From 0e1a7da6e5044424ddf66b25ca4871f4736ad07a Mon Sep 17 00:00:00 2001 From: Annangarachari R Date: Mon, 22 Jun 2026 23:02:48 +0530 Subject: [PATCH] New pattern: apigw-lambda-durable-tenant-isolation-callback-terraform --- .../README.md | 202 +++++++++++++ .../architecture/architecture.png | Bin 0 -> 42437 bytes .../example-pattern.json | 61 ++++ .../main.tf | 271 ++++++++++++++++++ .../src/callback/callback.mjs | 34 +++ .../src/callback/package.json | 1 + .../src/workflow/package.json | 1 + .../src/workflow/workflow.mjs | 27 ++ 8 files changed, 597 insertions(+) create mode 100644 apigw-lambda-durable-tenant-isolation-callback-terraform/README.md create mode 100644 apigw-lambda-durable-tenant-isolation-callback-terraform/architecture/architecture.png create mode 100644 apigw-lambda-durable-tenant-isolation-callback-terraform/example-pattern.json create mode 100644 apigw-lambda-durable-tenant-isolation-callback-terraform/main.tf create mode 100644 apigw-lambda-durable-tenant-isolation-callback-terraform/src/callback/callback.mjs create mode 100644 apigw-lambda-durable-tenant-isolation-callback-terraform/src/callback/package.json create mode 100644 apigw-lambda-durable-tenant-isolation-callback-terraform/src/workflow/package.json create mode 100644 apigw-lambda-durable-tenant-isolation-callback-terraform/src/workflow/workflow.mjs diff --git a/apigw-lambda-durable-tenant-isolation-callback-terraform/README.md b/apigw-lambda-durable-tenant-isolation-callback-terraform/README.md new file mode 100644 index 000000000..ff6cc3167 --- /dev/null +++ b/apigw-lambda-durable-tenant-isolation-callback-terraform/README.md @@ -0,0 +1,202 @@ +# Serverless multi-tenant workflow with external callback using Lambda durable functions + +![architecture](architecture/architecture.png) + +This pattern implements a multi-tenant workflow API that suspends execution while waiting for external confirmation and resumes processing when a callback arrives. It uses AWS Lambda durable functions for checkpoint-based execution and per-tenant compute isolation to ensure workflow state remains private across tenants. + +When a tenant submits a request to the `/workflow` endpoint, the function validates the request, checkpoints its progress, and suspends — consuming no compute resources while waiting. This wait can represent any external dependency: a payment gateway confirming a charge, a compliance system returning a decision, a human approver clicking "approve," or a third-party API completing an async operation. When the confirmation arrives at the `/callback` endpoint, the workflow resumes from its checkpoint, completes its final processing step, and returns the result — without re-executing any previously completed work. + +Multiple tenants can have workflows suspended simultaneously. Each tenant's execution environment is dedicated — cached configuration, intermediate results, and temporary files stored during Step 1 remain private and are never accessible to other tenants, even during long suspension periods. + +The combination of durable execution and tenant isolation is essential for this use case. During the suspension period, tenant-specific data from Step 1 — such as cached configuration, validated credentials, or computed intermediate results — remains in the execution environment's memory. Without per-tenant isolation, a different tenant's invocation could be routed to that same environment during the wait, exposing the suspended tenant's in-memory state. Without durable execution, the function cannot suspend and resume across the wait boundary, forcing you to externalize all intermediate state to a database and build a separate mechanism to trigger resumption. + +Please note that this pattern deploys API Gateway endpoints with no authorization. Production deployments should secure all endpoints with an appropriate authorization mechanism such as AWS IAM authorization, Amazon Cognito user pools, or API key usage plans. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/apigw-lambda-durable-tenant-isolation-callback-terraform + +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.com/tutorials/terraform/install-cli?in=terraform/aws-get-started) installed +* [Node.js 22.x](https://nodejs.org/) installed (for installing Lambda dependencies) + +## 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 + ``` +2. Change directory to the pattern directory: + ``` + cd apigw-lambda-durable-tenant-isolation-callback-terraform + ``` +3. Install Lambda function dependencies: + ``` + cd src/workflow && npm install && cd ../.. + cd src/callback && npm install && cd ../.. + ``` +4. From the command line, initialize Terraform to download and install the providers defined in the configuration: + ``` + terraform init + ``` +5. From the command line, apply the configuration in the main.tf file: + ``` + terraform apply + ``` +6. During the prompts: + + var.aws_region + - Enter a value: {enter the region for deployment, e.g. us-west-2} + + var.prefix + - Enter a value: {enter any prefix to associate with resources, or press Enter for default "durable-tenant"} + +7. Note the outputs from the Terraform deployment process. These contain the endpoint URLs used for testing. + +## How it works + +This pattern addresses a common SaaS requirement: processing tenant requests that depend on external confirmation before completing. Examples include order fulfillment waiting for payment confirmation, loan applications waiting for approval decisions, and document pipelines waiting for human review. + +1. **Start workflow** — Client sends a POST request with an `x-tenant-id` header to the `/workflow` endpoint. API Gateway maps the header to `X-Amz-Tenant-Id` and invokes the workflow Lambda asynchronously in a tenant-dedicated execution environment. + +2. **Step 1 executes and checkpoints** — The workflow function validates the request (e.g., checks business rules, caches tenant configuration) and creates a checkpoint. This step will not re-execute if the function is interrupted or resumed later. + +3. **Workflow suspends** — The function calls `waitForCallback`, which suspends execution at zero compute cost. A callback token is logged to CloudWatch. In production, this token would be sent to the external system (payment provider, approval system, etc.). + +4. **External confirmation arrives** — The external system sends the callback token and result payload to the `/callback` endpoint. This triggers the workflow to resume from its checkpoint. + +5. **Step 2 executes** — The workflow resumes, skipping Step 1 entirely (using the stored checkpoint result), and completes processing with the confirmation payload included in the final result. + +## Testing + +Use [curl](https://curl.se/) to send HTTP requests to the API. + +### 1. Start a workflow for Tenant A + +``` +curl -s -X POST "WORKFLOW_ENDPOINT" \ + -H "x-tenant-id: tenant-a" \ + -H "Content-Type: application/json" \ + -d '{"requestId": "req-001"}' +``` + +Note: Replace `WORKFLOW_ENDPOINT` with the `workflow_endpoint` output from Terraform. + +For example: +``` +curl -s -X POST "https://abc123.execute-api.us-west-2.amazonaws.com/dev/workflow" \ + -H "x-tenant-id: tenant-a" \ + -H "Content-Type: application/json" \ + -d '{"requestId": "req-001"}' +``` + +The response would be: +``` +{"message": "Workflow started"} +``` + +### 2. Start a workflow for Tenant B + +``` +curl -s -X POST "WORKFLOW_ENDPOINT" \ + -H "x-tenant-id: tenant-b" \ + -H "Content-Type: application/json" \ + -d '{"requestId": "req-002"}' +``` + +The response would be: +``` +{"message": "Workflow started"} +``` + +### 3. Verify workflows are suspended + +Open the AWS Lambda Console, navigate to the workflow function, and select the **Durable executions** tab. You should see two executions, both with status **Running** (suspended at waitForCallback): + +| Tenant ID | Status | +|-----------|--------| +| tenant-a | Running | +| tenant-b | Running | + +### 4. Get the callback token + +Open CloudWatch Logs, navigate to the log group `/aws/lambda/{prefix}-workflow`, and find the log entry containing the callback token for the tenant you want to resume: + +```json +{"waiting":true,"callbackToken":"Ab9hZX...","tenantId":"tenant-a"} +``` + +Copy the `callbackToken` value. + +### 5. Send callback to resume Tenant A only + +``` +curl -s -X POST "CALLBACK_ENDPOINT" \ + -H "Content-Type: application/json" \ + -d '{"callbackId": "CALLBACK_TOKEN", "payload": {"decision": "approved", "amount": 99.99}}' +``` + +Note: Replace `CALLBACK_ENDPOINT` with the `callback_endpoint` output from Terraform, and `CALLBACK_TOKEN` with the token from Step 4. + +For example: +``` +curl -s -X POST "https://abc123.execute-api.us-west-2.amazonaws.com/dev/callback" \ + -H "Content-Type: application/json" \ + -d '{"callbackId": "Ab9hZX...", "payload": {"decision": "approved", "amount": 99.99}}' +``` + +The response would be: +``` +{"message": "Callback sent"} +``` + +### 6. Verify results + +Return to the Lambda Console **Durable executions** tab: + +| Tenant ID | Status | +|-----------|--------| +| tenant-a | **Succeeded** | +| tenant-b | Running | + +Click on Tenant A's execution to see the complete timeline: +- Step 1: Validate — completed ✓ +- Wait: external-confirmation — callback received ✓ +- Step 2: Complete — completed ✓ (includes callback payload in result) + +Tenant B remains suspended and unaffected, confirming tenant isolation. + +### 7. Verify tenant isolation in CloudWatch Logs + +Navigate to CloudWatch Logs → `/aws/lambda/{prefix}-workflow`. Each tenant's logs appear in **separate log streams**, confirming they ran in isolated execution environments. Step 1 appears only once per tenant despite the function being re-invoked after the callback (confirming checkpoint replay skips completed work). + +## Cleanup + +1. Change directory to the pattern directory: + ``` + cd serverless-patterns/apigw-lambda-durable-tenant-isolation-callback-terraform + ``` + +1. Delete all created resources: + ``` + terraform destroy + ``` + +1. During the prompts: + ``` + Enter all details as entered during creation. + ``` + +1. Confirm all created resources have been deleted: + ``` + terraform show + ``` + +---- +Copyright 2026 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/apigw-lambda-durable-tenant-isolation-callback-terraform/architecture/architecture.png b/apigw-lambda-durable-tenant-isolation-callback-terraform/architecture/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..73aac29627f6e3d44a9e5d8598baa9409bda54a5 GIT binary patch literal 42437 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 K { + let body; + if (typeof event.body === 'string') { + body = JSON.parse(event.body); + } else { + body = event; + } + + const callbackId = body.callbackId; + const result = JSON.stringify(body.payload || {}); + + if (!callbackId) { + return { + statusCode: 400, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ error: "Missing callbackId" }) + }; + } + + await client.send(new SendDurableExecutionCallbackSuccessCommand({ + CallbackId: callbackId, + Result: result + })); + + return { + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ message: "Callback sent" }) + }; +}; diff --git a/apigw-lambda-durable-tenant-isolation-callback-terraform/src/callback/package.json b/apigw-lambda-durable-tenant-isolation-callback-terraform/src/callback/package.json new file mode 100644 index 000000000..d5508b037 --- /dev/null +++ b/apigw-lambda-durable-tenant-isolation-callback-terraform/src/callback/package.json @@ -0,0 +1 @@ +{"name":"durable-tenant-callback","version":"1.0.0","type":"module","dependencies":{"@aws-sdk/client-lambda":"^3.700.0"}} diff --git a/apigw-lambda-durable-tenant-isolation-callback-terraform/src/workflow/package.json b/apigw-lambda-durable-tenant-isolation-callback-terraform/src/workflow/package.json new file mode 100644 index 000000000..aee1af27d --- /dev/null +++ b/apigw-lambda-durable-tenant-isolation-callback-terraform/src/workflow/package.json @@ -0,0 +1 @@ +{"name":"durable-tenant-workflow","version":"1.0.0","type":"module","dependencies":{"@aws/durable-execution-sdk-js":"^1.0.0"}} diff --git a/apigw-lambda-durable-tenant-isolation-callback-terraform/src/workflow/workflow.mjs b/apigw-lambda-durable-tenant-isolation-callback-terraform/src/workflow/workflow.mjs new file mode 100644 index 000000000..0d5775173 --- /dev/null +++ b/apigw-lambda-durable-tenant-isolation-callback-terraform/src/workflow/workflow.mjs @@ -0,0 +1,27 @@ +import { withDurableExecution } from "@aws/durable-execution-sdk-js"; + +export const handler = withDurableExecution(async (event, context) => { + const tenantId = context.lambdaContext.tenantId; + const body = JSON.parse(event.body || "{}"); + const envId = process.env.AWS_LAMBDA_LOG_STREAM_NAME; + + const validated = await context.step(async (stepCtx) => { + stepCtx.logger.info(`Step 1: Validating for tenant ${tenantId}`); + return { tenantId, requestId: body.requestId, status: "validated", environment: envId }; + }); + + const callbackResult = await context.waitForCallback(async (callbackToken) => { + console.log(JSON.stringify({ waiting: true, callbackToken, tenantId })); + }); + + const completed = await context.step(async (stepCtx) => { + stepCtx.logger.info(`Step 2: Completing for tenant ${tenantId}`); + return { tenantId, requestId: body.requestId, status: "completed", callbackPayload: callbackResult, environment: envId }; + }); + + return { + statusCode: 200, + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ validated, completed }) + }; +});