proposal-system/infra/lib/compute-stack.ts
Adam Moussa aff38a11ae
Some checks failed
Deploy / Deploy to AWS (push) Has been cancelled
feat(infra): migrate Bedrock KB vector store to Aurora pgvector (#125)
Replaces OpenSearch Serverless with Aurora PostgreSQL Serverless v2 + pgvector as
the Bedrock Knowledge Base vector store (v1 PR3). Bedrock KB requires Aurora SSv2
(RDS Data API), not a plain RDS instance — so the DB engine moves to Aurora.

- foundation: rds.DatabaseInstance (PG15) -> rds.DatabaseCluster Aurora SSv2
  (0.5-4 ACU, enableDataApi). RDS alarms: free-storage -> freeable-memory.
- compute: delete all AOSS (collection, policies, VPC endpoint, index-creator);
  add bedrock_user secret + KB role (scoped rds-data + secret read); repoint
  CfnKnowledgeBase to RDS storage (bedrock_integration.bedrock_kb, vector(1024)).
- lambdas: oss-index-creator -> aurora-pgvector-init (bootstrap schema/table/
  indexes/role via RDS Data API; transient-error retry; password guard).
- ADR 0001 documents the decision.

Eliminates the ~$175-350/mo AOSS OCU floor. NAT kept (egress still needed).
GPT-4.1 cross-review: no BLOCK (FIX applied). tsc clean; foundation synth shows
Aurora cluster with Data API enabled; 23 pytest pass.
2026-06-12 18:44:42 -04:00

547 lines
21 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 rds from 'aws-cdk-lib/aws-rds';
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';
import { EnvConfig } from './config';
export interface ComputeStackProps extends cdk.StackProps {
config: EnvConfig;
vpc: ec2.IVpc;
lambdaSecurityGroup: ec2.ISecurityGroup;
dbSecret: secretsmanager.ISecret;
dbCluster: rds.IDatabaseCluster;
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 { config } = 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${config.stackSuffix}`,
generateSecretString: {
excludePunctuation: true,
passwordLength: 48,
},
});
// Bedrock Knowledge Base vector store: Aurora PostgreSQL + pgvector (PR3 —
// replaced OpenSearch Serverless). A dedicated bedrock_user role queries the
// pgvector table; the aurora-pgvector-init custom resource creates the schema.
const dbCluster = props.dbCluster;
// Credentials Bedrock uses to query the pgvector table as bedrock_user.
// SQL-unsafe characters are excluded so the bootstrap can inline the password.
const bedrockUserSecret = new secretsmanager.Secret(this, 'BedrockUserSecret', {
secretName: `proposal-system/bedrock-user${config.stackSuffix}`,
generateSecretString: {
secretStringTemplate: JSON.stringify({ username: 'bedrock_user' }),
generateStringKey: 'password',
excludePunctuation: true,
passwordLength: 32,
},
});
// Bedrock KB execution role
const kbRole = new iam.Role(this, 'KnowledgeBaseRole', {
roleName: `proposal-system-kb-role${config.stackSuffix}`,
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: ['bedrock:InvokeModel'],
resources: [`arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-embed-text-v2:0`],
}));
// KB queries the pgvector table via the RDS Data API as bedrock_user.
kbRole.addToPolicy(new iam.PolicyStatement({
actions: [
'rds-data:ExecuteStatement',
'rds-data:BatchExecuteStatement',
'rds-data:BeginTransaction',
'rds-data:CommitTransaction',
'rds-data:RollbackTransaction',
],
resources: [dbCluster.clusterArn],
}));
bedrockUserSecret.grantRead(kbRole);
// One-time bootstrap: enable pgvector + create the bedrock_integration schema,
// table, indexes, and bedrock_user role (via the RDS Data API as master).
const pgvectorInitFn = new lambda.Function(this, 'PgVectorInit', {
functionName: `proposal-system-pgvector-init${config.stackSuffix}`,
runtime: lambda.Runtime.PYTHON_3_12,
architecture: lambda.Architecture.ARM_64,
handler: 'app.handler',
code: lambda.Code.fromAsset('../lambdas/aurora-pgvector-init'),
timeout: cdk.Duration.minutes(5),
environment: {
CLUSTER_ARN: dbCluster.clusterArn,
MASTER_SECRET_ARN: props.dbSecret.secretArn,
BEDROCK_SECRET_ARN: bedrockUserSecret.secretArn,
DATABASE: 'proposals',
EMBED_DIM: '1024',
},
logRetention: logs.RetentionDays.TWO_MONTHS,
});
dbCluster.grantDataApiAccess(pgvectorInitFn);
bedrockUserSecret.grantRead(pgvectorInitFn);
const pgvectorProvider = new cr.Provider(this, 'PgVectorInitProvider', {
onEventHandler: pgvectorInitFn,
});
const pgvectorInit = new cdk.CustomResource(this, 'PgVectorInitResource', {
serviceToken: pgvectorProvider.serviceToken,
properties: {
// Bump to force the bootstrap to re-run when the schema/logic changes.
Version: '1',
},
});
const knowledgeBase = new bedrock.CfnKnowledgeBase(this, 'KnowledgeBase', {
name: `proposal-system-kb${config.stackSuffix}`,
roleArn: kbRole.roleArn,
knowledgeBaseConfiguration: {
type: 'VECTOR',
vectorKnowledgeBaseConfiguration: {
embeddingModelArn: `arn:aws:bedrock:us-east-1::foundation-model/amazon.titan-embed-text-v2:0`,
},
},
storageConfiguration: {
type: 'RDS',
rdsConfiguration: {
resourceArn: dbCluster.clusterArn,
credentialsSecretArn: bedrockUserSecret.secretArn,
databaseName: 'proposals',
tableName: 'bedrock_integration.bedrock_kb',
fieldMapping: {
primaryKeyField: 'id',
vectorField: 'embedding',
textField: 'chunks',
metadataField: 'metadata',
},
},
},
});
knowledgeBase.node.addDependency(pgvectorInit);
// KB Data Source (S3 library bucket)
const dataSource = new bedrock.CfnDataSource(this, 'KbDataSource', {
name: `proposal-system-library${config.stackSuffix}`,
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${config.stackSuffix}`,
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: `${config.cognitoDomainPrefix}.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${config.stackSuffix}`,
corsPreflight: {
allowOrigins: config.apiCorsOrigins,
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${config.stackSuffix}`,
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, apigatewayv2.HttpMethod.OPTIONS],
integration: apiIntegration,
});
httpApi.addRoutes({
path: '/swagger/{proxy+}',
methods: [apigatewayv2.HttpMethod.GET],
integration: apiIntegration,
});
httpApi.addRoutes({
path: '/swagger',
methods: [apigatewayv2.HttpMethod.GET],
integration: apiIntegration,
});
httpApi.addRoutes({
path: '/{proxy+}',
methods: [
apigatewayv2.HttpMethod.GET,
apigatewayv2.HttpMethod.POST,
apigatewayv2.HttpMethod.PUT,
apigatewayv2.HttpMethod.DELETE,
apigatewayv2.HttpMethod.PATCH,
],
integration: apiIntegration,
authorizer: jwtAuthorizer,
});
// Fix (v1 PR1): bundle pip dependencies into each Python Lambda asset (previously
// bare fromAsset shipped no deps -> ImportError at cold start). --platform/--only-binary
// fetches manylinux aarch64 wheels so the ARM64 functions get correct binaries
// regardless of the build-host architecture.
const pythonBundling = {
image: lambda.Runtime.PYTHON_3_12.bundlingImage,
command: [
'bash', '-c',
'pip install -r requirements.txt --platform manylinux2014_aarch64 --python-version 3.12 --implementation cp --abi cp312 --only-binary=:all: --target /asset-output && cp -au . /asset-output',
],
};
// Python Lambda: Suggestions Engine
const suggestionsFunction = new lambda.Function(this, 'SuggestionsFunction', {
functionName: `proposal-system-suggestions${config.stackSuffix}`,
runtime: lambda.Runtime.PYTHON_3_12,
architecture: lambda.Architecture.ARM_64,
handler: 'app.handler',
code: lambda.Code.fromAsset('../lambdas/suggestions', { bundling: pythonBundling }),
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${config.stackSuffix}`,
runtime: lambda.Runtime.PYTHON_3_12,
architecture: lambda.Architecture.ARM_64,
handler: 'app.handler',
code: lambda.Code.fromAsset('../lambdas/pdf-extract', { bundling: pythonBundling }),
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${config.stackSuffix}`,
runtime: lambda.Runtime.PYTHON_3_12,
architecture: lambda.Architecture.ARM_64,
handler: 'app.handler',
code: lambda.Code.fromAsset('../lambdas/pdf-generate', { bundling: pythonBundling }),
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${config.stackSuffix}`,
runtime: lambda.Runtime.PYTHON_3_12,
architecture: lambda.Architecture.ARM_64,
handler: 'app.handler',
code: lambda.Code.fromAsset('../lambdas/library-ingest', { bundling: pythonBundling }),
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${config.stackSuffix}`,
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${config.stackSuffix}`,
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 });
}
}