packages/blueprints/gen-ai-chatbot/static-assets/chatbot-genai-cdk/lib/api-publishment-stack.ts (196 lines of code) (raw):
import * as cdk from "aws-cdk-lib";
import { CfnOutput, Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as apigateway from "aws-cdk-lib/aws-apigateway";
import { DockerImageCode, DockerImageFunction } from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambdaEventSources from "aws-cdk-lib/aws-lambda-event-sources";
import * as path from "path";
import { Platform } from "aws-cdk-lib/aws-ecr-assets";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as wafv2 from "aws-cdk-lib/aws-wafv2";
import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager";
import * as sqs from "aws-cdk-lib/aws-sqs";
import * as s3 from "aws-cdk-lib/aws-s3";
export interface VpcConfig {
vpcId: string;
availabilityZones: string[];
publicSubnetIds: string[];
privateSubnetIds: string[];
isolatedSubnetIds: string[];
}
interface ApiPublishmentStackProps extends StackProps {
readonly bedrockRegion: string;
readonly vpcConfig: VpcConfig;
readonly dbConfigHostname: string;
readonly dbConfigPort: number;
readonly dbConfigSecretArn: string;
readonly dbSecurityGroupId: string;
readonly conversationTableName: string;
readonly tableAccessRoleArn: string;
readonly webAclArn: string;
readonly usagePlan: apigateway.UsagePlanProps;
readonly deploymentStage?: string;
readonly largeMessageBucketName: string;
readonly corsOptions?: apigateway.CorsOptions;
}
export class ApiPublishmentStack extends Stack {
public readonly chatQueue: sqs.Queue;
constructor(scope: Construct, id: string, props: ApiPublishmentStackProps) {
super(scope, id, props);
console.log(`usagePlan: ${JSON.stringify(props.usagePlan)}`); // DEBUG
const dbSecret = secretsmanager.Secret.fromSecretCompleteArn(
this,
"DbSecret",
props.dbConfigSecretArn
);
const deploymentStage = props.deploymentStage ?? "dev";
const vpc = ec2.Vpc.fromVpcAttributes(this, "Vpc", props.vpcConfig);
const chatQueue = new sqs.Queue(this, "ChatQueue", {
visibilityTimeout: cdk.Duration.minutes(30),
});
const handlerRole = new iam.Role(this, "HandlerRole", {
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
});
handlerRole.addToPolicy(
// Assume the table access role for row-level access control.
new iam.PolicyStatement({
actions: ["sts:AssumeRole"],
resources: [props.tableAccessRoleArn],
})
);
handlerRole.addToPolicy(
new iam.PolicyStatement({
actions: ["bedrock:*"],
resources: ["*"],
})
);
handlerRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSLambdaVPCAccessExecutionRole"
)
);
const largeMessageBucket = s3.Bucket.fromBucketName(
this,
"LargeMessageBucket",
props.largeMessageBucketName
);
largeMessageBucket.grantReadWrite(handlerRole);
// Handler for FastAPI
const apiHandler = new DockerImageFunction(this, "ApiHandler", {
code: DockerImageCode.fromImageAsset(
path.join(__dirname, "../../backend"),
{
platform: Platform.LINUX_AMD64,
file: "Dockerfile",
}
),
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
memorySize: 1024,
timeout: cdk.Duration.minutes(15),
environment: {
PUBLISHED_API_ID: id.replace("ApiPublishmentStack", ""),
QUEUE_URL: chatQueue.queueUrl,
TABLE_NAME: props.conversationTableName,
CORS_ALLOW_ORIGINS: (props.corsOptions?.allowOrigins ?? ["*"]).join(
","
),
ACCOUNT: Stack.of(this).account,
REGION: Stack.of(this).region,
BEDROCK_REGION: props.bedrockRegion,
LARGE_MESSAGE_BUCKET: props.largeMessageBucketName,
TABLE_ACCESS_ROLE_ARN: props.tableAccessRoleArn,
DB_SECRETS_ARN: props.dbConfigSecretArn,
},
role: handlerRole,
});
// Handler for SQS consumer
const sqsConsumeHandler = new DockerImageFunction(
this,
"SqsConsumeHandler",
{
code: DockerImageCode.fromImageAsset(
path.join(__dirname, "../../backend"),
{
platform: Platform.LINUX_AMD64,
file: "websocket.Dockerfile",
cmd: ["app.sqs_consumer.handler"],
}
),
vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
memorySize: 1024,
timeout: cdk.Duration.minutes(15),
environment: {
PUBLISHED_API_ID: id.replace("ApiPublishmentStack", ""),
QUEUE_URL: chatQueue.queueUrl,
TABLE_NAME: props.conversationTableName,
CORS_ALLOW_ORIGINS: (props.corsOptions?.allowOrigins ?? ["*"]).join(
","
),
ACCOUNT: Stack.of(this).account,
REGION: Stack.of(this).region,
BEDROCK_REGION: props.bedrockRegion,
TABLE_ACCESS_ROLE_ARN: props.tableAccessRoleArn,
DB_SECRETS_ARN: props.dbConfigSecretArn,
},
role: handlerRole,
}
);
sqsConsumeHandler.addEventSource(
new lambdaEventSources.SqsEventSource(chatQueue)
);
chatQueue.grantSendMessages(apiHandler);
chatQueue.grantConsumeMessages(sqsConsumeHandler);
// Allow the handler to access the pgvector.
const dbSg = ec2.SecurityGroup.fromSecurityGroupId(
this,
"DbSecurityGroup",
props.dbSecurityGroupId
);
dbSg.connections.allowFrom(
sqsConsumeHandler,
ec2.Port.tcp(props.dbConfigPort)
);
const api = new apigateway.LambdaRestApi(this, "Api", {
restApiName: id,
handler: apiHandler,
proxy: true,
deployOptions: {
stageName: deploymentStage,
},
defaultMethodOptions: { apiKeyRequired: true },
defaultCorsPreflightOptions: props.corsOptions,
});
const apiKey = api.addApiKey("ApiKey", {
description: "Default api key (Auto generated by CDK)",
});
const usagePlan = api.addUsagePlan("UsagePlan", {
...props.usagePlan,
});
usagePlan.addApiKey(apiKey);
usagePlan.addApiStage({ stage: api.deploymentStage });
const association = new wafv2.CfnWebACLAssociation(
this,
"WebAclAssociation",
{
resourceArn: `arn:aws:apigateway:${this.region}::/restapis/${api.restApiId}/stages/${api.deploymentStage.stageName}`,
webAclArn: props.webAclArn,
}
);
association.addDependency(api.node.defaultChild as cdk.CfnResource);
this.chatQueue = chatQueue;
new CfnOutput(this, "ApiId", {
value: api.restApiId,
});
new CfnOutput(this, "ApiName", {
value: api.restApiName,
});
new CfnOutput(this, "ApiUsagePlanId", {
value: usagePlan.usagePlanId,
});
new CfnOutput(this, "AllowedOrigins", {
value: props.corsOptions?.allowOrigins?.join(",") ?? "*",
});
new CfnOutput(this, "DeploymentStage", {
value: deploymentStage,
});
}
}