mirror of
https://github.com/Sea-Haven-Industries/proposal-system.git
synced 2026-09-30 06:33:13 +00:00
6-domain audit (API, web, mobile, lambdas, infra, QA) with fixes: API security: scope internal API key middleware to allowed paths only, return 401 on invalid key instead of falling through, remove unvalidated JWT code path, sanitize error messages, add UpdateProposal validator, remove status field from UpdateProposalRequest to prevent over-posting, log swallowed exceptions in ProposalService. Infrastructure: enforce SSL on all S3 buckets, encrypt SQS queues, enable optional MFA on Cognito, add API Gateway access logging. Lambdas: fix _retry_request undefined variable across all 4 Lambdas, re-raise exceptions in pdf-extract/pdf-generate instead of swallowing, add idempotency guard to suggestions Lambda. Web: add ErrorBoundary, add auth loading state to ProtectedRoute, add mutation error toasts in AdminWorkspace, fix dead Cognito link. Mobile: add mutex to offline queue processing, distinguish permanent vs retryable failures, register all screens for both roles, log sync errors. Swagger/OpenAPI: add Swashbuckle with JWT bearer security definition, add ProducesResponseType attributes to key endpoints. Includes AUDIT-REPORT.md with complete findings and CLAUDE.md project instructions.
516 lines
19 KiB
TypeScript
516 lines
19 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,
|
|
}),
|
|
});
|
|
|
|
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: true,
|
|
}]),
|
|
});
|
|
|
|
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],
|
|
}));
|
|
|
|
const ossDataAccessPolicy = new opensearchserverless.CfnAccessPolicy(this, 'OssDataAccessPolicy', {
|
|
name: 'proposal-system-kb-access',
|
|
type: 'data',
|
|
policy: JSON.stringify([{
|
|
Rules: [
|
|
{ ResourceType: 'collection', Resource: ['collection/proposal-system-kb'], Permission: ['aoss:*'] },
|
|
{ ResourceType: 'index', Resource: ['index/proposal-system-kb/*'], Permission: ['aoss:*'] },
|
|
],
|
|
Principal: [kbRole.roleArn, 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],
|
|
}));
|
|
|
|
// Function URL for internal Lambda-to-API calls (bypasses API Gateway JWT authorizer)
|
|
const apiFunctionUrl = apiFunction.addFunctionUrl({
|
|
authType: lambda.FunctionUrlAuthType.NONE,
|
|
});
|
|
|
|
// 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);
|
|
suggestionsFunction.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['bedrock:InvokeModel'],
|
|
resources: [`arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-*`],
|
|
}));
|
|
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);
|
|
props.uploadsBucket.grantRead(pdfExtractFunction);
|
|
pdfExtractFunction.addToRolePolicy(new iam.PolicyStatement({
|
|
actions: ['bedrock:InvokeModel'],
|
|
resources: [`arn:aws:bedrock:us-east-1::foundation-model/anthropic.claude-*`],
|
|
}));
|
|
|
|
// 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);
|
|
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);
|
|
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 });
|
|
}
|
|
}
|