UNPKG

cdk-serverless-agentic-api

Version:

CDK construct for serverless web applications with CloudFront, S3, Cognito, API Gateway, and Lambda

770 lines (603 loc) 24.8 kB
# CDK Serverless Agentic API A CDK construct that simplifies the creation of serverless web applications on AWS by providing a comprehensive solution that integrates CloudFront, S3, Cognito, API Gateway, and Lambda functions. [![npm version](https://badge.fury.io/js/cdk-serverless-agentic-api.svg)](https://badge.fury.io/js/cdk-serverless-agentic-api) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Build and Publish](https://github.com/rhyslewisakl/cdk-serverless-agentic-api/actions/workflows/publish.yml/badge.svg)](https://github.com/rhyslewisakl/cdk-serverless-agentic-api/actions/workflows/publish.yml) [![constructs.dev](https://img.shields.io/badge/constructs.dev-published-blue)](https://constructs.dev/packages/cdk-serverless-agentic-api) ## Overview The CDK Serverless Agentic API is a high-level CDK construct that helps you deploy a complete serverless web application infrastructure on AWS with minimal configuration. It combines several AWS services into a cohesive architecture: > **Important Note for TypeScript Users**: If you encounter TypeScript errors related to incompatible `Construct` types, you may need to ensure your project uses the same version of the `constructs` package as this library. Add `"skipLibCheck": true` to your `tsconfig.json` file's `compilerOptions` to resolve these issues. - **CloudFront** for global content delivery with SSL/TLS - **S3** for static website hosting - **Cognito** for user authentication and management - **API Gateway** for RESTful API endpoints - **Lambda** for serverless backend logic This construct follows AWS best practices for security, performance, and cost optimization. ## Architecture ``` ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ │ │ │ │ │ │ CloudFront │────▶│ S3 Bucket │ │ Cognito │ │ Distribution │ │ (Static Site) │ │ User Pool │ │ │ │ │ │ │ └───────┬───────┘ └───────────────┘ └───────┬───────┘ │ │ │ │ ▼ │ ┌───────────────┐ │ │ │ │ │ API Gateway │◀──────────────────────────────────┘ │ REST API │ Authorizes │ │ └───────┬───────┘ │ │ ▼ ┌───────────────┐ │ │ │ Lambda │ │ Functions │ │ │ └───────────────┘ ``` ## Requirements - **Node.js**: 22.0.0 or higher (LTS recommended) - **npm**: 8.0.0 or higher - **AWS CDK**: 2.170.0 or higher ## Quick Start ```typescript import { CDKServerlessAgenticAPI } from 'cdk-serverless-agentic-api'; import { Stack } from 'aws-cdk-lib'; export class MyStack extends Stack { constructor(scope, id, props) { super(scope, id, props); // Create the serverless API with default settings const api = new CDKServerlessAgenticAPI(this, 'MyApi'); // Add an endpoint api.addResource({ path: '/hello', lambdaSourcePath: './lambda/hello', requiresAuth: false }); } } ``` ## Installation ### TypeScript / JavaScript ```bash cdk init app --language typescript # npm npm install cdk-serverless-agentic-api ``` ## Project Structure ``` ├── src/ │ ├── index.ts # Main export file │ ├── cdk-serverless-agentic-api.ts # Core construct implementation │ ├── types.ts # TypeScript interfaces and types │ ├── error-handling.ts # Error handling utilities │ └── security-validation.ts # Security validation utilities ├── test/ │ ├── cdk-serverless-agentic-api.test.ts # Unit tests │ ├── integration/ # Integration tests │ └── lambda/ # Lambda function tests ├── lambda/ │ ├── health/ # Default health check endpoint │ └── whoami/ # Default authentication endpoint ├── lib/ # Compiled JavaScript output (generated) ├── package.json # Project dependencies and scripts ├── tsconfig.json # TypeScript configuration ├── vitest.config.ts # Test configuration ├── .eslintrc.js # ESLint configuration └── README.md # This file ``` ## Development ### Prerequisites - Node.js 22+ - npm or yarn - AWS CDK v2 ### Installation ```bash npm install ``` ### Building ```bash npm run build ``` ### Testing ```bash # Run tests once npm test # Run tests in watch mode npm run test:watch # Run integration tests npm run test:integration ``` ### Linting ```bash # Check for linting issues npm run lint # Fix linting issues automatically npm run lint:fix ``` ## Usage ### Basic Usage ```typescript import { CDKServerlessAgenticAPI } from 'cdk-serverless-agentic-api'; import { Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; export class MyStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); const webApp = new CDKServerlessAgenticAPI(this, 'MyWebApp'); // Add API resources webApp.addResource({ path: '/users', lambdaSourcePath: './lambda/users', requiresAuth: true }); } } ``` ### Custom Domain Configuration ```typescript const webApp = new CDKServerlessAgenticAPI(this, 'MyWebApp', { domainName: 'example.com', certificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012' }); ``` ### Adding API Resources ```typescript // Public endpoint (no authentication required) webApp.addResource({ path: '/products', lambdaSourcePath: './lambda/products', requiresAuth: false }); // Authenticated endpoint webApp.addResource({ path: '/users', lambdaSourcePath: './lambda/users', requiresAuth: true }); // Specific HTTP method webApp.addResource({ path: '/orders', method: 'POST', lambdaSourcePath: './lambda/create-order', requiresAuth: true }); // With environment variables webApp.addResource({ path: '/payments', lambdaSourcePath: './lambda/payments', requiresAuth: true, environment: { STRIPE_API_KEY: process.env.STRIPE_API_KEY || '', PAYMENT_MODE: 'test' } }); // With Dead Letter Queue enabled webApp.addResource({ path: '/critical', lambdaSourcePath: './lambda/critical', requiresAuth: true, enableDLQ: true // Enable DLQ for failed invocations }); // With health alarms enabled webApp.addResource({ path: '/monitored', lambdaSourcePath: './lambda/monitored', requiresAuth: false, enableHealthAlarms: true // Enable CloudWatch alarms }); ``` ### Accessing Lambda Functions ```typescript // Access Lambda functions using the new getLambdaFunction method const usersFunction = webApp.getLambdaFunction('/users', 'GET'); if (usersFunction) { // Grant additional permissions myDynamoTable.grantReadWriteData(usersFunction); // Add event source mappings usersFunction.addEventSource(new SqsEventSource(myQueue)); } // Use the helper method for DynamoDB permissions const createUserFunction = webApp.addResource({ path: '/users', method: 'POST', lambdaSourcePath: './lambda/create-user', requiresAuth: true }); // Grant DynamoDB access using the helper method webApp.grantDynamoDBAccess(createUserFunction, myTable, 'readwrite'); ``` ### Security Validation ```typescript // Validate security configuration const securityResults = webApp.validateSecurity(); // Check results securityResults.forEach(result => { if (!result.passed) { console.warn(`Security issue: ${result.message}`); console.warn(`Details: ${JSON.stringify(result.details)}`); } }); // Validate with custom options webApp.validateSecurity({ throwOnFailure: true, // Throw error if validation fails logResults: true // Log results to console }); ``` ## API Reference ### Key Methods ```typescript // Add an API endpoint with Lambda integration const userFunction = api.addResource({ path: '/users', method: 'GET', lambdaSourcePath: './lambda/users', requiresAuth: true }); // Get a Lambda function by path and method const getUsersFunction = api.getLambdaFunction('/users', 'GET'); // Create a standalone Lambda function const processor = api.createLambdaFunction( 'DataProcessor', './lambda/processor', { BUCKET_NAME: api.bucket?.bucketName } ); // Grant DynamoDB access to a Lambda function api.grantDynamoDBAccess(userFunction, myTable, 'readwrite'); // Get exportable resource IDs for extension stacks const resourceIds = api.getExportableResourceIds(); // Validate security configuration api.validateSecurity({ throwOnFailure: true }); // Apply security best practices api.enforceSecurityBestPractices(); ``` ### Key Properties ```typescript // Access underlying AWS resources api.bucket // S3 bucket for static files api.distribution // CloudFront distribution api.api // API Gateway REST API api.userPool // Cognito User Pool api.lambdaFunctions // Map of all Lambda functions ``` For complete API documentation, see [API_REFERENCE.md](./API_REFERENCE.md) ### CDKServerlessAgenticAPIProps Configuration properties for the CDKServerlessAgenticAPI. | Property | Type | Required | Default | Description | | ------------------ | --------- | ------------------------------ | ----------------------------- | ----------------------------------------------------------------------------------------- | | `domainName` | `string` | No | CloudFront generated domain | Custom domain name for the CloudFront distribution | | `certificateArn` | `string` | Only if domainName is provided | - | ARN of the SSL certificate for the custom domain | | `bucketName` | `string` | No | CDK generated name | Custom name for the S3 bucket | | `userPoolName` | `string` | No | CDK generated name | Custom name for the Cognito User Pool | | `apiName` | `string` | No | CDK generated name | Custom name for the API Gateway | | `enableLogging` | `boolean` | No | `true` | Enable detailed logging for all components | | `lambdaSourcePath` | `string` | No | Bundled lambda directory | Custom path to the directory containing Lambda function source code for default endpoints | | `errorPagesPath` | `string` | No | Bundled error-pages directory | Custom path to the directory containing error page HTML files | ### AddResourceOptions Options for adding a new API resource to the construct. | Property | Type | Required | Default | Description | | ------------------ | --------------------------- | -------- | ------- | ---------------------------------------------------------------- | | `path` | `string` | Yes | - | The API path for the resource (e.g., '/users', '/products') | | `method` | `string` | No | `'GET'` | HTTP method for the resource | | `lambdaSourcePath` | `string` | Yes | - | Path to the directory containing the Lambda function source code | | `requiresAuth` | `boolean` | No | `false` | Whether the resource requires authentication | | `cognitoGroup` | `string` | No | - | Cognito group required to access this resource | | `environment` | `{ [key: string]: string }` | No | - | Environment variables to pass to the Lambda function | | `enableDLQ` | `boolean` | No | `false` | Whether to enable Dead Letter Queue for the Lambda function | | `enableHealthAlarms` | `boolean` | No | `false` | Whether to enable health alarms for the Lambda function | ## Lambda Function Structure When adding resources with `addResource`, you need to provide a Lambda function source directory. The directory should have the following structure: ``` lambda/ └── my-function/ ├── index.js # Main handler file ├── package.json # Dependencies └── node_modules/ # Installed dependencies (optional) ``` The `index.js` file should export a handler function: ```javascript exports.handler = async (event, context) => { // Your Lambda function code return { statusCode: 200, headers: { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' }, body: JSON.stringify({ message: 'Hello from Lambda!' }) }; }; ``` ## Default Endpoints The construct automatically creates three default endpoints: 1. **Health Check** (`/api/health`): A public endpoint that returns a 200 OK response 2. **WhoAmI** (`/api/whoami`): An authenticated endpoint that returns the current user's Cognito claims 3. **Config** (`/api/config`): A public endpoint that provides frontend configuration information ### Using the Config Endpoint The config endpoint returns essential information needed by frontend applications to connect to the backend services: ```javascript // Example of fetching configuration from the config endpoint async function fetchConfig() { const response = await fetch('https://your-api-url.com/api/config'); const config = await response.json(); // Use the configuration to set up your frontend app // This avoids hardcoding values from CDK outputs return config; } // Example response structure { "auth": { "region": "us-east-1", "userPoolId": "us-east-1_abcdefghi", "userPoolWebClientId": "1234567890abcdefghijklmnop", "oauth": { "domain": "your-domain.auth.us-east-1.amazoncognito.com", "scope": ["email", "profile", "openid"], "redirectSignIn": "", "redirectSignOut": "", "responseType": "code" } }, "api": { "endpoints": [ { "name": "api", "endpoint": "https://api-id.execute-api.us-east-1.amazonaws.com/api", "region": "us-east-1" } ] }, "version": "1.0.0" } ``` > **Important**: Always use the config endpoint instead of hardcoding values from CDK outputs. This ensures your frontend application can adapt to changes in the backend infrastructure. ## Customizing Default Resources The construct comes with bundled Lambda functions and error pages that are used by default. However, you can customize these resources by providing your own paths. ### Custom Lambda Functions You can provide your own Lambda functions for the default endpoints (health, whoami, config) by specifying the `lambdaSourcePath` property: ```typescript const webApp = new CDKServerlessAgenticAPI(this, 'MyWebApp', { lambdaSourcePath: './my-custom-lambda' }); ``` Your custom Lambda directory should have the following structure: ``` my-custom-lambda/ ├── health/ │ └── index.js ├── whoami/ │ └── index.js └── config/ └── index.js ``` ### Custom Error Pages You can provide your own error pages by specifying the `errorPagesPath` property: ```typescript const webApp = new CDKServerlessAgenticAPI(this, 'MyWebApp', { errorPagesPath: './my-custom-error-pages' }); ``` Your custom error pages directory should have the following structure: ``` my-custom-error-pages/ ├── 400.html ├── 403.html ├── 404.html └── 500.html ``` > **Note**: If you don't provide custom paths, the construct will use the bundled Lambda functions and error pages that come with the package. ## Extension Mode (Multi-Stack Support) For large applications that exceed the 500 resource limit per stack, you can use extension mode to split resources across multiple stacks while sharing core infrastructure. ### Main Stack ```typescript // Create the main stack with core infrastructure const mainStack = new CDKServerlessAgenticAPI(this, 'MainAPI', { domainName: 'example.com', certificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012' }); // Add some API resources mainStack.addResource({ path: '/users', lambdaSourcePath: './lambda/users', requiresAuth: true }); // Export resource IDs for use in extension stacks const resourceIds = mainStack.getExportableResourceIds(); ``` ### Extension Stack ```typescript // Create an extension stack that reuses the main stack's infrastructure const extensionStack = new CDKServerlessAgenticAPI(this, 'ExtensionAPI', { extensionMode: { apiId: resourceIds.apiId, userPoolId: resourceIds.userPoolId, userPoolClientId: resourceIds.userPoolClientId, cognitoAuthorizerId: resourceIds.cognitoAuthorizerId }, skipResources: { skipBucket: true, // Don't create S3 bucket skipDistribution: true, // Don't create CloudFront distribution skipDefaultEndpoints: true, // Don't create default endpoints skipLoggingBucket: true // Don't create logging bucket } }); // Add additional API resources to the extension stack extensionStack.addResource({ path: '/products', lambdaSourcePath: './lambda/products', requiresAuth: true }); ``` ## Security Best Practices The construct implements several security best practices: - **HTTPS Enforcement**: All traffic is encrypted in transit - **S3 Bucket Security**: Public access blocking, encryption at rest - **IAM Least Privilege**: Minimal permissions for Lambda functions - **Cognito Authentication**: JWT-based authentication for API endpoints - **CORS Configuration**: Secure cross-origin resource sharing - **Security Headers**: HTTP security headers for CloudFront ## Examples ### Complete Example with Custom Domain ```typescript import { CDKServerlessAgenticAPI } from 'cdk-serverless-agentic-api'; import { Stack, StackProps, App } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as dynamodb from 'aws-cdk-lib/aws-dynamodb'; class MyWebAppStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // Create DynamoDB table for the application const table = new dynamodb.Table(this, 'UsersTable', { partitionKey: { name: 'userId', type: dynamodb.AttributeType.STRING }, billingMode: dynamodb.BillingMode.PAY_PER_REQUEST }); // Create the serverless web app construct const webApp = new CDKServerlessAgenticAPI(this, 'MyWebApp', { domainName: 'example.com', certificateArn: 'arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012', enableLogging: true }); // Add API resources const getUsersFunction = webApp.addResource({ path: '/users', method: 'GET', lambdaSourcePath: './lambda/get-users', requiresAuth: true, environment: { TABLE_NAME: table.tableName } }); const createUserFunction = webApp.addResource({ path: '/users', method: 'POST', lambdaSourcePath: './lambda/create-user', requiresAuth: true, environment: { TABLE_NAME: table.tableName } }); // Grant permissions to Lambda functions table.grantReadData(getUsersFunction); table.grantWriteData(createUserFunction); } } const app = new App(); new MyWebAppStack(app, 'MyWebAppStack'); app.synth(); ``` ### Example with Security Validation ```typescript import { CDKServerlessAgenticAPI } from 'cdk-serverless-agentic-api'; import { Stack, StackProps, App } from 'aws-cdk-lib'; import { Construct } from 'constructs'; class SecureWebAppStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // Create the serverless web app construct const webApp = new CDKServerlessAgenticAPI(this, 'SecureWebApp'); // Add API resources webApp.addResource({ path: '/secure-data', lambdaSourcePath: './lambda/secure-data', requiresAuth: true }); // Validate security configuration const securityResults = webApp.validateSecurity({ throwOnFailure: true, // Throw error if validation fails logResults: true // Log results to console }); // Output security validation results new cdk.CfnOutput(this, 'SecurityValidationResults', { value: JSON.stringify(securityResults.map(r => ({ component: r.message, passed: r.passed }))) }); } } const app = new App(); new SecureWebAppStack(app, 'SecureWebAppStack'); app.synth(); ``` ## Deployment Guide ### Prerequisites 1. Install the AWS CLI and configure your credentials: ```bash aws configure ``` 2. Install the AWS CDK: ```bash npm install -g aws-cdk ``` 3. Bootstrap your AWS environment (if not already done): ```bash cdk bootstrap aws://ACCOUNT-NUMBER/REGION ``` ### Deployment Steps 1. Create a new CDK project: ```bash mkdir my-serverless-app cd my-serverless-app cdk init app --language typescript ``` 2. Install the cdk-serverless-agentic-api: ```bash npm install cdk-serverless-agentic-api ``` 3. Update your stack code to use the construct (see examples above) 4. Deploy the stack: ```bash cdk deploy ``` 5. After deployment, the CDK will output the CloudFront domain name and other important information. ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## Troubleshooting ### TypeScript Errors with Constructs If you encounter TypeScript errors like this: ``` TS2345: Argument of type 'this' is not assignable to parameter of type 'Construct'. Type 'YourStack' is not assignable to type 'Construct'. Types of property 'node' are incompatible. ``` This is due to a version mismatch between the `constructs` package in your project and the one used by this library. To fix this, you can: 1. **Use version 0.3.15 or later of this library**, which has a more flexible type definition that avoids this issue. 2. Add `"skipLibCheck": true` to your `tsconfig.json` file's `compilerOptions`: ```json { "compilerOptions": { // other options... "skipLibCheck": true } } ``` 3. Ensure you're using a compatible version of the `constructs` package: ```bash npm install constructs@^10.0.0 ``` 4. If you're using AWS CDK v2, make sure you're using the correct imports: ```typescript import { Construct } from 'constructs'; import * as cdk from 'aws-cdk-lib'; import { CDKServerlessAgenticAPI } from 'cdk-serverless-agentic-api'; export class MyStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const api = new CDKServerlessAgenticAPI(this, 'AgenticAPI'); } } ``` ## License This project is licensed under the MIT License - see the LICENSE file for details.