diff --git a/appsync-events-lambda-cdk/README.md b/appsync-events-lambda-cdk/README.md new file mode 100644 index 000000000..fcbd37edb --- /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 000000000..ac84652fe --- /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 000000000..bebf36838 --- /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 000000000..53580af89 --- /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 000000000..76a4d128f --- /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 000000000..f42efca6a --- /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 000000000..a11a33eb3 --- /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 000000000..5a686fa68 --- /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":"."}}