proposal-system/infra/lib/compute-stack.ts

200 lines
7 KiB
TypeScript
Raw Normal View History

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 });
}
}