import * as cdk from 'aws-cdk-lib'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as lambda from 'aws-cdk-lib/aws-lambda'; import * as apigatewayv2 from 'aws-cdk-lib/aws-apigatewayv2'; import * as apigatewayv2Integrations from 'aws-cdk-lib/aws-apigatewayv2-integrations'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as sqs from 'aws-cdk-lib/aws-sqs'; import * as cognito from 'aws-cdk-lib/aws-cognito'; import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager'; import * as lambdaEventSources from 'aws-cdk-lib/aws-lambda-event-sources'; import { Construct } from 'constructs'; export interface ComputeStackProps extends cdk.StackProps { vpc: ec2.IVpc; lambdaSecurityGroup: ec2.ISecurityGroup; dbSecret: secretsmanager.ISecret; uploadsBucket: s3.IBucket; generatedBucket: s3.IBucket; libraryBucket: s3.IBucket; jobsQueue: sqs.IQueue; userPool: cognito.IUserPool; } export class ComputeStack extends cdk.Stack { constructor(scope: Construct, id: string, props: ComputeStackProps) { super(scope, id, props); const privateSubnets = { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }; // .NET 8 API Lambda const apiFunction = new lambda.Function(this, 'ApiFunction', { functionName: 'proposal-system-api', runtime: lambda.Runtime.DOTNET_8, architecture: lambda.Architecture.ARM_64, handler: 'ProposalSystem.Api', code: lambda.Code.fromAsset('../api/src/ProposalSystem.Api/bin/Release/net8.0/linux-arm64/publish'), memorySize: 1024, timeout: cdk.Duration.seconds(30), vpc: props.vpc, vpcSubnets: privateSubnets, securityGroups: [props.lambdaSecurityGroup], environment: { ASPNETCORE_ENVIRONMENT: 'Production', DB_SECRET_ARN: props.dbSecret.secretArn, UPLOADS_BUCKET: props.uploadsBucket.bucketName, GENERATED_BUCKET: props.generatedBucket.bucketName, LIBRARY_BUCKET: props.libraryBucket.bucketName, JOBS_QUEUE_URL: props.jobsQueue.queueUrl, }, tracing: lambda.Tracing.ACTIVE, }); // API Lambda permissions props.dbSecret.grantRead(apiFunction); props.uploadsBucket.grantReadWrite(apiFunction); props.generatedBucket.grantRead(apiFunction); props.jobsQueue.grantSendMessages(apiFunction); apiFunction.addToRolePolicy(new iam.PolicyStatement({ actions: ['cognito-idp:AdminGetUser', 'cognito-idp:AdminListGroupsForUser'], resources: [props.userPool.userPoolArn], })); // API Gateway HTTP API const httpApi = new apigatewayv2.HttpApi(this, 'HttpApi', { apiName: 'proposal-system-gateway', corsPreflight: { allowOrigins: [ 'https://proposals.seahaven.com', 'http://localhost:5173', ], allowMethods: [ apigatewayv2.CorsHttpMethod.GET, apigatewayv2.CorsHttpMethod.POST, apigatewayv2.CorsHttpMethod.PUT, apigatewayv2.CorsHttpMethod.DELETE, apigatewayv2.CorsHttpMethod.OPTIONS, ], allowHeaders: ['Authorization', 'Content-Type', 'X-Requested-With'], maxAge: cdk.Duration.hours(1), }, }); const apiIntegration = new apigatewayv2Integrations.HttpLambdaIntegration( 'ApiIntegration', apiFunction ); httpApi.addRoutes({ path: '/{proxy+}', methods: [apigatewayv2.HttpMethod.ANY], integration: apiIntegration, }); // Python Lambda: PDF Extract const pdfExtractFunction = new lambda.Function(this, 'PdfExtractFunction', { functionName: 'proposal-system-pdf-extract', runtime: lambda.Runtime.PYTHON_3_12, architecture: lambda.Architecture.ARM_64, handler: 'app.handler', code: lambda.Code.fromAsset('../lambdas/pdf-extract'), memorySize: 1024, timeout: cdk.Duration.seconds(120), vpc: props.vpc, vpcSubnets: privateSubnets, securityGroups: [props.lambdaSecurityGroup], environment: { UPLOADS_BUCKET: props.uploadsBucket.bucketName, API_BASE_URL: httpApi.apiEndpoint, }, }); props.uploadsBucket.grantRead(pdfExtractFunction); pdfExtractFunction.addToRolePolicy(new iam.PolicyStatement({ actions: ['bedrock:InvokeModel'], resources: ['*'], })); // Python Lambda: PDF Generate const pdfGenerateFunction = new lambda.Function(this, 'PdfGenerateFunction', { functionName: 'proposal-system-pdf-generate', runtime: lambda.Runtime.PYTHON_3_12, architecture: lambda.Architecture.ARM_64, handler: 'app.handler', code: lambda.Code.fromAsset('../lambdas/pdf-generate'), memorySize: 512, timeout: cdk.Duration.seconds(30), vpc: props.vpc, vpcSubnets: privateSubnets, securityGroups: [props.lambdaSecurityGroup], environment: { GENERATED_BUCKET: props.generatedBucket.bucketName, API_BASE_URL: httpApi.apiEndpoint, }, }); props.generatedBucket.grantWrite(pdfGenerateFunction); // Python Lambda: Library Ingest const libraryIngestFunction = new lambda.Function(this, 'LibraryIngestFunction', { functionName: 'proposal-system-library-ingest', runtime: lambda.Runtime.PYTHON_3_12, architecture: lambda.Architecture.ARM_64, handler: 'app.handler', code: lambda.Code.fromAsset('../lambdas/library-ingest'), memorySize: 512, timeout: cdk.Duration.seconds(60), vpc: props.vpc, vpcSubnets: privateSubnets, securityGroups: [props.lambdaSecurityGroup], environment: { LIBRARY_BUCKET: props.libraryBucket.bucketName, API_BASE_URL: httpApi.apiEndpoint, }, }); props.libraryBucket.grantWrite(libraryIngestFunction); libraryIngestFunction.addToRolePolicy(new iam.PolicyStatement({ actions: ['bedrock:StartIngestionJob'], resources: ['*'], })); // SQS Event Source for Python Lambdas // All three consume from the same queue, routed by message attributes // For now, use a single consumer that routes internally // TODO: Phase 4 will refine this to use message filtering or separate queues per function pdfExtractFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, { batchSize: 1, filters: [ lambda.FilterCriteria.filter({ body: { jobType: lambda.FilterRule.isEqual('pdf-extract') }, }), ], })); pdfGenerateFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, { batchSize: 1, filters: [ lambda.FilterCriteria.filter({ body: { jobType: lambda.FilterRule.isEqual('pdf-generate') }, }), ], })); libraryIngestFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, { batchSize: 1, filters: [ lambda.FilterCriteria.filter({ body: { jobType: lambda.FilterRule.isEqual('library-ingest') }, }), ], })); // Outputs new cdk.CfnOutput(this, 'ApiEndpoint', { value: httpApi.apiEndpoint }); new cdk.CfnOutput(this, 'ApiFunctionArn', { value: apiFunction.functionArn }); } }