packages/blueprints/gen-ai-chatbot/static-assets/chatbot-genai-cdk/lib/constructs/api.ts (239 lines of code) (raw):
import { Construct } from "constructs";
import { CfnOutput, Duration } from "aws-cdk-lib";
import { ITable } from "aws-cdk-lib/aws-dynamodb";
import { HttpLambdaIntegration } from "aws-cdk-lib/aws-apigatewayv2-integrations";
import { HttpUserPoolAuthorizer } from "aws-cdk-lib/aws-apigatewayv2-authorizers";
import {
DockerImageCode,
DockerImageFunction,
IFunction,
} from "aws-cdk-lib/aws-lambda";
import {
CorsHttpMethod,
HttpApi,
HttpMethod,
} from "aws-cdk-lib/aws-apigatewayv2";
import { Auth } from "./auth";
import { Platform } from "aws-cdk-lib/aws-ecr-assets";
import { Stack } from "aws-cdk-lib";
import * as iam from "aws-cdk-lib/aws-iam";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as path from "path";
import { IBucket } from "aws-cdk-lib/aws-s3";
import { ISecret } from "aws-cdk-lib/aws-secretsmanager"
import * as codebuild from "aws-cdk-lib/aws-codebuild";
import { UsageAnalysis } from "./usage-analysis";
export interface ApiProps {
readonly vpc: ec2.IVpc;
readonly database: ITable;
readonly dbSecrets: ISecret;
readonly corsAllowOrigins?: string[];
readonly auth: Auth;
readonly bedrockRegion: string;
readonly tableAccessRole: iam.IRole;
readonly documentBucket: IBucket;
readonly largeMessageBucket: IBucket;
readonly apiPublishProject: codebuild.IProject;
readonly usageAnalysis?: UsageAnalysis;
readonly enableMistral: boolean;
}
export class Api extends Construct {
readonly api: HttpApi;
readonly handler: IFunction;
constructor(scope: Construct, id: string, props: ApiProps) {
super(scope, id);
const {
database,
tableAccessRole,
corsAllowOrigins: allowOrigins = ["*"],
} = props;
const usageAnalysisOutputLocation =
`s3://${props.usageAnalysis?.resultOutputBucket.bucketName}` || "";
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: [tableAccessRole.roleArn],
})
);
handlerRole.addToPolicy(
new iam.PolicyStatement({
actions: ["bedrock:*"],
resources: ["*"],
})
);
handlerRole.addManagedPolicy(
iam.ManagedPolicy.fromAwsManagedPolicyName(
"service-role/AWSLambdaVPCAccessExecutionRole"
)
);
handlerRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["codebuild:StartBuild"],
resources: [props.apiPublishProject.projectArn],
})
);
handlerRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"cloudformation:DescribeStacks",
"cloudformation:DescribeStackEvents",
"cloudformation:DescribeStackResource",
"cloudformation:DescribeStackResources",
"cloudformation:DeleteStack",
],
resources: [`*`],
})
);
handlerRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["codebuild:BatchGetBuilds"],
resources: [props.apiPublishProject.projectArn],
})
);
handlerRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"apigateway:GET",
"apigateway:POST",
"apigateway:PUT",
"apigateway:DELETE",
],
resources: [`arn:aws:apigateway:${Stack.of(this).region}::/*`],
})
);
handlerRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"athena:GetWorkGroup",
"athena:StartQueryExecution",
"athena:StopQueryExecution",
"athena:GetQueryExecution",
"athena:GetQueryResults",
"athena:GetDataCatalog",
],
resources: [props.usageAnalysis?.workgroupArn || ""],
})
);
handlerRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["glue:GetDatabase", "glue:GetDatabases"],
resources: [
props.usageAnalysis?.database.databaseArn || "",
props.usageAnalysis?.database.catalogArn || "",
],
})
);
handlerRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: [
"glue:GetDatabase",
"glue:GetTable",
"glue:GetTables",
"glue:GetPartition",
"glue:GetPartitions",
],
resources: [
props.usageAnalysis?.database.databaseArn || "",
props.usageAnalysis?.database.catalogArn || "",
props.usageAnalysis?.ddbExportTable.tableArn || "",
],
})
);
handlerRole.addToPolicy(
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ["cognito-idp:AdminGetUser"],
resources: [props.auth.userPool.userPoolArn],
})
);
props.usageAnalysis?.resultOutputBucket.grantReadWrite(handlerRole);
props.usageAnalysis?.ddbBucket.grantRead(handlerRole);
props.largeMessageBucket.grantReadWrite(handlerRole);
const handler = new DockerImageFunction(this, "Handler", {
code: DockerImageCode.fromImageAsset(
path.join(__dirname, "../../../backend"),
{
platform: Platform.LINUX_AMD64,
file: "Dockerfile",
}
),
vpc: props.vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
memorySize: 1024,
timeout: Duration.minutes(15),
environment: {
TABLE_NAME: database.tableName,
CORS_ALLOW_ORIGINS: allowOrigins.join(","),
USER_POOL_ID: props.auth.userPool.userPoolId,
CLIENT_ID: props.auth.client.userPoolClientId,
ACCOUNT: Stack.of(this).account,
REGION: Stack.of(this).region,
BEDROCK_REGION: props.bedrockRegion,
TABLE_ACCESS_ROLE_ARN: tableAccessRole.roleArn,
DB_SECRETS_ARN: props.dbSecrets.secretArn,
DOCUMENT_BUCKET: props.documentBucket.bucketName,
LARGE_MESSAGE_BUCKET: props.largeMessageBucket.bucketName,
PUBLISH_API_CODEBUILD_PROJECT_NAME: props.apiPublishProject.projectName,
USAGE_ANALYSIS_DATABASE:
props.usageAnalysis?.database.databaseName || "",
USAGE_ANALYSIS_TABLE:
props.usageAnalysis?.ddbExportTable.tableName || "",
USAGE_ANALYSIS_WORKGROUP: props.usageAnalysis?.workgroupName || "",
USAGE_ANALYSIS_OUTPUT_LOCATION: usageAnalysisOutputLocation,
ENABLE_MISTRAL: props.enableMistral.toString(),
},
role: handlerRole,
});
props.dbSecrets.grantRead(handler)
const api = new HttpApi(this, "Default", {
corsPreflight: {
allowHeaders: ["*"],
allowMethods: [
CorsHttpMethod.GET,
CorsHttpMethod.HEAD,
CorsHttpMethod.OPTIONS,
CorsHttpMethod.POST,
CorsHttpMethod.PUT,
CorsHttpMethod.PATCH,
CorsHttpMethod.DELETE,
],
allowOrigins: allowOrigins,
maxAge: Duration.days(10),
},
});
const integration = new HttpLambdaIntegration("Integration", handler);
const authorizer = new HttpUserPoolAuthorizer(
"Authorizer",
props.auth.userPool,
{
userPoolClients: [props.auth.client],
}
);
let routeProps: any = {
path: "/{proxy+}",
integration,
methods: [
HttpMethod.GET,
HttpMethod.POST,
HttpMethod.PUT,
HttpMethod.PATCH,
HttpMethod.DELETE,
],
authorizer,
};
api.addRoutes(routeProps);
this.api = api;
this.handler = handler;
new CfnOutput(this, "BackendApiUrl", { value: api.apiEndpoint });
}
}