cdk-serverless-agentic-api
Version:
CDK construct for serverless web applications with CloudFront, S3, Cognito, API Gateway, and Lambda
340 lines • 14.9 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.CDKServerlessAgenticAPI = void 0;
const constructs_1 = require("constructs");
const s3 = __importStar(require("aws-cdk-lib/aws-s3"));
const cognito = __importStar(require("aws-cdk-lib/aws-cognito"));
const apigateway = __importStar(require("aws-cdk-lib/aws-apigateway"));
const iam = __importStar(require("aws-cdk-lib/aws-iam"));
const security_validation_1 = require("./security-validation");
const s3_1 = require("./s3");
const cognito_1 = require("./cognito");
const api_gateway_1 = require("./api-gateway");
const lambda_1 = require("./lambda");
const cloudfront_1 = require("./cloudfront");
const monitoring_1 = require("./monitoring");
const path = __importStar(require("path"));
/**
* CDK construct that creates a complete serverless web application infrastructure
* including CloudFront, S3, Cognito, API Gateway, and Lambda functions.
*/
class CDKServerlessAgenticAPI extends constructs_1.Construct {
/**
* Gets the Cognito User Pool Client for API Gateway integration
*/
get userPoolClient() {
return this._userPoolClient;
}
/**
* Creates a new CDKServerlessAgenticAPI
*
* @param scope The parent construct
* @param id The construct ID
* @param props Configuration properties
*/
constructor(scope, id, props) {
super(scope, id);
// Initialize internal state
this.lambdaFunctions = {};
this.resourceConfigs = {};
// Store props in node context for use in other methods
this.node.setContext('props', props);
// Validate props
if (props?.domainName && !props?.certificateArn) {
throw new Error('certificateArn is required when domainName is provided');
}
// Extension mode - use existing resources or create new ones
if (props?.extensionMode?.apiId) {
this.api = apigateway.RestApi.fromRestApiId(this, 'ImportedApi', props.extensionMode.apiId);
}
else {
this.api = (0, api_gateway_1.createApiGateway)(this, id, props);
}
if (props?.extensionMode?.userPoolId) {
this.userPool = cognito.UserPool.fromUserPoolId(this, 'ImportedUserPool', props.extensionMode.userPoolId);
this._userPoolClient = cognito.UserPoolClient.fromUserPoolClientId(this, 'ImportedUserPoolClient', props.extensionMode.userPoolClientId);
}
else {
const cognitoResources = (0, cognito_1.createUserPool)(this, id, props);
this.userPool = cognitoResources.userPool;
this._userPoolClient = cognitoResources.userPoolClient;
}
if (props?.extensionMode?.cognitoAuthorizerId) {
// Create a new authorizer reference for extension mode
this.cognitoAuthorizer = new apigateway.CfnAuthorizer(this, 'ImportedAuthorizer', {
restApiId: this.api.restApiId,
type: 'COGNITO_USER_POOLS',
identitySource: 'method.request.header.Authorization',
name: 'CognitoAuthorizer',
providerArns: [this.userPool.userPoolArn]
});
// Override the ref to use the imported ID
this.cognitoAuthorizer.ref = props.extensionMode.cognitoAuthorizerId;
}
else {
this.cognitoAuthorizer = (0, api_gateway_1.createCognitoAuthorizer)(this, this.api, this.userPool, id);
}
// Create S3 bucket only if not skipped
if (!props?.skipResources?.skipBucket) {
if (props?.extensionMode?.bucketName) {
this.bucket = s3.Bucket.fromBucketName(this, 'ImportedBucket', props.extensionMode.bucketName);
}
else {
this.bucket = (0, s3_1.createS3Bucket)(this, id, props);
}
// Create CloudFront Origin Access Identity for secure S3 access
this.originAccessIdentity = (0, s3_1.createOriginAccessIdentity)(this, id);
// Configure S3 bucket policy for CloudFront access
if (this.bucket && this.originAccessIdentity) {
(0, s3_1.configureBucketPolicy)(this.bucket, this.originAccessIdentity);
}
}
// Create logging bucket if logging is enabled and not skipped
const loggingBucket = (props?.enableLogging !== false && !props?.skipResources?.skipLoggingBucket) ? (0, s3_1.createLoggingBucket)(this) : undefined;
// Create CloudFront distribution only if not skipped
if (!props?.skipResources?.skipDistribution) {
if (props?.extensionMode?.distributionId) {
// Note: CloudFront Distribution cannot be imported, so we'll set it to undefined
// Extension stacks should not need to reference the distribution directly
this.distribution = undefined;
}
else if (this.bucket && this.originAccessIdentity) {
this.distribution = (0, cloudfront_1.createCloudFrontDistribution)(this, id, this.bucket, this.originAccessIdentity, this.api, props, loggingBucket);
}
}
// Create default endpoints first to populate lambdaFunctions registry (unless skipped)
if (!props?.skipResources?.skipDefaultEndpoints) {
this.createDefaultEndpoints();
}
// Configure monitoring and alarms if logging is enabled
// This must come after createDefaultEndpoints() to avoid circular dependency
if (props?.enableLogging !== false && this.distribution) {
(0, monitoring_1.createMonitoringResources)(this, this.api, this.lambdaFunctions, this.distribution, id);
}
}
/**
* Creates default health, whoami, and config endpoints
*/
createDefaultEndpoints() {
// Determine the base lambda source path
const baseLambdaPath = this.node.tryGetContext('props')?.lambdaSourcePath ||
path.join(__dirname, '../lambda');
// Create health endpoint
this.addResource({
path: '/health',
method: 'GET',
lambdaSourcePath: path.join(baseLambdaPath, 'health'),
requiresAuth: false,
environment: {
API_VERSION: '1.0.0',
SERVICE_NAME: 'serverless-web-app-api'
}
});
// Create whoami endpoint
this.addResource({
path: '/whoami',
method: 'GET',
lambdaSourcePath: path.join(baseLambdaPath, 'whoami'),
requiresAuth: true,
environment: {
API_VERSION: '1.0.0',
SERVICE_NAME: 'serverless-web-app-api'
}
});
// Create config endpoint for frontend configuration
// Note: Cognito values are now looked up dynamically to avoid circular dependencies
const configLambda = this.addResource({
path: '/config',
method: 'GET',
lambdaSourcePath: path.join(baseLambdaPath, 'config'),
requiresAuth: false,
environment: {
API_VERSION: '1.0.0'
}
});
// Add permissions to list Cognito resources
configLambda.addToRolePolicy(new iam.PolicyStatement({
sid: 'CognitoListAccess',
effect: iam.Effect.ALLOW,
actions: [
'cognito-idp:ListUserPools',
'cognito-idp:ListUserPoolClients'
],
resources: ['*'] // Scope is limited to listing operations only
}));
}
/**
* Adds a new API resource with an associated Lambda function
*
* @param options Configuration for the new resource
* @returns The created Lambda function
*/
addResource(options) {
// Validate input parameters
this.validateAddResourceOptions(options);
// Create resource configuration
const config = {
path: `/api${options.path}`,
method: options.method || 'GET',
requiresAuth: options.requiresAuth || false,
cognitoGroup: options.cognitoGroup,
lambdaSourcePath: options.lambdaSourcePath,
environment: options.environment,
enableDLQ: options.enableDLQ || false,
enableHealthAlarms: options.enableHealthAlarms || false
};
// Store configuration for later implementation
const resourceKey = `${config.method}:${config.path}`;
this.resourceConfigs[resourceKey] = config;
// Create Lambda function for this resource
const lambdaFunction = (0, lambda_1.createApiLambdaFunction)(this, config.path, config, this.userPool, this.userPoolClient, this.node.id);
// Create API Gateway resource and method
const apiResource = (0, api_gateway_1.createApiGatewayResource)(this.api, config.path);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
(0, api_gateway_1.createApiGatewayMethod)(apiResource, config, lambdaFunction, this.cognitoAuthorizer, this.api);
// Store Lambda function in registry
const lambdaEntry = {
function: lambdaFunction,
config: config
};
this.lambdaFunctions[`${config.method} ${config.path}`] = lambdaEntry;
return lambdaFunction;
}
/**
* Validates the options provided to addResource method
*
* @param options The options to validate
* @throws Error if validation fails
*/
validateAddResourceOptions(options) {
if (!options.path) {
throw new Error('Resource path is required');
}
if (!options.path.startsWith('/')) {
throw new Error('Resource path must start with "/"');
}
if (!options.lambdaSourcePath) {
throw new Error('Lambda source path is required');
}
if (options.method && !['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'].includes(options.method.toUpperCase())) {
throw new Error('Invalid HTTP method. Supported methods: GET, POST, PUT, DELETE, PATCH, OPTIONS');
}
if (options.cognitoGroup && !options.requiresAuth) {
throw new Error('cognitoGroup can only be specified when requiresAuth is true');
}
}
/**
* Creates a Lambda function from a source directory with proper IAM role and configuration
*
* @param functionName Unique name for the Lambda function
* @param sourcePath Path to the directory containing the Lambda function source code
* @param environment Environment variables to pass to the Lambda function
* @param additionalPolicies Additional IAM policies to attach to the Lambda execution role
* @returns The created Lambda function
*/
createLambdaFunction(functionName, sourcePath, environment, additionalPolicies) {
return (0, lambda_1.createLambdaFunction)(this, functionName, sourcePath, this.node.id, environment, additionalPolicies, false);
}
/**
* Validates the security configuration of the construct
*
* @param options Security validation options
* @returns Array of validation results
*/
validateSecurity(options = {}) {
const defaultOptions = {
throwOnFailure: false,
logResults: true
};
return (0, security_validation_1.validateSecurityConfiguration)(this, { ...defaultOptions, ...options });
}
/**
* Enforces security best practices for the construct
*
* @param options Security enforcement options
*/
enforceSecurityBestPractices(options = {}) {
(0, security_validation_1.enforceSecurityBestPractices)(this, options);
}
/**
* Gets a Lambda function by path and method
*
* @param path The API path (e.g., '/users')
* @param method The HTTP method (defaults to 'GET')
* @returns The Lambda function or undefined if not found
*/
getLambdaFunction(path, method = 'GET') {
const key = `${method.toUpperCase()} /api${path}`;
return this.lambdaFunctions[key]?.function;
}
/**
* Gets exportable resource IDs for use in extension stacks
*
* @returns Object containing resource IDs that can be used by extension stacks
*/
getExportableResourceIds() {
return {
apiId: this.api.restApiId,
userPoolId: this.userPool.userPoolId,
userPoolClientId: this.userPoolClient.userPoolClientId,
bucketName: this.bucket?.bucketName,
distributionId: this.distribution?.distributionId,
cognitoAuthorizerId: this.cognitoAuthorizer.ref
};
}
/**
* Grants DynamoDB access to a Lambda function
*
* @param lambdaFunction The Lambda function to grant access to
* @param table The DynamoDB table to grant access to
* @param accessType The type of access to grant ('read', 'write', or 'readwrite')
*/
grantDynamoDBAccess(lambdaFunction, table, accessType = 'readwrite') {
switch (accessType) {
case 'read':
table.grantReadData(lambdaFunction);
break;
case 'write':
table.grantWriteData(lambdaFunction);
break;
case 'readwrite':
table.grantReadWriteData(lambdaFunction);
break;
}
}
}
exports.CDKServerlessAgenticAPI = CDKServerlessAgenticAPI;
//# sourceMappingURL=cdk-serverless-agentic-api.js.map