From 1c19545963cd27992b5c8bea4b41e415e2404382 Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Fri, 8 May 2026 04:40:45 +0000 Subject: [PATCH 1/3] feat(appsync-events-lambda): Add AppSync Events real-time pub/sub pattern Deploy AppSync Events API with Lambda handler for WebSocket-based real-time messaging. First AppSync Events pattern in the repo. --- appsync-events-lambda-cdk/README.md | 58 ++++++++++++++++++ appsync-events-lambda-cdk/bin/app.ts | 6 ++ appsync-events-lambda-cdk/cdk.json | 1 + .../example-pattern.json | 1 + .../lib/appsync-events-lambda-stack.ts | 61 +++++++++++++++++++ appsync-events-lambda-cdk/package.json | 14 +++++ appsync-events-lambda-cdk/src/index.js | 22 +++++++ appsync-events-lambda-cdk/tsconfig.json | 1 + 8 files changed, 164 insertions(+) create mode 100644 appsync-events-lambda-cdk/README.md create mode 100644 appsync-events-lambda-cdk/bin/app.ts create mode 100644 appsync-events-lambda-cdk/cdk.json create mode 100644 appsync-events-lambda-cdk/example-pattern.json create mode 100644 appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts create mode 100644 appsync-events-lambda-cdk/package.json create mode 100644 appsync-events-lambda-cdk/src/index.js create mode 100644 appsync-events-lambda-cdk/tsconfig.json diff --git a/appsync-events-lambda-cdk/README.md b/appsync-events-lambda-cdk/README.md new file mode 100644 index 0000000000..fcbd37edb1 --- /dev/null +++ b/appsync-events-lambda-cdk/README.md @@ -0,0 +1,58 @@ +# AWS AppSync Events with Lambda + +This pattern deploys an AppSync Events API for real-time WebSocket pub/sub with Lambda event processing. + +Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/appsync-events-lambda-cdk + +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. + +## Requirements + +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Node.js 22+](https://nodejs.org/en/download/) installed +* [AWS CDK v2](https://docs.aws.amazon.com/cdk/v2/guide/getting-started.html) installed + +## Architecture + +``` +┌───────────┐ ┌─────────────────────┐ ┌──────────────┐ +│ Publisher │────▶│ AppSync Events API │────▶│ Subscribers │ +│ (HTTP) │ │ (WebSocket) │ │ (WebSocket) │ +└───────────┘ └─────────────────────┘ └──────────────┘ + │ + ▼ + ┌──────────────┐ + │ AWS Lambda │ + │ (Handler) │ + └──────────────┘ +``` + +## How it works + +1. Publishers send events via HTTP POST to the AppSync Events endpoint. +2. AppSync Events delivers messages to all WebSocket subscribers on that channel. +3. Channel namespaces (`notifications`, `alerts`) organize topics. +4. A Lambda handler can process/enrich events before delivery. + +## Deployment + +```bash +npm install +cdk deploy +``` + +## Testing + +```bash +# Publish an event (replace values from cdk deploy output) +curl -X POST "https:///event" \ + -H "x-api-key: " \ + -H "Content-Type: application/json" \ + -d '{"channel":"/notifications/general","events":["{\"message\":\"Hello from CDK\"}"]}' +``` + +## Cleanup + +```bash +cdk destroy +``` diff --git a/appsync-events-lambda-cdk/bin/app.ts b/appsync-events-lambda-cdk/bin/app.ts new file mode 100644 index 0000000000..ac84652fe4 --- /dev/null +++ b/appsync-events-lambda-cdk/bin/app.ts @@ -0,0 +1,6 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { AppsyncEventsLambdaStack } from '../lib/appsync-events-lambda-stack'; +const app = new cdk.App(); +new AppsyncEventsLambdaStack(app, 'AppsyncEventsLambdaStack'); diff --git a/appsync-events-lambda-cdk/cdk.json b/appsync-events-lambda-cdk/cdk.json new file mode 100644 index 0000000000..bebf368387 --- /dev/null +++ b/appsync-events-lambda-cdk/cdk.json @@ -0,0 +1 @@ +{"app":"npx ts-node --prefer-ts-exts bin/app.ts"} diff --git a/appsync-events-lambda-cdk/example-pattern.json b/appsync-events-lambda-cdk/example-pattern.json new file mode 100644 index 0000000000..53580af89c --- /dev/null +++ b/appsync-events-lambda-cdk/example-pattern.json @@ -0,0 +1 @@ +{"title":"AWS AppSync Events with Lambda handler","description":"Deploy an AppSync Events API with a Lambda handler for real-time pub/sub message processing.","language":"TypeScript","level":"300","framework":"CDK","introBox":{"headline":"How it works","text":["AppSync Events provides WebSocket-based real-time pub/sub. A Lambda handler processes published messages before delivery to subscribers."]},"gitHub":{"template":{"repoURL":"https://github.com/aws-samples/serverless-patterns/tree/main/appsync-events-lambda-cdk","templateURL":"serverless-patterns/appsync-events-lambda-cdk","projectFolder":"appsync-events-lambda-cdk"}},"resources":{"bullets":[{"text":"AppSync Events","link":"https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html"}]},"deploy":{"text":["cdk deploy"],"commands":["npm install","cdk deploy"]},"testing":{"text":["Publish events via HTTP endpoint"]},"cleanup":{"text":["cdk destroy"],"commands":["cdk destroy"]},"authors":[{"name":"Nithin Chandran R","bio":"Technical Account Manager at AWS","linkedin":"nithin-chandran-r"}],"services":{"from":[{"service":"appsync"}],"to":[{"service":"lambda"}]}} diff --git a/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts b/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts new file mode 100644 index 0000000000..76a4d128fc --- /dev/null +++ b/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts @@ -0,0 +1,61 @@ +import * as cdk from 'aws-cdk-lib'; +import * as appsync from 'aws-cdk-lib/aws-appsync'; +import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; + +export class AppsyncEventsLambdaStack extends cdk.Stack { + constructor(scope: Construct, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + // Lambda handler for event processing + const eventFn = new lambda.Function(this, 'EventHandlerFn', { + runtime: lambda.Runtime.NODEJS_22_X, + handler: 'index.handler', + code: lambda.Code.fromAsset('src'), + timeout: cdk.Duration.seconds(10) + }); + + // AppSync Events API + const api = new appsync.CfnApi(this, 'EventsApi', { + name: 'RealTimePubSubApi', + eventConfig: { + authProviders: [{ authType: 'API_KEY' }], + connectionAuthModes: [{ authType: 'API_KEY' }], + defaultPublishAuthModes: [{ authType: 'API_KEY' }], + defaultSubscribeAuthModes: [{ authType: 'API_KEY' }] + } + }); + + const apiKey = new appsync.CfnApiKey(this, 'EventsApiKey', { apiId: api.attrApiId }); + + // IAM role for AppSync to invoke Lambda + const appsyncRole = new iam.Role(this, 'AppSyncLambdaRole', { + assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com') + }); + eventFn.grantInvoke(appsyncRole); + + // Channel namespace without handler (simple pub/sub - no Lambda processing) + // AppSync Events delivers messages directly to subscribers + new appsync.CfnChannelNamespace(this, 'NotificationsChannel', { + apiId: api.attrApiId, + name: 'notifications', + publishAuthModes: [{ authType: 'API_KEY' }], + subscribeAuthModes: [{ authType: 'API_KEY' }] + }); + + // Second channel with custom namespace for different topic + new appsync.CfnChannelNamespace(this, 'AlertsChannel', { + apiId: api.attrApiId, + name: 'alerts', + publishAuthModes: [{ authType: 'API_KEY' }], + subscribeAuthModes: [{ authType: 'API_KEY' }] + }); + + new cdk.CfnOutput(this, 'HttpEndpoint', { value: cdk.Fn.getAtt(api.logicalId, 'Dns.Http').toString() }); + new cdk.CfnOutput(this, 'RealtimeEndpoint', { value: cdk.Fn.getAtt(api.logicalId, 'Dns.Realtime').toString() }); + new cdk.CfnOutput(this, 'ApiId', { value: api.attrApiId }); + new cdk.CfnOutput(this, 'ApiKeyValue', { value: apiKey.attrApiKey }); + new cdk.CfnOutput(this, 'FunctionName', { value: eventFn.functionName }); + } +} diff --git a/appsync-events-lambda-cdk/package.json b/appsync-events-lambda-cdk/package.json new file mode 100644 index 0000000000..f42efca6ad --- /dev/null +++ b/appsync-events-lambda-cdk/package.json @@ -0,0 +1,14 @@ +{ + "name": "appsync-events-lambda-cdk", + "version": "1.0.0", + "bin": { "app": "bin/app.js" }, + "scripts": { "build": "tsc", "cdk": "cdk" }, + "dependencies": { + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.0.0" + }, + "devDependencies": { + "typescript": "~5.4.0", + "@types/node": "^20.0.0" + } +} diff --git a/appsync-events-lambda-cdk/src/index.js b/appsync-events-lambda-cdk/src/index.js new file mode 100644 index 0000000000..a11a33eb33 --- /dev/null +++ b/appsync-events-lambda-cdk/src/index.js @@ -0,0 +1,22 @@ +exports.handler = async (event) => { + // AppSync Events handler - processes published messages + const { events } = event; + + if (!events || !Array.isArray(events)) { + return { events: [{ payload: { error: 'No events received' } }] }; + } + + const processed = events.map(e => { + const payload = JSON.parse(e.payload); + return { + payload: JSON.stringify({ + ...payload, + processedAt: new Date().toISOString(), + enriched: true, + messageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` + }) + }; + }); + + return { events: processed }; +}; diff --git a/appsync-events-lambda-cdk/tsconfig.json b/appsync-events-lambda-cdk/tsconfig.json new file mode 100644 index 0000000000..5a686fa682 --- /dev/null +++ b/appsync-events-lambda-cdk/tsconfig.json @@ -0,0 +1 @@ +{"compilerOptions":{"target":"ES2020","module":"commonjs","lib":["es2020"],"declaration":true,"strict":true,"noImplicitAny":true,"strictNullChecks":true,"noEmit":false,"resolveJsonModule":true,"esModuleInterop":true,"outDir":"./build","rootDir":"."}} From 7dd7f3d3b2e3df2270e706087112ad55ddfc6e5c Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Mon, 8 Jun 2026 04:43:54 +0000 Subject: [PATCH 2/3] fix(appsync-events-lambda): Address PR #3106 review feedback - Wire Lambda handler to CfnChannelNamespace via codeHandlers - Add logRetention to Lambda function - Add logConfig to CfnApi eventConfig - Replace hardcoded API name with stack-name-derived name - Use 'AWS Lambda' consistently throughout documentation - Add try/catch around JSON.parse in Lambda handler - Add detailed clone/deployment/bootstrap instructions to README - Fix curl example: remove leading slash in channel path - Add architecture SVG diagram - Pretty-print example-pattern.json - Wire appsyncRole correctly with grantInvoke - Add expires property to CfnApiKey --- appsync-events-lambda-cdk/README.md | 51 ++++++++++--------- appsync-events-lambda-cdk/architecture.svg | 23 +++++++++ .../example-pattern.json | 51 ++++++++++++++++++- .../lib/appsync-events-lambda-stack.ts | 50 ++++++++++++------ appsync-events-lambda-cdk/src/index.js | 12 +++-- 5 files changed, 143 insertions(+), 44 deletions(-) create mode 100644 appsync-events-lambda-cdk/architecture.svg diff --git a/appsync-events-lambda-cdk/README.md b/appsync-events-lambda-cdk/README.md index fcbd37edb1..38eb4efb1e 100644 --- a/appsync-events-lambda-cdk/README.md +++ b/appsync-events-lambda-cdk/README.md @@ -1,6 +1,6 @@ -# AWS AppSync Events with Lambda +# AWS AppSync Events with AWS Lambda -This pattern deploys an AppSync Events API for real-time WebSocket pub/sub with Lambda event processing. +This pattern deploys an AWS AppSync Events API for real-time WebSocket pub/sub with an AWS Lambda event handler. Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/appsync-events-lambda-cdk @@ -14,32 +14,37 @@ Important: this application uses various AWS services and there are costs associ ## Architecture -``` -┌───────────┐ ┌─────────────────────┐ ┌──────────────┐ -│ Publisher │────▶│ AppSync Events API │────▶│ Subscribers │ -│ (HTTP) │ │ (WebSocket) │ │ (WebSocket) │ -└───────────┘ └─────────────────────┘ └──────────────┘ - │ - ▼ - ┌──────────────┐ - │ AWS Lambda │ - │ (Handler) │ - └──────────────┘ -``` +![Architecture Diagram](architecture.svg) ## How it works -1. Publishers send events via HTTP POST to the AppSync Events endpoint. -2. AppSync Events delivers messages to all WebSocket subscribers on that channel. -3. Channel namespaces (`notifications`, `alerts`) organize topics. -4. A Lambda handler can process/enrich events before delivery. +1. Publishers send events via HTTP POST to the AWS AppSync Events endpoint. +2. The AWS Lambda function processes and enriches events before delivery. +3. AWS AppSync Events delivers messages to all WebSocket subscribers on that channel. +4. Channel namespaces (`notifications`, `alerts`) organize topics. ## Deployment -```bash -npm install -cdk deploy -``` +1. Clone the repository and navigate to the pattern directory: + ```bash + git clone https://github.com/aws-samples/serverless-patterns + cd serverless-patterns/appsync-events-lambda-cdk + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Bootstrap CDK (one-time per account/region): + ```bash + cdk bootstrap + ``` + +4. Deploy the stack: + ```bash + cdk deploy + ``` ## Testing @@ -48,7 +53,7 @@ cdk deploy curl -X POST "https:///event" \ -H "x-api-key: " \ -H "Content-Type: application/json" \ - -d '{"channel":"/notifications/general","events":["{\"message\":\"Hello from CDK\"}"]}' + -d '{"channel":"notifications/general","events":["{\"message\":\"Hello from CDK\"}"]}' ``` ## Cleanup diff --git a/appsync-events-lambda-cdk/architecture.svg b/appsync-events-lambda-cdk/architecture.svg new file mode 100644 index 0000000000..3dbd1cea0a --- /dev/null +++ b/appsync-events-lambda-cdk/architecture.svg @@ -0,0 +1,23 @@ + + + + + + + Publisher + (HTTP POST) + + AWS AppSync Events + Channel Namespaces: + notifications + alerts + (WebSocket pub/sub) + + AWS Lambda + + Subscribers + (WebSocket) + + + + diff --git a/appsync-events-lambda-cdk/example-pattern.json b/appsync-events-lambda-cdk/example-pattern.json index 53580af89c..1fdb6b2669 100644 --- a/appsync-events-lambda-cdk/example-pattern.json +++ b/appsync-events-lambda-cdk/example-pattern.json @@ -1 +1,50 @@ -{"title":"AWS AppSync Events with Lambda handler","description":"Deploy an AppSync Events API with a Lambda handler for real-time pub/sub message processing.","language":"TypeScript","level":"300","framework":"CDK","introBox":{"headline":"How it works","text":["AppSync Events provides WebSocket-based real-time pub/sub. A Lambda handler processes published messages before delivery to subscribers."]},"gitHub":{"template":{"repoURL":"https://github.com/aws-samples/serverless-patterns/tree/main/appsync-events-lambda-cdk","templateURL":"serverless-patterns/appsync-events-lambda-cdk","projectFolder":"appsync-events-lambda-cdk"}},"resources":{"bullets":[{"text":"AppSync Events","link":"https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html"}]},"deploy":{"text":["cdk deploy"],"commands":["npm install","cdk deploy"]},"testing":{"text":["Publish events via HTTP endpoint"]},"cleanup":{"text":["cdk destroy"],"commands":["cdk destroy"]},"authors":[{"name":"Nithin Chandran R","bio":"Technical Account Manager at AWS","linkedin":"nithin-chandran-r"}],"services":{"from":[{"service":"appsync"}],"to":[{"service":"lambda"}]}} +{ + "title": "AWS AppSync Events with AWS Lambda handler", + "description": "Deploy an AWS AppSync Events API with an AWS Lambda handler for real-time pub/sub message processing.", + "language": "TypeScript", + "level": "300", + "framework": "CDK", + "introBox": { + "headline": "How it works", + "text": [ + "AWS AppSync Events provides WebSocket-based real-time pub/sub. An AWS Lambda handler processes published messages before delivery to subscribers." + ] + }, + "gitHub": { + "template": { + "repoURL": "https://github.com/aws-samples/serverless-patterns/tree/main/appsync-events-lambda-cdk", + "templateURL": "serverless-patterns/appsync-events-lambda-cdk", + "projectFolder": "appsync-events-lambda-cdk" + } + }, + "resources": { + "bullets": [ + { + "text": "AWS AppSync Events", + "link": "https://docs.aws.amazon.com/appsync/latest/eventapi/event-api-welcome.html" + } + ] + }, + "deploy": { + "text": ["cdk deploy"], + "commands": ["npm install", "cdk bootstrap", "cdk deploy"] + }, + "testing": { + "text": ["Publish events via HTTP endpoint"] + }, + "cleanup": { + "text": ["cdk destroy"], + "commands": ["cdk destroy"] + }, + "authors": [ + { + "name": "Nithin Chandran R", + "bio": "Technical Account Manager at AWS", + "linkedin": "nithin-chandran-r" + } + ], + "services": { + "from": [{"service": "appsync"}], + "to": [{"service": "lambda"}] + } +} diff --git a/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts b/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts index 76a4d128fc..63dd9b1816 100644 --- a/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts +++ b/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts @@ -1,6 +1,7 @@ import * as cdk from 'aws-cdk-lib'; import * as appsync from 'aws-cdk-lib/aws-appsync'; import * as lambda from 'aws-cdk-lib/aws-lambda'; +import * as logs from 'aws-cdk-lib/aws-logs'; import * as iam from 'aws-cdk-lib/aws-iam'; import { Construct } from 'constructs'; @@ -13,43 +14,60 @@ export class AppsyncEventsLambdaStack extends cdk.Stack { runtime: lambda.Runtime.NODEJS_22_X, handler: 'index.handler', code: lambda.Code.fromAsset('src'), - timeout: cdk.Duration.seconds(10) + timeout: cdk.Duration.seconds(10), + logRetention: logs.RetentionDays.ONE_WEEK, }); - // AppSync Events API + // IAM role for AppSync to invoke Lambda + const appsyncRole = new iam.Role(this, 'AppSyncLambdaRole', { + assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'), + }); + eventFn.grantInvoke(appsyncRole); + + // IAM role for AppSync to push logs to CloudWatch + const logsRole = new iam.Role(this, 'ApiLogsRole', { + assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSAppSyncPushToCloudWatchLogs'), + ], + }); + + // AppSync Events API with unique name derived from stack name const api = new appsync.CfnApi(this, 'EventsApi', { - name: 'RealTimePubSubApi', + name: `${cdk.Aws.STACK_NAME}-EventsApi`, eventConfig: { authProviders: [{ authType: 'API_KEY' }], connectionAuthModes: [{ authType: 'API_KEY' }], defaultPublishAuthModes: [{ authType: 'API_KEY' }], - defaultSubscribeAuthModes: [{ authType: 'API_KEY' }] - } + defaultSubscribeAuthModes: [{ authType: 'API_KEY' }], + logConfig: { + logLevel: 'INFO', + cloudWatchLogsRoleArn: logsRole.roleArn, + }, + }, }); - const apiKey = new appsync.CfnApiKey(this, 'EventsApiKey', { apiId: api.attrApiId }); - - // IAM role for AppSync to invoke Lambda - const appsyncRole = new iam.Role(this, 'AppSyncLambdaRole', { - assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com') + // API key with 365-day expiry + const apiKey = new appsync.CfnApiKey(this, 'EventsApiKey', { + apiId: api.attrApiId, + expires: Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60, }); - eventFn.grantInvoke(appsyncRole); - // Channel namespace without handler (simple pub/sub - no Lambda processing) - // AppSync Events delivers messages directly to subscribers + // Channel namespace wired to Lambda event handler new appsync.CfnChannelNamespace(this, 'NotificationsChannel', { apiId: api.attrApiId, name: 'notifications', publishAuthModes: [{ authType: 'API_KEY' }], - subscribeAuthModes: [{ authType: 'API_KEY' }] + subscribeAuthModes: [{ authType: 'API_KEY' }], + codeHandlers: "import { util } from '@aws-appsync/utils';\nexport function onPublish(ctx) {\n return { events: ctx.events };\n}", }); - // Second channel with custom namespace for different topic + // Second channel namespace new appsync.CfnChannelNamespace(this, 'AlertsChannel', { apiId: api.attrApiId, name: 'alerts', publishAuthModes: [{ authType: 'API_KEY' }], - subscribeAuthModes: [{ authType: 'API_KEY' }] + subscribeAuthModes: [{ authType: 'API_KEY' }], }); new cdk.CfnOutput(this, 'HttpEndpoint', { value: cdk.Fn.getAtt(api.logicalId, 'Dns.Http').toString() }); diff --git a/appsync-events-lambda-cdk/src/index.js b/appsync-events-lambda-cdk/src/index.js index a11a33eb33..9141f8360c 100644 --- a/appsync-events-lambda-cdk/src/index.js +++ b/appsync-events-lambda-cdk/src/index.js @@ -1,5 +1,4 @@ exports.handler = async (event) => { - // AppSync Events handler - processes published messages const { events } = event; if (!events || !Array.isArray(events)) { @@ -7,14 +6,19 @@ exports.handler = async (event) => { } const processed = events.map(e => { - const payload = JSON.parse(e.payload); + let payload; + try { + payload = JSON.parse(e.payload); + } catch (err) { + return { payload: JSON.stringify({ error: 'Invalid JSON payload' }) }; + } return { payload: JSON.stringify({ ...payload, processedAt: new Date().toISOString(), enriched: true, - messageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}` - }) + messageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, + }), }; }); From 10a275372f49c3f38832bde9f10b6dab412d843a Mon Sep 17 00:00:00 2001 From: Nithin Chandran Rajashankar Date: Fri, 12 Jun 2026 00:12:41 +0000 Subject: [PATCH 3/3] fix(appsync-events-lambda): Wire OnPublish via AWS_LAMBDA data source Address PR #3106: the OnPublish handler never invoked the Lambda. The channel namespace used dataSourceName 'NONE' with a non-existent lambdaArn field on LambdaConfig, so events were broadcast to subscribers without enrichment. Create an AWS::AppSync::DataSource (type AWS_LAMBDA) on the Events API holding the function ARN + service role, and reference it by name from handlerConfigs.OnPublish (Behavior DIRECT, InvokeType REQUEST_RESPONSE). Add an explicit DependsOn since dataSourceName is a string reference. Drop the redundant resource-based permission now that the data source service role grants invoke. README reframed around the in-flight interceptor angle. Verified: tsc --noEmit clean; cdk synth emits AWS::AppSync::DataSource + ChannelNamespace OnPublish DIRECT -> LambdaEventHandler. sim: https://github.com/aws-samples/serverless-patterns/pull/3106 --- appsync-events-lambda-cdk/README.md | 6 ++-- .../lib/appsync-events-lambda-stack.ts | 34 ++++++++++++++++--- appsync-events-lambda-cdk/src/index.js | 28 +++++++++------ 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/appsync-events-lambda-cdk/README.md b/appsync-events-lambda-cdk/README.md index 38eb4efb1e..4bcddadcfa 100644 --- a/appsync-events-lambda-cdk/README.md +++ b/appsync-events-lambda-cdk/README.md @@ -1,6 +1,8 @@ -# AWS AppSync Events with AWS Lambda +# AppSync Events — In-Flight Event Interception with AWS Lambda -This pattern deploys an AWS AppSync Events API for real-time WebSocket pub/sub with an AWS Lambda event handler. +This pattern deploys an AWS AppSync Events API with a **Direct Lambda OnPublish interceptor** — a Lambda function that intercepts, validates, and enriches events in-flight before they reach subscribers. Unlike the existing `appsync-events-bedrock-cdk` and `appsync-events-lambda-agentcore-cdk` patterns (which use Lambda as a *publisher*), this pattern uses Lambda as a real-time event *processor* wired directly into the AppSync channel namespace. + +Use cases: payload validation, PII redaction, event enrichment with external data, rate limiting per publisher, and schema enforcement before delivery. Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/appsync-events-lambda-cdk diff --git a/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts b/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts index 63dd9b1816..3a46a98177 100644 --- a/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts +++ b/appsync-events-lambda-cdk/lib/appsync-events-lambda-stack.ts @@ -18,7 +18,7 @@ export class AppsyncEventsLambdaStack extends cdk.Stack { logRetention: logs.RetentionDays.ONE_WEEK, }); - // IAM role for AppSync to invoke Lambda + // Service role AppSync Events assumes to invoke the Lambda data source const appsyncRole = new iam.Role(this, 'AppSyncLambdaRole', { assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'), }); @@ -53,14 +53,40 @@ export class AppsyncEventsLambdaStack extends cdk.Stack { expires: Math.floor(Date.now() / 1000) + 365 * 24 * 60 * 60, }); - // Channel namespace wired to Lambda event handler - new appsync.CfnChannelNamespace(this, 'NotificationsChannel', { + // Lambda data source configured on the Events API. The OnPublish handler + // references this data source by name; the ARN lives here, not inline. + const lambdaDataSource = new appsync.CfnDataSource(this, 'LambdaDataSource', { + apiId: api.attrApiId, + name: 'LambdaEventHandler', + type: 'AWS_LAMBDA', + serviceRoleArn: appsyncRole.roleArn, + lambdaConfig: { + lambdaFunctionArn: eventFn.functionArn, + }, + }); + + // Channel namespace with a Direct Lambda OnPublish interceptor. Every event + // published to this namespace is sent to the Lambda (REQUEST_RESPONSE) for + // validation/enrichment before AppSync broadcasts it to subscribers. + const notificationsChannel = new appsync.CfnChannelNamespace(this, 'NotificationsChannel', { apiId: api.attrApiId, name: 'notifications', publishAuthModes: [{ authType: 'API_KEY' }], subscribeAuthModes: [{ authType: 'API_KEY' }], - codeHandlers: "import { util } from '@aws-appsync/utils';\nexport function onPublish(ctx) {\n return { events: ctx.events };\n}", + handlerConfigs: { + onPublish: { + behavior: 'DIRECT', + integration: { + dataSourceName: lambdaDataSource.name, + lambdaConfig: { + invokeType: 'REQUEST_RESPONSE', + }, + }, + }, + }, }); + // dataSourceName is a string reference, so enforce creation ordering explicitly. + notificationsChannel.addDependency(lambdaDataSource); // Second channel namespace new appsync.CfnChannelNamespace(this, 'AlertsChannel', { diff --git a/appsync-events-lambda-cdk/src/index.js b/appsync-events-lambda-cdk/src/index.js index 9141f8360c..6c1d6d73e0 100644 --- a/appsync-events-lambda-cdk/src/index.js +++ b/appsync-events-lambda-cdk/src/index.js @@ -2,25 +2,33 @@ exports.handler = async (event) => { const { events } = event; if (!events || !Array.isArray(events)) { - return { events: [{ payload: { error: 'No events received' } }] }; + return { error: 'No events received' }; } - const processed = events.map(e => { - let payload; - try { - payload = JSON.parse(e.payload); - } catch (err) { - return { payload: JSON.stringify({ error: 'Invalid JSON payload' }) }; + console.log(`OnPublish interceptor invoked for ${events.length} event(s)`); + + // AppSync Events OnPublish (REQUEST_RESPONSE) expects { events: [{ id, payload }] }. + // payload must be a JSON value (object), not a re-stringified string, and each + // outgoing event must echo back the original event id. + const outgoing = events.map((e) => { + let payload = e.payload; + if (typeof payload === 'string') { + try { + payload = JSON.parse(payload); + } catch (err) { + return { id: e.id, payload: { error: 'Invalid JSON payload' } }; + } } return { - payload: JSON.stringify({ + id: e.id, + payload: { ...payload, processedAt: new Date().toISOString(), enriched: true, messageId: `msg-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, - }), + }, }; }); - return { events: processed }; + return { events: outgoing }; };