mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-09-30 07:43:14 +00:00
INF-M1: Replace wildcard anthropic.claude-* foundation-model ARN with the specific cross-region inference profile ARN and its backing foundation model. Both suggestions and pdf-extract Lambdas use us.anthropic.claude-sonnet-4-5-20250929-v1:0. INF-M2: Replace aoss:* data access policy permissions with scoped actions. KB role gets DescribeCollectionItems/CreateCollectionItems/UpdateCollectionItems on collection and DescribeIndex/ReadDocument/WriteDocument on indexes. Index creator gets CreateIndex/DescribeIndex/WriteDocument plus collection describe/create. INF-M9: Change --require-approval never to --require-approval broadening in infra/package.json deploy script so IAM/security changes require manual confirmation during local development.
592 lines
22 KiB
TypeScript
592 lines
22 KiB
TypeScript
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 apigatewayv2Authorizers from 'aws-cdk-lib/aws-apigatewayv2-authorizers';
|
|
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 sns from 'aws-cdk-lib/aws-sns';
|
|
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 * as bedrock from 'aws-cdk-lib/aws-bedrock';
|
|
import * as opensearchserverless from 'aws-cdk-lib/aws-opensearchserverless';
|
|
import * as logs from 'aws-cdk-lib/aws-logs';
|
|
import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch';
|
|
import * as cloudwatchActions from 'aws-cdk-lib/aws-cloudwatch-actions';
|
|
import * as cr from 'aws-cdk-lib/custom-resources';
|
|
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;
|
|
alarmTopic: sns.ITopic;
|
|
webClientId: string;
|
|
mobileClientId: string;
|
|
}
|
|
|
|
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 };
|
|
|
|
// Internal API key for Lambda-to-API calls (stored in Secrets Manager)
|
|
const internalApiKeySecret = new secretsmanager.Secret(this, 'InternalApiKeySecret', {
|
|
secretName: 'proposal-system/internal-api-key',
|
|
generateSecretString: {
|
|
excludePunctuation: true,
|
|
passwordLength: 48,
|
|
},
|
|
});
|
|
|
|
// OpenSearch Serverless collection for Bedrock KB vector store
|
|
const ossEncryptionPolicy = new opensearchserverless.CfnSecurityPolicy(this, 'OssEncryptionPolicy', {
|
|
name: 'proposal-system-kb-enc',
|
|
type: 'encryption',
|
|
policy: JSON.stringify({
|
|
Rules: [{ ResourceType: 'collection', Resource: ['collection/proposal-system-kb'] }],
|
|
AWSOwnedKey: true,
|
|
}),
|
|
});
|
|
|
|
// Fix: INF-H3 — restrict OpenSearch Serverless to VPC (was AllowFromPublic: true).
|
|
// Create a VPC endpoint so Lambdas in private subnets can reach the collection.
|
|
const ossVpcEndpoint = new opensearchserverless.CfnVpcEndpoint(this, 'OssVpcEndpoint', {
|
|
name: 'proposal-system-kb-vpce',
|
|
vpcId: props.vpc.vpcId,
|
|
subnetIds: props.vpc.selectSubnets({ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }).subnetIds,
|
|
securityGroupIds: [props.lambdaSecurityGroup.securityGroupId],
|
|
});
|
|
|
|
const ossNetworkPolicy = new opensearchserverless.CfnSecurityPolicy(this, 'OssNetworkPolicy', {
|
|
name: 'proposal-system-kb-net',
|
|
type: 'network',
|
|
policy: JSON.stringify([{
|
|
Rules: [
|
|
{ ResourceType: 'collection', Resource: ['collection/proposal-system-kb'] },
|
|
],
|
|
AllowFromPublic: false,
|
|
SourceVPCEs: [ossVpcEndpoint.attrId],
|
|
}]),
|
|
});
|
|
ossNetworkPolicy.addDependency(ossVpcEndpoint);
|
|
|
|
const ossCollection = new opensearchserverless.CfnCollection(this, 'OssCollection', {
|
|
name: 'proposal-system-kb',
|
|
type: 'VECTORSEARCH',
|
|
});
|
|
ossCollection.addDependency(ossEncryptionPolicy);
|
|
ossCollection.addDependency(ossNetworkPolicy);
|
|
|
|
// Bedrock KB execution role
|
|
const kbRole = new iam.Role(this, 'KnowledgeBaseRole', {
|
|
roleName: 'proposal-system-kb-role',
|
|
assumedBy: new iam.ServicePrincipal('bedrock.amazonaws.com'),
|
|
});
|
|
|
|
kbRole.addToPolicy(new iam.PolicyStatement({
|
|
actions: ['s3:GetObject', 's3:ListBucket'],
|
|
resources: [props.libraryBucket.bucketArn, `${props.libraryBucket.bucketArn}/*`],
|
|
}));
|
|
|
|
kbRole.addToPolicy(new iam.PolicyStatement({
|
|
actions: ['aoss:APIAccessAll'],
|
|
resources: [ossCollection.attrArn],
|
|
}));
|
|
|
|
kbRole.addToPolicy(new iam.PolicyStatement({
|
|
actions: ['bedrock:InvokeModel'],
|
|
resources: [`arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-embed-text-v2:0`],
|
|
}));
|
|
|
|
// Lambda to pre-create the vector index (retries until AOSS access policy propagates)
|
|
const indexCreatorFn = new lambda.Function(this, 'OssIndexCreator', {
|
|
functionName: 'proposal-system-oss-index-creator',
|
|
runtime: lambda.Runtime.PYTHON_3_12,
|
|
architecture: lambda.Architecture.ARM_64,
|
|
handler: 'app.handler',
|
|
code: lambda.Code.fromAsset('../lambdas/oss-index-creator', {
|
|
bundling: {
|
|
image: lambda.Runtime.PYTHON_3_12.bundlingImage,
|
|
command: [
|
|
'bash', '-c',
|
|
'pip install -r requirements.txt -t /asset-output && cp -au . /asset-output',
|
|
],
|
|
},
|
|
}),
|
|
timeout: cdk.Duration.minutes(6),
|
|
logRetention: logs.RetentionDays.TWO_MONTHS,
|
|
});
|
|
|
|
indexCreatorFn.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['aoss:APIAccessAll'],
|
|
resources: [ossCollection.attrArn],
|
|
}));
|
|
|
|
// Fix: INF-M2 — scope AOSS data access policy permissions (was aoss:* on both
|
|
// collection and index). KB role needs read/write for embeddings. Index creator
|
|
// needs create/describe for bootstrapping the vector index.
|
|
const ossDataAccessPolicy = new opensearchserverless.CfnAccessPolicy(this, 'OssDataAccessPolicy', {
|
|
name: 'proposal-system-kb-access',
|
|
type: 'data',
|
|
policy: JSON.stringify([
|
|
{
|
|
Description: 'Bedrock KB role — read/write documents and describe collection',
|
|
Rules: [
|
|
{
|
|
ResourceType: 'collection',
|
|
Resource: ['collection/proposal-system-kb'],
|
|
Permission: [
|
|
'aoss:DescribeCollectionItems',
|
|
'aoss:CreateCollectionItems',
|
|
'aoss:UpdateCollectionItems',
|
|
],
|
|
},
|
|
{
|
|
ResourceType: 'index',
|
|
Resource: ['index/proposal-system-kb/*'],
|
|
Permission: [
|
|
'aoss:DescribeIndex',
|
|
'aoss:ReadDocument',
|
|
'aoss:WriteDocument',
|
|
],
|
|
},
|
|
],
|
|
Principal: [kbRole.roleArn],
|
|
},
|
|
{
|
|
Description: 'Index creator Lambda — create and describe index during bootstrap',
|
|
Rules: [
|
|
{
|
|
ResourceType: 'collection',
|
|
Resource: ['collection/proposal-system-kb'],
|
|
Permission: [
|
|
'aoss:DescribeCollectionItems',
|
|
'aoss:CreateCollectionItems',
|
|
],
|
|
},
|
|
{
|
|
ResourceType: 'index',
|
|
Resource: ['index/proposal-system-kb/*'],
|
|
Permission: [
|
|
'aoss:CreateIndex',
|
|
'aoss:DescribeIndex',
|
|
'aoss:WriteDocument',
|
|
],
|
|
},
|
|
],
|
|
Principal: [indexCreatorFn.role!.roleArn],
|
|
},
|
|
]),
|
|
});
|
|
ossDataAccessPolicy.addDependency(ossCollection);
|
|
|
|
const indexProvider = new cr.Provider(this, 'OssIndexProvider', {
|
|
onEventHandler: indexCreatorFn,
|
|
});
|
|
|
|
const ossIndex = new cdk.CustomResource(this, 'OssIndex', {
|
|
serviceToken: indexProvider.serviceToken,
|
|
properties: {
|
|
Endpoint: ossCollection.attrCollectionEndpoint,
|
|
IndexName: 'proposal-system-index',
|
|
VectorField: 'embedding',
|
|
TextField: 'text',
|
|
MetadataField: 'metadata',
|
|
},
|
|
});
|
|
ossIndex.node.addDependency(ossDataAccessPolicy);
|
|
|
|
const knowledgeBase = new bedrock.CfnKnowledgeBase(this, 'KnowledgeBase', {
|
|
name: 'proposal-system-kb',
|
|
roleArn: kbRole.roleArn,
|
|
knowledgeBaseConfiguration: {
|
|
type: 'VECTOR',
|
|
vectorKnowledgeBaseConfiguration: {
|
|
embeddingModelArn: `arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-embed-text-v2:0`,
|
|
},
|
|
},
|
|
storageConfiguration: {
|
|
type: 'OPENSEARCH_SERVERLESS',
|
|
opensearchServerlessConfiguration: {
|
|
collectionArn: ossCollection.attrArn,
|
|
vectorIndexName: 'proposal-system-index',
|
|
fieldMapping: {
|
|
vectorField: 'embedding',
|
|
textField: 'text',
|
|
metadataField: 'metadata',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
knowledgeBase.node.addDependency(ossIndex);
|
|
|
|
// KB Data Source (S3 library bucket)
|
|
const dataSource = new bedrock.CfnDataSource(this, 'KbDataSource', {
|
|
name: 'proposal-system-library',
|
|
knowledgeBaseId: knowledgeBase.attrKnowledgeBaseId,
|
|
dataSourceConfiguration: {
|
|
type: 'S3',
|
|
s3Configuration: {
|
|
bucketArn: props.libraryBucket.bucketArn,
|
|
},
|
|
},
|
|
vectorIngestionConfiguration: {
|
|
chunkingConfiguration: {
|
|
chunkingStrategy: 'FIXED_SIZE',
|
|
fixedSizeChunkingConfiguration: {
|
|
maxTokens: 512,
|
|
overlapPercentage: 20,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
// .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,
|
|
INTERNAL_API_KEY_SECRET_ARN: internalApiKeySecret.secretArn,
|
|
Auth__Authority: `https://cognito-idp.${this.region}.amazonaws.com/${props.userPool.userPoolId}`,
|
|
Auth__ClientId: props.webClientId,
|
|
Auth__CognitoDomain: `proposal-system-seahaven.auth.${this.region}.amazoncognito.com`,
|
|
COGNITO_WEB_CLIENT_ID: props.webClientId,
|
|
COGNITO_MOBILE_CLIENT_ID: props.mobileClientId,
|
|
},
|
|
tracing: lambda.Tracing.ACTIVE,
|
|
logRetention: logs.RetentionDays.TWO_MONTHS,
|
|
});
|
|
|
|
// API Lambda permissions
|
|
props.dbSecret.grantRead(apiFunction);
|
|
internalApiKeySecret.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],
|
|
}));
|
|
|
|
// Fix: LAM-C1/INF-H1 — require IAM auth on Function URL (was authType NONE).
|
|
// NOTE: Lambda HTTP clients (suggestions, pdf-extract, pdf-generate, library-ingest)
|
|
// must use SigV4 signing when calling this URL. The API key header alone is no longer
|
|
// sufficient for authentication at the transport layer.
|
|
const apiFunctionUrl = apiFunction.addFunctionUrl({
|
|
authType: lambda.FunctionUrlAuthType.AWS_IAM,
|
|
});
|
|
|
|
// 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 apiAccessLogGroup = new logs.LogGroup(this, 'ApiAccessLogs', {
|
|
logGroupName: '/aws/apigateway/proposal-system',
|
|
retention: logs.RetentionDays.TWO_MONTHS,
|
|
removalPolicy: cdk.RemovalPolicy.DESTROY,
|
|
});
|
|
|
|
const defaultStage = httpApi.defaultStage!.node.defaultChild as apigatewayv2.CfnStage;
|
|
defaultStage.defaultRouteSettings = {
|
|
throttlingBurstLimit: 50,
|
|
throttlingRateLimit: 100,
|
|
};
|
|
defaultStage.accessLogSettings = {
|
|
destinationArn: apiAccessLogGroup.logGroupArn,
|
|
format: JSON.stringify({
|
|
requestId: '$context.requestId',
|
|
ip: '$context.identity.sourceIp',
|
|
method: '$context.httpMethod',
|
|
path: '$context.path',
|
|
status: '$context.status',
|
|
latency: '$context.responseLatency',
|
|
userAgent: '$context.identity.userAgent',
|
|
}),
|
|
};
|
|
|
|
const apiIntegration = new apigatewayv2Integrations.HttpLambdaIntegration(
|
|
'ApiIntegration',
|
|
apiFunction
|
|
);
|
|
|
|
const jwtAuthorizer = new apigatewayv2Authorizers.HttpJwtAuthorizer(
|
|
'CognitoAuthorizer',
|
|
`https://cognito-idp.${this.region}.amazonaws.com/${props.userPool.userPoolId}`,
|
|
{ jwtAudience: [props.webClientId, props.mobileClientId] },
|
|
);
|
|
|
|
httpApi.addRoutes({
|
|
path: '/api/health',
|
|
methods: [apigatewayv2.HttpMethod.GET],
|
|
integration: apiIntegration,
|
|
});
|
|
|
|
httpApi.addRoutes({
|
|
path: '/api/auth/{proxy+}',
|
|
methods: [apigatewayv2.HttpMethod.POST],
|
|
integration: apiIntegration,
|
|
});
|
|
|
|
httpApi.addRoutes({
|
|
path: '/{proxy+}',
|
|
methods: [apigatewayv2.HttpMethod.ANY],
|
|
integration: apiIntegration,
|
|
authorizer: jwtAuthorizer,
|
|
});
|
|
|
|
// Python Lambda: Suggestions Engine
|
|
const suggestionsFunction = new lambda.Function(this, 'SuggestionsFunction', {
|
|
functionName: 'proposal-system-suggestions',
|
|
runtime: lambda.Runtime.PYTHON_3_12,
|
|
architecture: lambda.Architecture.ARM_64,
|
|
handler: 'app.handler',
|
|
code: lambda.Code.fromAsset('../lambdas/suggestions'),
|
|
memorySize: 512,
|
|
timeout: cdk.Duration.seconds(60),
|
|
vpc: props.vpc,
|
|
vpcSubnets: privateSubnets,
|
|
securityGroups: [props.lambdaSecurityGroup],
|
|
environment: {
|
|
KNOWLEDGE_BASE_ID: knowledgeBase.attrKnowledgeBaseId,
|
|
MODEL_ID: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
|
|
API_BASE_URL: apiFunctionUrl.url,
|
|
INTERNAL_API_KEY_SECRET_ARN: internalApiKeySecret.secretArn,
|
|
},
|
|
logRetention: logs.RetentionDays.TWO_MONTHS,
|
|
});
|
|
|
|
internalApiKeySecret.grantRead(suggestionsFunction);
|
|
// Fix: LAM-C1/INF-H1 — grant Function URL invoke permission for IAM auth
|
|
apiFunctionUrl.grantInvokeUrl(suggestionsFunction);
|
|
// Fix: INF-M1 — scope Bedrock model ARN to the specific inference profile used
|
|
// (was wildcard anthropic.claude-*). Lambda MODEL_ID is a cross-region inference profile.
|
|
suggestionsFunction.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['bedrock:InvokeModel'],
|
|
resources: [
|
|
`arn:aws:bedrock:us-east-1:${this.account}:inference-profile/us.anthropic.claude-sonnet-4-5-20250929-v1:0`,
|
|
`arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0`,
|
|
],
|
|
}));
|
|
suggestionsFunction.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['bedrock:Retrieve'],
|
|
resources: [knowledgeBase.attrKnowledgeBaseArn],
|
|
}));
|
|
|
|
// 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,
|
|
MODEL_ID: 'us.anthropic.claude-sonnet-4-5-20250929-v1:0',
|
|
API_BASE_URL: apiFunctionUrl.url,
|
|
INTERNAL_API_KEY_SECRET_ARN: internalApiKeySecret.secretArn,
|
|
},
|
|
logRetention: logs.RetentionDays.TWO_MONTHS,
|
|
});
|
|
|
|
internalApiKeySecret.grantRead(pdfExtractFunction);
|
|
// Fix: LAM-C1/INF-H1 — grant Function URL invoke permission for IAM auth
|
|
apiFunctionUrl.grantInvokeUrl(pdfExtractFunction);
|
|
props.uploadsBucket.grantRead(pdfExtractFunction);
|
|
// Fix: INF-M1 — scope Bedrock model ARN to the specific inference profile used
|
|
pdfExtractFunction.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['bedrock:InvokeModel'],
|
|
resources: [
|
|
`arn:aws:bedrock:us-east-1:${this.account}:inference-profile/us.anthropic.claude-sonnet-4-5-20250929-v1:0`,
|
|
`arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-sonnet-4-5-20250929-v1:0`,
|
|
],
|
|
}));
|
|
|
|
// 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: apiFunctionUrl.url,
|
|
INTERNAL_API_KEY_SECRET_ARN: internalApiKeySecret.secretArn,
|
|
},
|
|
logRetention: logs.RetentionDays.TWO_MONTHS,
|
|
});
|
|
|
|
internalApiKeySecret.grantRead(pdfGenerateFunction);
|
|
// Fix: LAM-C1/INF-H1 — grant Function URL invoke permission for IAM auth
|
|
apiFunctionUrl.grantInvokeUrl(pdfGenerateFunction);
|
|
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,
|
|
KNOWLEDGE_BASE_ID: knowledgeBase.attrKnowledgeBaseId,
|
|
DATA_SOURCE_ID: dataSource.attrDataSourceId,
|
|
API_BASE_URL: apiFunctionUrl.url,
|
|
INTERNAL_API_KEY_SECRET_ARN: internalApiKeySecret.secretArn,
|
|
},
|
|
logRetention: logs.RetentionDays.TWO_MONTHS,
|
|
});
|
|
|
|
internalApiKeySecret.grantRead(libraryIngestFunction);
|
|
// Fix: LAM-C1/INF-H1 — grant Function URL invoke permission for IAM auth
|
|
apiFunctionUrl.grantInvokeUrl(libraryIngestFunction);
|
|
props.libraryBucket.grantWrite(libraryIngestFunction);
|
|
libraryIngestFunction.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['bedrock:StartIngestionJob'],
|
|
resources: [knowledgeBase.attrKnowledgeBaseArn],
|
|
}));
|
|
|
|
// SQS Event Sources with message filtering
|
|
suggestionsFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, {
|
|
batchSize: 1,
|
|
reportBatchItemFailures: true,
|
|
filters: [
|
|
lambda.FilterCriteria.filter({
|
|
body: { jobType: lambda.FilterRule.isEqual('suggestions') },
|
|
}),
|
|
],
|
|
}));
|
|
|
|
pdfExtractFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, {
|
|
batchSize: 1,
|
|
reportBatchItemFailures: true,
|
|
filters: [
|
|
lambda.FilterCriteria.filter({
|
|
body: { jobType: lambda.FilterRule.isEqual('pdf-extract') },
|
|
}),
|
|
],
|
|
}));
|
|
|
|
pdfGenerateFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, {
|
|
batchSize: 1,
|
|
reportBatchItemFailures: true,
|
|
filters: [
|
|
lambda.FilterCriteria.filter({
|
|
body: { jobType: lambda.FilterRule.isEqual('pdf-generate') },
|
|
}),
|
|
],
|
|
}));
|
|
|
|
libraryIngestFunction.addEventSource(new lambdaEventSources.SqsEventSource(props.jobsQueue, {
|
|
batchSize: 1,
|
|
reportBatchItemFailures: true,
|
|
filters: [
|
|
lambda.FilterCriteria.filter({
|
|
body: { jobType: lambda.FilterRule.isEqual('library-ingest') },
|
|
}),
|
|
],
|
|
}));
|
|
|
|
// CloudWatch Alarms
|
|
const alarmAction = new cloudwatchActions.SnsAction(props.alarmTopic);
|
|
|
|
const lambdaFunctions = [
|
|
{ fn: apiFunction, name: 'api' },
|
|
{ fn: suggestionsFunction, name: 'suggestions' },
|
|
{ fn: pdfExtractFunction, name: 'pdf-extract' },
|
|
{ fn: pdfGenerateFunction, name: 'pdf-generate' },
|
|
{ fn: libraryIngestFunction, name: 'library-ingest' },
|
|
];
|
|
|
|
for (const { fn, name } of lambdaFunctions) {
|
|
const alarm = new cloudwatch.Alarm(this, `LambdaErrors-${name}`, {
|
|
alarmName: `proposal-system-${name}-errors`,
|
|
alarmDescription: `Lambda errors for ${name}`,
|
|
metric: fn.metricErrors({ period: cdk.Duration.minutes(5) }),
|
|
threshold: 1,
|
|
evaluationPeriods: 1,
|
|
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
});
|
|
alarm.addAlarmAction(alarmAction);
|
|
}
|
|
|
|
const api5xxAlarm = new cloudwatch.Alarm(this, 'Api5xxAlarm', {
|
|
alarmName: 'proposal-system-api-5xx',
|
|
alarmDescription: 'API Gateway 5xx errors',
|
|
metric: new cloudwatch.Metric({
|
|
namespace: 'AWS/ApiGateway',
|
|
metricName: '5xx',
|
|
dimensionsMap: { ApiId: httpApi.httpApiId },
|
|
statistic: 'Sum',
|
|
period: cdk.Duration.minutes(5),
|
|
}),
|
|
threshold: 5,
|
|
evaluationPeriods: 1,
|
|
treatMissingData: cloudwatch.TreatMissingData.NOT_BREACHING,
|
|
});
|
|
api5xxAlarm.addAlarmAction(alarmAction);
|
|
|
|
// Outputs
|
|
new cdk.CfnOutput(this, 'ApiEndpoint', { value: httpApi.apiEndpoint });
|
|
new cdk.CfnOutput(this, 'ApiFunctionArn', { value: apiFunction.functionArn });
|
|
new cdk.CfnOutput(this, 'KnowledgeBaseId', { value: knowledgeBase.attrKnowledgeBaseId });
|
|
new cdk.CfnOutput(this, 'DataSourceId', { value: dataSource.attrDataSourceId });
|
|
}
|
|
}
|