From bd6c8116c8f170aff0f8ec17b93ef2297f3b6c65 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Fri, 7 Mar 2025 19:28:15 +0900 Subject: [PATCH 01/22] feat(cdk): implement typed parameters with Zod for CDK context - Create separate parameter models for each entry point using Zod - Define BaseParametersSchema for common parameters across all entry points - Implement specific parameter schemas for bedrock-chat, api-publish, and bedrock-custom-bot - Add type-safe parameter retrieval functions for each entry point - Simplify parameter handling in entry point files - Ensure proper default values and validation for all parameters --- cdk/bin/api-publish.ts | 72 +++++-------- cdk/bin/bedrock-chat.ts | 76 ++++--------- cdk/bin/bedrock-custom-bot.ts | 46 +++++--- cdk/lib/utils/parameter-models.ts | 172 ++++++++++++++++++++++++++++++ cdk/package-lock.json | 13 ++- cdk/package.json | 3 +- 6 files changed, 260 insertions(+), 122 deletions(-) create mode 100644 cdk/lib/utils/parameter-models.ts diff --git a/cdk/bin/api-publish.ts b/cdk/bin/api-publish.ts index 753376914..b4081ba82 100644 --- a/cdk/bin/api-publish.ts +++ b/cdk/bin/api-publish.ts @@ -3,53 +3,31 @@ import "source-map-support/register"; import * as cdk from "aws-cdk-lib"; import { ApiPublishmentStack } from "../lib/api-publishment-stack"; import * as apigateway from "aws-cdk-lib/aws-apigateway"; +import { getApiPublishParameters } from "../lib/utils/parameter-models"; const app = new cdk.App(); -const BEDROCK_REGION = app.node.tryGetContext("bedrockRegion"); +// Get parameters specific to API publishing +const params = getApiPublishParameters(app); -// Usage plan for the published API -const PUBLISHED_API_THROTTLE_RATE_LIMIT: number | undefined = - app.node.tryGetContext("publishedApiThrottleRateLimit") - ? Number(app.node.tryGetContext("publishedApiThrottleRateLimit")) - : undefined; -const PUBLISHED_API_THROTTLE_BURST_LIMIT: number | undefined = - app.node.tryGetContext("publishedApiThrottleBurstLimit") - ? Number(app.node.tryGetContext("publishedApiThrottleBurstLimit")) - : undefined; -const PUBLISHED_API_QUOTA_LIMIT: number | undefined = app.node.tryGetContext( - "publishedApiQuotaLimit" -) - ? Number(app.node.tryGetContext("publishedApiQuotaLimit")) - : undefined; -const PUBLISHED_API_QUOTA_PERIOD: "DAY" | "WEEK" | "MONTH" | undefined = - app.node.tryGetContext("publishedApiQuotaPeriod") - ? app.node.tryGetContext("publishedApiQuotaPeriod") - : undefined; -const PUBLISHED_API_DEPLOYMENT_STAGE = app.node.tryGetContext( - "publishedApiDeploymentStage" -); -const PUBLISHED_API_ID: string = app.node.tryGetContext("publishedApiId"); -const PUBLISHED_API_ALLOWED_ORIGINS_STRING: string = app.node.tryGetContext( - "publishedApiAllowedOrigins" -); -const PUBLISHED_API_ALLOWED_ORIGINS: string[] = JSON.parse( - PUBLISHED_API_ALLOWED_ORIGINS_STRING || '["*"]' +// Parse allowed origins +const publishedApiAllowedOrigins = JSON.parse( + params.publishedApiAllowedOrigins || '["*"]' ); console.log( - `PUBLISHED_API_THROTTLE_RATE_LIMIT: ${PUBLISHED_API_THROTTLE_RATE_LIMIT}` + `PUBLISHED_API_THROTTLE_RATE_LIMIT: ${params.publishedApiThrottleRateLimit}` ); console.log( - `PUBLISHED_API_THROTTLE_BURST_LIMIT: ${PUBLISHED_API_THROTTLE_BURST_LIMIT}` + `PUBLISHED_API_THROTTLE_BURST_LIMIT: ${params.publishedApiThrottleBurstLimit}` ); -console.log(`PUBLISHED_API_QUOTA_LIMIT: ${PUBLISHED_API_QUOTA_LIMIT}`); -console.log(`PUBLISHED_API_QUOTA_PERIOD: ${PUBLISHED_API_QUOTA_PERIOD}`); +console.log(`PUBLISHED_API_QUOTA_LIMIT: ${params.publishedApiQuotaLimit}`); +console.log(`PUBLISHED_API_QUOTA_PERIOD: ${params.publishedApiQuotaPeriod}`); console.log( - `PUBLISHED_API_DEPLOYMENT_STAGE: ${PUBLISHED_API_DEPLOYMENT_STAGE}` + `PUBLISHED_API_DEPLOYMENT_STAGE: ${params.publishedApiDeploymentStage}` ); -console.log(`PUBLISHED_API_ID: ${PUBLISHED_API_ID}`); -console.log(`PUBLISHED_API_ALLOWED_ORIGINS: ${PUBLISHED_API_ALLOWED_ORIGINS}`); +console.log(`PUBLISHED_API_ID: ${params.publishedApiId}`); +console.log(`PUBLISHED_API_ALLOWED_ORIGINS: ${publishedApiAllowedOrigins}`); const webAclArn = cdk.Fn.importValue("PublishedApiWebAclArn"); @@ -66,37 +44,37 @@ const largeMessageBucketName = cdk.Fn.importValue( // NOTE: DO NOT change the stack id naming rule. const publishedApi = new ApiPublishmentStack( app, - `ApiPublishmentStack${PUBLISHED_API_ID}`, + `ApiPublishmentStack${params.publishedApiId}`, { env: { region: process.env.CDK_DEFAULT_REGION, }, - bedrockRegion: BEDROCK_REGION, + bedrockRegion: params.bedrockRegion, conversationTableName: conversationTableName, tableAccessRoleArn: tableAccessRoleArn, webAclArn: webAclArn, largeMessageBucketName: largeMessageBucketName, usagePlan: { throttle: - PUBLISHED_API_THROTTLE_RATE_LIMIT !== undefined && - PUBLISHED_API_THROTTLE_BURST_LIMIT !== undefined + params.publishedApiThrottleRateLimit !== undefined && + params.publishedApiThrottleBurstLimit !== undefined ? { - rateLimit: PUBLISHED_API_THROTTLE_RATE_LIMIT, - burstLimit: PUBLISHED_API_THROTTLE_BURST_LIMIT, + rateLimit: params.publishedApiThrottleRateLimit, + burstLimit: params.publishedApiThrottleBurstLimit, } : undefined, quota: - PUBLISHED_API_QUOTA_LIMIT !== undefined && - PUBLISHED_API_QUOTA_PERIOD !== undefined + params.publishedApiQuotaLimit !== undefined && + params.publishedApiQuotaPeriod !== undefined ? { - limit: PUBLISHED_API_QUOTA_LIMIT, - period: apigateway.Period[PUBLISHED_API_QUOTA_PERIOD], + limit: params.publishedApiQuotaLimit, + period: apigateway.Period[params.publishedApiQuotaPeriod], } : undefined, }, - deploymentStage: PUBLISHED_API_DEPLOYMENT_STAGE, + deploymentStage: params.publishedApiDeploymentStage, corsOptions: { - allowOrigins: PUBLISHED_API_ALLOWED_ORIGINS, + allowOrigins: publishedApiAllowedOrigins, allowMethods: apigateway.Cors.ALL_METHODS, allowHeaders: apigateway.Cors.DEFAULT_HEADERS, allowCredentials: true, diff --git a/cdk/bin/bedrock-chat.ts b/cdk/bin/bedrock-chat.ts index f3809caf1..962d856df 100644 --- a/cdk/bin/bedrock-chat.ts +++ b/cdk/bin/bedrock-chat.ts @@ -4,46 +4,13 @@ import * as cdk from "aws-cdk-lib"; import { BedrockChatStack } from "../lib/bedrock-chat-stack"; import { BedrockRegionResourcesStack } from "../lib/bedrock-region-resources"; import { FrontendWafStack } from "../lib/frontend-waf-stack"; -import { TIdentityProvider } from "../lib/utils/identity-provider"; import { LogRetentionChecker } from "../rules/log-retention-checker"; +import { getBedrockChatParameters } from "../lib/utils/parameter-models"; const app = new cdk.App(); -const BEDROCK_REGION = app.node.tryGetContext("bedrockRegion"); - -// Allowed IP address ranges for this app itself -const ALLOWED_IP_V4_ADDRESS_RANGES: string[] = app.node.tryGetContext( - "allowedIpV4AddressRanges" -); -const ALLOWED_IP_V6_ADDRESS_RANGES: string[] = app.node.tryGetContext( - "allowedIpV6AddressRanges" -); - -// Allowed IP address ranges for the published API -const PUBLISHED_API_ALLOWED_IP_V4_ADDRESS_RANGES: string[] = - app.node.tryGetContext("publishedApiAllowedIpV4AddressRanges"); -const PUBLISHED_API_ALLOWED_IP_V6_ADDRESS_RANGES: string[] = - app.node.tryGetContext("publishedApiAllowedIpV6AddressRanges"); -const ALLOWED_SIGN_UP_EMAIL_DOMAINS: string[] = app.node.tryGetContext( - "allowedSignUpEmailDomains" -); -const IDENTITY_PROVIDERS: TIdentityProvider[] = - app.node.tryGetContext("identityProviders"); -const USER_POOL_DOMAIN_PREFIX: string = app.node.tryGetContext( - "userPoolDomainPrefix" -); -const AUTO_JOIN_USER_GROUPS: string[] = - app.node.tryGetContext("autoJoinUserGroups"); - -const ENABLE_MISTRAL: boolean = app.node.tryGetContext("enableMistral"); -const SELF_SIGN_UP_ENABLED: boolean = - app.node.tryGetContext("selfSignUpEnabled"); -const USE_STAND_BY_REPLICAS: boolean = - app.node.tryGetContext("enableRagReplicas"); -const ENABLE_BEDROCK_CROSS_REGION_INFERENCE: boolean = app.node.tryGetContext( - "enableBedrockCrossRegionInference" -); -const ENABLE_LAMBDA_SNAPSTART: boolean = app.node.tryGetContext("enableLambdaSnapStart"); +// Get parameters specific to the Bedrock Chat application +const params = getBedrockChatParameters(app); // WAF for frontend // 2023/9: Currently, the WAF for CloudFront needs to be created in the North America region (us-east-1), so the stacks are separated @@ -53,8 +20,8 @@ const waf = new FrontendWafStack(app, `FrontendWafStack`, { // account: process.env.CDK_DEFAULT_ACCOUNT, region: "us-east-1", }, - allowedIpV4AddressRanges: ALLOWED_IP_V4_ADDRESS_RANGES, - allowedIpV6AddressRanges: ALLOWED_IP_V6_ADDRESS_RANGES, + allowedIpV4AddressRanges: params.allowedIpV4AddressRanges, + allowedIpV6AddressRanges: params.allowedIpV6AddressRanges, }); // The region of the LLM model called by the converse API and the region of Guardrail must be in the same region. @@ -67,40 +34,37 @@ const bedrockRegionResources = new BedrockRegionResourcesStack( { env: { // account: process.env.CDK_DEFAULT_ACCOUNT, - region: BEDROCK_REGION, + region: params.bedrockRegion, }, crossRegionReferences: true, } ); -const ALTERNATE_DOMAIN_NAME: string = app.node.tryGetContext("alternateDomainName"); -const HOSTED_ZONE_ID: string = app.node.tryGetContext("hostedZoneId"); - const chat = new BedrockChatStack(app, `BedrockChatStack`, { env: { // account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }, crossRegionReferences: true, - bedrockRegion: BEDROCK_REGION, + bedrockRegion: params.bedrockRegion, webAclId: waf.webAclArn.value, enableIpV6: waf.ipV6Enabled, - identityProviders: IDENTITY_PROVIDERS, - userPoolDomainPrefix: USER_POOL_DOMAIN_PREFIX, + identityProviders: params.identityProviders, + userPoolDomainPrefix: params.userPoolDomainPrefix, publishedApiAllowedIpV4AddressRanges: - PUBLISHED_API_ALLOWED_IP_V4_ADDRESS_RANGES, + params.publishedApiAllowedIpV4AddressRanges, publishedApiAllowedIpV6AddressRanges: - PUBLISHED_API_ALLOWED_IP_V6_ADDRESS_RANGES, - allowedSignUpEmailDomains: ALLOWED_SIGN_UP_EMAIL_DOMAINS, - autoJoinUserGroups: AUTO_JOIN_USER_GROUPS, - enableMistral: ENABLE_MISTRAL, - selfSignUpEnabled: SELF_SIGN_UP_ENABLED, + params.publishedApiAllowedIpV6AddressRanges, + allowedSignUpEmailDomains: params.allowedSignUpEmailDomains, + autoJoinUserGroups: params.autoJoinUserGroups, + enableMistral: params.enableMistral, + selfSignUpEnabled: params.selfSignUpEnabled, documentBucket: bedrockRegionResources.documentBucket, - useStandbyReplicas: USE_STAND_BY_REPLICAS, - enableBedrockCrossRegionInference: ENABLE_BEDROCK_CROSS_REGION_INFERENCE, - enableLambdaSnapStart: ENABLE_LAMBDA_SNAPSTART, - alternateDomainName: ALTERNATE_DOMAIN_NAME, - hostedZoneId: HOSTED_ZONE_ID, + useStandbyReplicas: params.enableRagReplicas, + enableBedrockCrossRegionInference: params.enableBedrockCrossRegionInference, + enableLambdaSnapStart: params.enableLambdaSnapStart, + alternateDomainName: params.alternateDomainName, + hostedZoneId: params.hostedZoneId, }); chat.addDependency(waf); chat.addDependency(bedrockRegionResources); diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index da745e22b..bf1dd9296 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -9,13 +9,13 @@ import { getCrowlingScope, getCrawlingFilters, } from "../lib/utils/bedrock-knowledge-base-args"; -import { - CrawlingFilters, -} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/web-crawler-data-source"; +import { CrawlingFilters } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/web-crawler-data-source"; +import { getBedrockCustomBotParameters } from "../lib/utils/parameter-models"; const app = new cdk.App(); -const BEDROCK_REGION = app.node.tryGetContext("bedrockRegion"); +// Get parameters specific to Bedrock Custom Bot +const params = getBedrockCustomBotParameters(app); const PK: string = process.env.PK!; const SK: string = process.env.SK!; @@ -59,13 +59,17 @@ console.log("existingS3Urls: ", existingS3Urls); console.log("sourceUrls: ", sourceUrls); const embeddingsModel = getEmbeddingModel(knowledgeBase.embeddings_model.S); -const parsingModel = getParsingModel(knowledgeBase.parsing_model.S) -const crawlingScope = getCrowlingScope(knowledgeBase.web_crawling_scope.S) -const crawlingFilters: CrawlingFilters = getCrawlingFilters(knowledgeBase.web_crawling_filters.M) -const existKnowledgeBaseId: string | undefined = knowledgeBase.exist_knowledge_base_id.S +const parsingModel = getParsingModel(knowledgeBase.parsing_model.S); +const crawlingScope = getCrowlingScope(knowledgeBase.web_crawling_scope.S); +const crawlingFilters: CrawlingFilters = getCrawlingFilters( + knowledgeBase.web_crawling_filters.M +); +const existKnowledgeBaseId: string | undefined = knowledgeBase + .exist_knowledge_base_id.S ? knowledgeBase.exist_knowledge_base_id.S : undefined; -const maxTokens: number | undefined = knowledgeBase.chunking_configuration.M.max_tokens +const maxTokens: number | undefined = knowledgeBase.chunking_configuration.M + .max_tokens ? Number(knowledgeBase.chunking_configuration.M.max_tokens.N) : undefined; const instruction: string | undefined = knowledgeBase.instruction @@ -74,23 +78,31 @@ const instruction: string | undefined = knowledgeBase.instruction const analyzer = knowledgeBase.open_search.M.analyzer.M ? getAnalyzer(knowledgeBase.open_search.M.analyzer.M) : undefined; -const overlapPercentage: number | undefined = knowledgeBase.chunking_configuration.M.overlap_percentage +const overlapPercentage: number | undefined = knowledgeBase + .chunking_configuration.M.overlap_percentage ? Number(knowledgeBase.chunking_configuration.M.overlap_percentage.N) : undefined; -const overlapTokens: number | undefined = knowledgeBase.chunking_configuration.M.overlap_tokens +const overlapTokens: number | undefined = knowledgeBase.chunking_configuration.M + .overlap_tokens ? Number(knowledgeBase.chunking_configuration.M.overlap_tokens.N) : undefined; -const maxParentTokenSize: number | undefined = knowledgeBase.chunking_configuration.M.max_parent_token_size +const maxParentTokenSize: number | undefined = knowledgeBase + .chunking_configuration.M.max_parent_token_size ? Number(knowledgeBase.chunking_configuration.M.max_parent_token_size.N) : undefined; -const maxChildTokenSize: number | undefined = knowledgeBase.chunking_configuration.M.max_child_token_size +const maxChildTokenSize: number | undefined = knowledgeBase + .chunking_configuration.M.max_child_token_size ? Number(knowledgeBase.chunking_configuration.M.max_child_token_size.N) : undefined; -const bufferSize: number | undefined = knowledgeBase.chunking_configuration.M.buffer_size +const bufferSize: number | undefined = knowledgeBase.chunking_configuration.M + .buffer_size ? Number(knowledgeBase.chunking_configuration.M.buffer_size.N) : undefined; -const breakpointPercentileThreshold: number | undefined = knowledgeBase.chunking_configuration.M.breakpoint_percentile_threshold - ? Number(knowledgeBase.chunking_configuration.M.breakpoint_percentile_threshold.N) +const breakpointPercentileThreshold: number | undefined = knowledgeBase + .chunking_configuration.M.breakpoint_percentile_threshold + ? Number( + knowledgeBase.chunking_configuration.M.breakpoint_percentile_threshold.N + ) : undefined; const is_guardrail_enabled: boolean | undefined = guardrails.is_guardrail_enabled @@ -171,7 +183,7 @@ const bedrockCustomBotStack = new BedrockCustomBotStack( { env: { // account: process.env.CDK_DEFAULT_ACCOUNT, - region: BEDROCK_REGION, + region: params.bedrockRegion, }, ownerUserId, botId, diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts new file mode 100644 index 000000000..bd1a2dc72 --- /dev/null +++ b/cdk/lib/utils/parameter-models.ts @@ -0,0 +1,172 @@ +import { z } from "zod"; +import { TIdentityProvider } from "./identity-provider"; + +/** + * Base parameters schema that is common across all entry points + */ +export const BaseParametersSchema = z.object({ + // Bedrock configuration + bedrockRegion: z.string().default("us-east-1"), +}); + +/** + * Parameters schema for the main Bedrock Chat application + */ +export const BedrockChatParametersSchema = BaseParametersSchema.extend({ + // Bedrock configuration + enableMistral: z.boolean().default(false), + enableBedrockCrossRegionInference: z.boolean().default(true), + + // IP address restrictions + allowedIpV4AddressRanges: z + .array(z.string()) + .default(["0.0.0.0/1", "128.0.0.0/1"]), + allowedIpV6AddressRanges: z + .array(z.string()) + .default([ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ]), + publishedApiAllowedIpV4AddressRanges: z + .array(z.string()) + .default(["0.0.0.0/1", "128.0.0.0/1"]), + publishedApiAllowedIpV6AddressRanges: z + .array(z.string()) + .default([ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ]), + + // Authentication and user management + identityProviders: z.array(z.custom()).default([]), + userPoolDomainPrefix: z.string().default(""), + allowedSignUpEmailDomains: z.array(z.string()).default([]), + autoJoinUserGroups: z.array(z.string()).default(["CreatingBotAllowed"]), + selfSignUpEnabled: z.boolean().default(true), + + // Performance and availability + enableRagReplicas: z.boolean().default(true), + enableLambdaSnapStart: z.boolean().default(true), + + // Custom domain configuration + alternateDomainName: z.string().default(""), + hostedZoneId: z.string().default(""), +}); + +/** + * Parameters schema for API publishing + */ +export const ApiPublishParametersSchema = BaseParametersSchema.extend({ + // API publishing configuration + publishedApiThrottleRateLimit: z.number().optional(), + publishedApiThrottleBurstLimit: z.number().optional(), + publishedApiQuotaLimit: z.number().optional(), + publishedApiQuotaPeriod: z.enum(["DAY", "WEEK", "MONTH"]).optional(), + publishedApiDeploymentStage: z.string().optional(), + publishedApiId: z.string().optional(), + publishedApiAllowedOrigins: z.string().default('["*"]'), +}); + +/** + * Parameters schema for Bedrock Custom Bot + */ +export const BedrockCustomBotParametersSchema = BaseParametersSchema; + +/** + * Type definitions for each parameter set + */ +export type BaseParameters = z.infer; +export type BedrockChatParameters = z.infer; +export type ApiPublishParameters = z.infer; +export type BedrockCustomBotParameters = z.infer< + typeof BedrockCustomBotParametersSchema +>; + +/** + * Parse and validate CDK context parameters for the main Bedrock Chat application + * @param app CDK App instance + * @returns Validated parameters object + */ +export function getBedrockChatParameters(app: any): BedrockChatParameters { + const contextParams = { + bedrockRegion: app.node.tryGetContext("bedrockRegion"), + enableMistral: app.node.tryGetContext("enableMistral"), + allowedIpV4AddressRanges: app.node.tryGetContext( + "allowedIpV4AddressRanges" + ), + allowedIpV6AddressRanges: app.node.tryGetContext( + "allowedIpV6AddressRanges" + ), + identityProviders: app.node.tryGetContext("identityProviders"), + userPoolDomainPrefix: app.node.tryGetContext("userPoolDomainPrefix"), + allowedSignUpEmailDomains: app.node.tryGetContext( + "allowedSignUpEmailDomains" + ), + autoJoinUserGroups: app.node.tryGetContext("autoJoinUserGroups"), + selfSignUpEnabled: app.node.tryGetContext("selfSignUpEnabled"), + publishedApiAllowedIpV4AddressRanges: app.node.tryGetContext( + "publishedApiAllowedIpV4AddressRanges" + ), + publishedApiAllowedIpV6AddressRanges: app.node.tryGetContext( + "publishedApiAllowedIpV6AddressRanges" + ), + enableRagReplicas: app.node.tryGetContext("enableRagReplicas"), + enableBedrockCrossRegionInference: app.node.tryGetContext( + "enableBedrockCrossRegionInference" + ), + enableLambdaSnapStart: app.node.tryGetContext("enableLambdaSnapStart"), + alternateDomainName: app.node.tryGetContext("alternateDomainName"), + hostedZoneId: app.node.tryGetContext("hostedZoneId"), + }; + + return BedrockChatParametersSchema.parse(contextParams); +} + +/** + * Parse and validate CDK context parameters for API publishing + * @param app CDK App instance + * @returns Validated parameters object + */ +export function getApiPublishParameters(app: any): ApiPublishParameters { + const contextParams = { + bedrockRegion: app.node.tryGetContext("bedrockRegion"), + publishedApiThrottleRateLimit: app.node.tryGetContext( + "publishedApiThrottleRateLimit" + ) + ? Number(app.node.tryGetContext("publishedApiThrottleRateLimit")) + : undefined, + publishedApiThrottleBurstLimit: app.node.tryGetContext( + "publishedApiThrottleBurstLimit" + ) + ? Number(app.node.tryGetContext("publishedApiThrottleBurstLimit")) + : undefined, + publishedApiQuotaLimit: app.node.tryGetContext("publishedApiQuotaLimit") + ? Number(app.node.tryGetContext("publishedApiQuotaLimit")) + : undefined, + publishedApiQuotaPeriod: app.node.tryGetContext("publishedApiQuotaPeriod"), + publishedApiDeploymentStage: app.node.tryGetContext( + "publishedApiDeploymentStage" + ), + publishedApiId: app.node.tryGetContext("publishedApiId"), + publishedApiAllowedOrigins: app.node.tryGetContext( + "publishedApiAllowedOrigins" + ), + }; + + return ApiPublishParametersSchema.parse(contextParams); +} + +/** + * Parse and validate CDK context parameters for Bedrock Custom Bot + * @param app CDK App instance + * @returns Validated parameters object + */ +export function getBedrockCustomBotParameters( + app: any +): BedrockCustomBotParameters { + const contextParams = { + bedrockRegion: app.node.tryGetContext("bedrockRegion"), + }; + + return BedrockCustomBotParametersSchema.parse(contextParams); +} diff --git a/cdk/package-lock.json b/cdk/package-lock.json index 783ce47e9..bf0d18b00 100644 --- a/cdk/package-lock.json +++ b/cdk/package-lock.json @@ -30,7 +30,8 @@ "jest": "^29.6.1", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", - "typescript": "~5.1.6" + "typescript": "~5.1.6", + "zod": "^3.24.2" } }, "node_modules/@ampproject/remapping": { @@ -5769,6 +5770,16 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.24.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", + "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/cdk/package.json b/cdk/package.json index 97086bbc8..325eeb45f 100644 --- a/cdk/package.json +++ b/cdk/package.json @@ -18,7 +18,8 @@ "jest": "^29.6.1", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", - "typescript": "~5.1.6" + "typescript": "~5.1.6", + "zod": "^3.24.2" }, "dependencies": { "@aws-cdk/aws-glue-alpha": "^2.155.0-alpha.0", From 722b2ddd323347984ce6bcc751cfadc5e6ed058d Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Fri, 7 Mar 2025 19:41:53 +0900 Subject: [PATCH 02/22] feat(cdk): add environment-specific parameter support Add ability to specify environment name when getting Bedrock chat parameters. When envName is provided, parameters are fetched from bedrockChatParams map. If envName is undefined, 'default' is used. If 'default' is not found in the map, CDK context values are used. For non-default environments, an error is thrown if not found in the map. --- cdk/lib/utils/parameter-models.ts | 82 +++++++++++++++++++------------ cdk/parameter.ts | 3 ++ 2 files changed, 53 insertions(+), 32 deletions(-) create mode 100644 cdk/parameter.ts diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index bd1a2dc72..7fb1992cd 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -85,41 +85,59 @@ export type BedrockCustomBotParameters = z.infer< /** * Parse and validate CDK context parameters for the main Bedrock Chat application * @param app CDK App instance + * @param envName Optional environment name to use for parameter lookup * @returns Validated parameters object */ -export function getBedrockChatParameters(app: any): BedrockChatParameters { - const contextParams = { - bedrockRegion: app.node.tryGetContext("bedrockRegion"), - enableMistral: app.node.tryGetContext("enableMistral"), - allowedIpV4AddressRanges: app.node.tryGetContext( - "allowedIpV4AddressRanges" - ), - allowedIpV6AddressRanges: app.node.tryGetContext( - "allowedIpV6AddressRanges" - ), - identityProviders: app.node.tryGetContext("identityProviders"), - userPoolDomainPrefix: app.node.tryGetContext("userPoolDomainPrefix"), - allowedSignUpEmailDomains: app.node.tryGetContext( - "allowedSignUpEmailDomains" - ), - autoJoinUserGroups: app.node.tryGetContext("autoJoinUserGroups"), - selfSignUpEnabled: app.node.tryGetContext("selfSignUpEnabled"), - publishedApiAllowedIpV4AddressRanges: app.node.tryGetContext( - "publishedApiAllowedIpV4AddressRanges" - ), - publishedApiAllowedIpV6AddressRanges: app.node.tryGetContext( - "publishedApiAllowedIpV6AddressRanges" - ), - enableRagReplicas: app.node.tryGetContext("enableRagReplicas"), - enableBedrockCrossRegionInference: app.node.tryGetContext( - "enableBedrockCrossRegionInference" - ), - enableLambdaSnapStart: app.node.tryGetContext("enableLambdaSnapStart"), - alternateDomainName: app.node.tryGetContext("alternateDomainName"), - hostedZoneId: app.node.tryGetContext("hostedZoneId"), - }; +export function getBedrockChatParameters(app: any, envName?: string): BedrockChatParameters { + // Use 'default' if envName is undefined + const environment = envName || 'default'; + + // Import bedrockChatParams from parameter.ts + const { bedrockChatParams } = require('../../parameter'); + + // If environment parameters exist in bedrockChatParams, use them + if (bedrockChatParams.has(environment)) { + return bedrockChatParams.get(environment)!; + } + + // If environment is 'default' and not found in bedrockChatParams, use context values + if (environment === 'default') { + const contextParams = { + bedrockRegion: app.node.tryGetContext("bedrockRegion"), + enableMistral: app.node.tryGetContext("enableMistral"), + allowedIpV4AddressRanges: app.node.tryGetContext( + "allowedIpV4AddressRanges" + ), + allowedIpV6AddressRanges: app.node.tryGetContext( + "allowedIpV6AddressRanges" + ), + identityProviders: app.node.tryGetContext("identityProviders"), + userPoolDomainPrefix: app.node.tryGetContext("userPoolDomainPrefix"), + allowedSignUpEmailDomains: app.node.tryGetContext( + "allowedSignUpEmailDomains" + ), + autoJoinUserGroups: app.node.tryGetContext("autoJoinUserGroups"), + selfSignUpEnabled: app.node.tryGetContext("selfSignUpEnabled"), + publishedApiAllowedIpV4AddressRanges: app.node.tryGetContext( + "publishedApiAllowedIpV4AddressRanges" + ), + publishedApiAllowedIpV6AddressRanges: app.node.tryGetContext( + "publishedApiAllowedIpV6AddressRanges" + ), + enableRagReplicas: app.node.tryGetContext("enableRagReplicas"), + enableBedrockCrossRegionInference: app.node.tryGetContext( + "enableBedrockCrossRegionInference" + ), + enableLambdaSnapStart: app.node.tryGetContext("enableLambdaSnapStart"), + alternateDomainName: app.node.tryGetContext("alternateDomainName"), + hostedZoneId: app.node.tryGetContext("hostedZoneId"), + }; - return BedrockChatParametersSchema.parse(contextParams); + return BedrockChatParametersSchema.parse(contextParams); + } + + // If environment is not 'default' and not found in bedrockChatParams, throw an error + throw new Error(`Environment '${environment}' not found in bedrockChatParams`); } /** diff --git a/cdk/parameter.ts b/cdk/parameter.ts new file mode 100644 index 000000000..cce6d7df5 --- /dev/null +++ b/cdk/parameter.ts @@ -0,0 +1,3 @@ +import { BedrockChatParameters } from "./lib/utils/parameter-models"; + +export const bedrockChatParams = new Map(); From 1ce5773e1ce028277268e24e2c1b0d0c08c2ff1d Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Fri, 7 Mar 2025 20:58:40 +0900 Subject: [PATCH 03/22] feat(cdk): improve parameter model type definitions for better developer experience - Use z.input for input types to allow optional parameters with defaults - Use z.infer for output types to ensure all properties are required - Remove unnecessary .optional() calls from properties with defaults - Update parameter.ts to use the new input type - Ensure getBedrockChatParameters returns fully resolved parameters This change allows users to pass empty objects to bedrockChatParams while maintaining strong typing for the returned parameters from get*Parameters functions. --- cdk/lib/utils/parameter-models.ts | 38 +++++++++++++++++++------------ cdk/parameter.ts | 9 ++++++-- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index 7fb1992cd..fe45c6270 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -75,12 +75,17 @@ export const BedrockCustomBotParametersSchema = BaseParametersSchema; /** * Type definitions for each parameter set */ +// Input types (for user input, default values are optional) +export type BaseParametersInput = z.input; +export type BedrockChatParametersInput = z.input; +export type ApiPublishParametersInput = z.input; +export type BedrockCustomBotParametersInput = z.input; + +// Output types (for function returns, all properties are required) export type BaseParameters = z.infer; export type BedrockChatParameters = z.infer; export type ApiPublishParameters = z.infer; -export type BedrockCustomBotParameters = z.infer< - typeof BedrockCustomBotParametersSchema ->; +export type BedrockCustomBotParameters = z.infer; /** * Parse and validate CDK context parameters for the main Bedrock Chat application @@ -88,20 +93,23 @@ export type BedrockCustomBotParameters = z.infer< * @param envName Optional environment name to use for parameter lookup * @returns Validated parameters object */ -export function getBedrockChatParameters(app: any, envName?: string): BedrockChatParameters { +export function getBedrockChatParameters( + app: any, + envName?: string +): BedrockChatParameters { // Use 'default' if envName is undefined - const environment = envName || 'default'; - + const environment = envName || "default"; + // Import bedrockChatParams from parameter.ts - const { bedrockChatParams } = require('../../parameter'); - + const { bedrockChatParams } = require("../../parameter"); + // If environment parameters exist in bedrockChatParams, use them if (bedrockChatParams.has(environment)) { - return bedrockChatParams.get(environment)!; + return BedrockChatParametersSchema.parse(bedrockChatParams.get(environment)!); } - + // If environment is 'default' and not found in bedrockChatParams, use context values - if (environment === 'default') { + if (environment === "default") { const contextParams = { bedrockRegion: app.node.tryGetContext("bedrockRegion"), enableMistral: app.node.tryGetContext("enableMistral"), @@ -135,9 +143,11 @@ export function getBedrockChatParameters(app: any, envName?: string): BedrockCha return BedrockChatParametersSchema.parse(contextParams); } - + // If environment is not 'default' and not found in bedrockChatParams, throw an error - throw new Error(`Environment '${environment}' not found in bedrockChatParams`); + throw new Error( + `Environment '${environment}' not found in bedrockChatParams` + ); } /** @@ -187,4 +197,4 @@ export function getBedrockCustomBotParameters( }; return BedrockCustomBotParametersSchema.parse(contextParams); -} +} \ No newline at end of file diff --git a/cdk/parameter.ts b/cdk/parameter.ts index cce6d7df5..ea1e55192 100644 --- a/cdk/parameter.ts +++ b/cdk/parameter.ts @@ -1,3 +1,8 @@ -import { BedrockChatParameters } from "./lib/utils/parameter-models"; +import { BedrockChatParametersInput } from "./lib/utils/parameter-models"; -export const bedrockChatParams = new Map(); +export const bedrockChatParams = new Map(); +// You can define multiple environments and their parameters here +// bedrockChatParams.set("dev", {}); + +// If you define "default" environment here, parameters in cdk.json are ignored +// bedrockChatParams.set("default", {}); From e3ff14e897549dcb716ae685fcd41708b1f14a46 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Sun, 9 Mar 2025 21:11:48 +0900 Subject: [PATCH 04/22] refactor(cdk): improve parameter resolution for multi-environment deployments - Rename parameter getter functions to better reflect their purpose (get* -> resolve*) - Add explicit App type for better type safety - Improve handling of array parameters with proper null checks - Add default value for publishedApiAllowedOrigins - Add unit tests for parameter resolution functions --- cdk/lib/utils/parameter-models.ts | 186 ++++++---- cdk/test/utils/parameter-models.test.ts | 475 ++++++++++++++++++++++++ 2 files changed, 581 insertions(+), 80 deletions(-) create mode 100644 cdk/test/utils/parameter-models.test.ts diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index fe45c6270..87e187a12 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -1,10 +1,11 @@ import { z } from "zod"; import { TIdentityProvider } from "./identity-provider"; +import { App } from "aws-cdk-lib"; /** * Base parameters schema that is common across all entry points */ -export const BaseParametersSchema = z.object({ +const BaseParametersSchema = z.object({ // Bedrock configuration bedrockRegion: z.string().default("us-east-1"), }); @@ -12,7 +13,7 @@ export const BaseParametersSchema = z.object({ /** * Parameters schema for the main Bedrock Chat application */ -export const BedrockChatParametersSchema = BaseParametersSchema.extend({ +const BedrockChatParametersSchema = BaseParametersSchema.extend({ // Bedrock configuration enableMistral: z.boolean().default(false), enableBedrockCrossRegionInference: z.boolean().default(true), @@ -56,7 +57,7 @@ export const BedrockChatParametersSchema = BaseParametersSchema.extend({ /** * Parameters schema for API publishing */ -export const ApiPublishParametersSchema = BaseParametersSchema.extend({ +const ApiPublishParametersSchema = BaseParametersSchema.extend({ // API publishing configuration publishedApiThrottleRateLimit: z.number().optional(), publishedApiThrottleBurstLimit: z.number().optional(), @@ -70,131 +71,156 @@ export const ApiPublishParametersSchema = BaseParametersSchema.extend({ /** * Parameters schema for Bedrock Custom Bot */ -export const BedrockCustomBotParametersSchema = BaseParametersSchema; +const BedrockCustomBotParametersSchema = BaseParametersSchema; /** * Type definitions for each parameter set */ // Input types (for user input, default values are optional) export type BaseParametersInput = z.input; -export type BedrockChatParametersInput = z.input; -export type ApiPublishParametersInput = z.input; -export type BedrockCustomBotParametersInput = z.input; +export type BedrockChatParametersInput = z.input< + typeof BedrockChatParametersSchema +>; +export type ApiPublishParametersInput = z.input< + typeof ApiPublishParametersSchema +>; +export type BedrockCustomBotParametersInput = z.input< + typeof BedrockCustomBotParametersSchema +>; // Output types (for function returns, all properties are required) export type BaseParameters = z.infer; export type BedrockChatParameters = z.infer; export type ApiPublishParameters = z.infer; -export type BedrockCustomBotParameters = z.infer; +export type BedrockCustomBotParameters = z.infer< + typeof BedrockCustomBotParametersSchema +>; /** - * Parse and validate CDK context parameters for the main Bedrock Chat application + * Parse and validate parameters for the main Bedrock Chat application * @param app CDK App instance - * @param envName Optional environment name to use for parameter lookup + * @param parametersInput Optional input parameters that override context values * @returns Validated parameters object */ -export function getBedrockChatParameters( - app: any, - envName?: string +export function resolveBedrockChatParameters( + app: App, + parametersInput?: BedrockChatParametersInput ): BedrockChatParameters { - // Use 'default' if envName is undefined - const environment = envName || "default"; - - // Import bedrockChatParams from parameter.ts - const { bedrockChatParams } = require("../../parameter"); - - // If environment parameters exist in bedrockChatParams, use them - if (bedrockChatParams.has(environment)) { - return BedrockChatParametersSchema.parse(bedrockChatParams.get(environment)!); + // If parametersInput is provided, use it directly + if (parametersInput) { + return BedrockChatParametersSchema.parse(parametersInput); } - // If environment is 'default' and not found in bedrockChatParams, use context values - if (environment === "default") { - const contextParams = { - bedrockRegion: app.node.tryGetContext("bedrockRegion"), - enableMistral: app.node.tryGetContext("enableMistral"), - allowedIpV4AddressRanges: app.node.tryGetContext( - "allowedIpV4AddressRanges" - ), - allowedIpV6AddressRanges: app.node.tryGetContext( - "allowedIpV6AddressRanges" - ), - identityProviders: app.node.tryGetContext("identityProviders"), - userPoolDomainPrefix: app.node.tryGetContext("userPoolDomainPrefix"), - allowedSignUpEmailDomains: app.node.tryGetContext( - "allowedSignUpEmailDomains" - ), - autoJoinUserGroups: app.node.tryGetContext("autoJoinUserGroups"), - selfSignUpEnabled: app.node.tryGetContext("selfSignUpEnabled"), - publishedApiAllowedIpV4AddressRanges: app.node.tryGetContext( - "publishedApiAllowedIpV4AddressRanges" - ), - publishedApiAllowedIpV6AddressRanges: app.node.tryGetContext( - "publishedApiAllowedIpV6AddressRanges" - ), - enableRagReplicas: app.node.tryGetContext("enableRagReplicas"), - enableBedrockCrossRegionInference: app.node.tryGetContext( - "enableBedrockCrossRegionInference" - ), - enableLambdaSnapStart: app.node.tryGetContext("enableLambdaSnapStart"), - alternateDomainName: app.node.tryGetContext("alternateDomainName"), - hostedZoneId: app.node.tryGetContext("hostedZoneId"), - }; - - return BedrockChatParametersSchema.parse(contextParams); - } + // Otherwise, get parameters from context + const identityProviders = app.node.tryGetContext("identityProviders"); - // If environment is not 'default' and not found in bedrockChatParams, throw an error - throw new Error( - `Environment '${environment}' not found in bedrockChatParams` - ); + const contextParams = { + bedrockRegion: app.node.tryGetContext("bedrockRegion"), + enableMistral: app.node.tryGetContext("enableMistral"), + allowedIpV4AddressRanges: app.node.tryGetContext( + "allowedIpV4AddressRanges" + ), + allowedIpV6AddressRanges: app.node.tryGetContext( + "allowedIpV6AddressRanges" + ), + // 配列でない場合は空配列を使用 + identityProviders: Array.isArray(identityProviders) + ? identityProviders + : [], + userPoolDomainPrefix: app.node.tryGetContext("userPoolDomainPrefix"), + allowedSignUpEmailDomains: app.node.tryGetContext( + "allowedSignUpEmailDomains" + ), + autoJoinUserGroups: app.node.tryGetContext("autoJoinUserGroups"), + selfSignUpEnabled: app.node.tryGetContext("selfSignUpEnabled"), + publishedApiAllowedIpV4AddressRanges: app.node.tryGetContext( + "publishedApiAllowedIpV4AddressRanges" + ), + publishedApiAllowedIpV6AddressRanges: app.node.tryGetContext( + "publishedApiAllowedIpV6AddressRanges" + ), + enableRagReplicas: app.node.tryGetContext("enableRagReplicas"), + enableBedrockCrossRegionInference: app.node.tryGetContext( + "enableBedrockCrossRegionInference" + ), + enableLambdaSnapStart: app.node.tryGetContext("enableLambdaSnapStart"), + alternateDomainName: app.node.tryGetContext("alternateDomainName"), + hostedZoneId: app.node.tryGetContext("hostedZoneId"), + }; + + return BedrockChatParametersSchema.parse(contextParams); } /** - * Parse and validate CDK context parameters for API publishing + * Parse and validate parameters for API publishing * @param app CDK App instance + * @param parametersInput Optional input parameters that override context values * @returns Validated parameters object */ -export function getApiPublishParameters(app: any): ApiPublishParameters { +export function resolveApiPublishParameters( + app: App, + parametersInput?: ApiPublishParametersInput +): ApiPublishParameters { + // If parametersInput is provided, use it directly + if (parametersInput) { + return ApiPublishParametersSchema.parse(parametersInput); + } + + // Otherwise, get parameters from context + const publishedApiThrottleRateLimit = app.node.tryGetContext( + "publishedApiThrottleRateLimit" + ); + const publishedApiThrottleBurstLimit = app.node.tryGetContext( + "publishedApiThrottleBurstLimit" + ); + const publishedApiQuotaLimit = app.node.tryGetContext( + "publishedApiQuotaLimit" + ); + const publishedApiAllowedOrigins = app.node.tryGetContext( + "publishedApiAllowedOrigins" + ); + const contextParams = { bedrockRegion: app.node.tryGetContext("bedrockRegion"), - publishedApiThrottleRateLimit: app.node.tryGetContext( - "publishedApiThrottleRateLimit" - ) - ? Number(app.node.tryGetContext("publishedApiThrottleRateLimit")) + publishedApiThrottleRateLimit: publishedApiThrottleRateLimit + ? Number(publishedApiThrottleRateLimit) : undefined, - publishedApiThrottleBurstLimit: app.node.tryGetContext( - "publishedApiThrottleBurstLimit" - ) - ? Number(app.node.tryGetContext("publishedApiThrottleBurstLimit")) + publishedApiThrottleBurstLimit: publishedApiThrottleBurstLimit + ? Number(publishedApiThrottleBurstLimit) : undefined, - publishedApiQuotaLimit: app.node.tryGetContext("publishedApiQuotaLimit") - ? Number(app.node.tryGetContext("publishedApiQuotaLimit")) + publishedApiQuotaLimit: publishedApiQuotaLimit + ? Number(publishedApiQuotaLimit) : undefined, publishedApiQuotaPeriod: app.node.tryGetContext("publishedApiQuotaPeriod"), publishedApiDeploymentStage: app.node.tryGetContext( "publishedApiDeploymentStage" ), publishedApiId: app.node.tryGetContext("publishedApiId"), - publishedApiAllowedOrigins: app.node.tryGetContext( - "publishedApiAllowedOrigins" - ), + publishedApiAllowedOrigins: publishedApiAllowedOrigins || '["*"]', }; return ApiPublishParametersSchema.parse(contextParams); } /** - * Parse and validate CDK context parameters for Bedrock Custom Bot + * Parse and validate parameters for Bedrock Custom Bot * @param app CDK App instance + * @param parametersInput Optional input parameters that override context values * @returns Validated parameters object */ -export function getBedrockCustomBotParameters( - app: any +export function resolveBedrockCustomBotParameters( + app: App, + parametersInput?: BedrockCustomBotParametersInput ): BedrockCustomBotParameters { + // If parametersInput is provided, use it directly + if (parametersInput) { + return BedrockCustomBotParametersSchema.parse(parametersInput); + } + + // Otherwise, get parameters from context const contextParams = { bedrockRegion: app.node.tryGetContext("bedrockRegion"), }; return BedrockCustomBotParametersSchema.parse(contextParams); -} \ No newline at end of file +} diff --git a/cdk/test/utils/parameter-models.test.ts b/cdk/test/utils/parameter-models.test.ts new file mode 100644 index 000000000..e457dfc9e --- /dev/null +++ b/cdk/test/utils/parameter-models.test.ts @@ -0,0 +1,475 @@ +import { App } from "aws-cdk-lib"; +import { + resolveBedrockChatParameters, + resolveApiPublishParameters, + resolveBedrockCustomBotParameters +} from "../../lib/utils/parameter-models"; +import { ZodError } from "zod"; + +describe("resolveBedrockChatParameters", () => { + describe("パラメータソースの選択", () => { + test("parametersInputが指定されている場合、それが使用される", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + const inputParams = { + bedrockRegion: "eu-west-1", + enableMistral: true, + }; + + // Act + const result = resolveBedrockChatParameters(app, inputParams); + + // Assert + expect(result.bedrockRegion).toBe("eu-west-1"); + expect(result.enableMistral).toBe(true); + }); + + test("parametersInputが未指定の場合、コンテキストからパラメータが取得される", () => { + // Arrange + const app = new App({ + autoSynth: false, + context: { + bedrockRegion: "ap-northeast-1", + enableMistral: true, + }, + }); + + // Act + const result = resolveBedrockChatParameters(app); + + // Assert + expect(result.bedrockRegion).toBe("ap-northeast-1"); + expect(result.enableMistral).toBe(true); + }); + }); + + describe("パラメータのバリデーション", () => { + test("必須パラメータが欠けている場合でも、デフォルト値が適用される", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + + // Act + const result = resolveBedrockChatParameters(app); + + // Assert + expect(result.bedrockRegion).toBe("us-east-1"); // デフォルト値 + expect(result.enableMistral).toBe(false); // デフォルト値 + expect(result.allowedIpV4AddressRanges).toEqual([ + "0.0.0.0/1", + "128.0.0.0/1", + ]); // デフォルト値 + }); + + test("すべてのパラメータが指定されている場合、正しく解析される", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + const inputParams = { + bedrockRegion: "us-west-2", + enableMistral: true, + allowedIpV4AddressRanges: ["192.168.0.0/16"], + allowedIpV6AddressRanges: ["2001:db8::/32"], + identityProviders: [{ service: "google", secretName: "GoogleSecret" }], + userPoolDomainPrefix: "my-app", + allowedSignUpEmailDomains: ["example.com"], + autoJoinUserGroups: ["Users"], + selfSignUpEnabled: false, + publishedApiAllowedIpV4AddressRanges: ["10.0.0.0/8"], + publishedApiAllowedIpV6AddressRanges: ["2001:db8:1::/48"], + enableRagReplicas: false, + enableBedrockCrossRegionInference: false, + enableLambdaSnapStart: false, + alternateDomainName: "chat.example.com", + hostedZoneId: "Z1234567890", + }; + + // Act + const result = resolveBedrockChatParameters(app, inputParams); + + // Assert + expect(result.bedrockRegion).toBe("us-west-2"); + expect(result.enableMistral).toBe(true); + expect(result.allowedIpV4AddressRanges).toEqual(["192.168.0.0/16"]); + expect(result.allowedIpV6AddressRanges).toEqual(["2001:db8::/32"]); + expect(result.identityProviders).toEqual([ + { service: "google", secretName: "GoogleSecret" }, + ]); + expect(result.userPoolDomainPrefix).toBe("my-app"); + expect(result.allowedSignUpEmailDomains).toEqual(["example.com"]); + expect(result.autoJoinUserGroups).toEqual(["Users"]); + expect(result.selfSignUpEnabled).toBe(false); + expect(result.publishedApiAllowedIpV4AddressRanges).toEqual([ + "10.0.0.0/8", + ]); + expect(result.publishedApiAllowedIpV6AddressRanges).toEqual([ + "2001:db8:1::/48", + ]); + expect(result.enableRagReplicas).toBe(false); + expect(result.enableBedrockCrossRegionInference).toBe(false); + expect(result.enableLambdaSnapStart).toBe(false); + expect(result.alternateDomainName).toBe("chat.example.com"); + expect(result.hostedZoneId).toBe("Z1234567890"); + }); + + test("無効なパラメータが指定された場合、ZodErrorがスローされる", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + const invalidParams = { + bedrockRegion: 123, // 文字列ではなく数値 + }; + + // Act & Assert + expect(() => { + resolveBedrockChatParameters(app, invalidParams as any); + }).toThrow(ZodError); + }); + }); + + describe("特殊なパラメータ処理", () => { + test("配列パラメータが正しく処理される", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + const inputParams = { + allowedIpV4AddressRanges: ["192.168.1.0/24", "10.0.0.0/8"], + allowedSignUpEmailDomains: ["example.com", "test.com"], + }; + + // Act + const result = resolveBedrockChatParameters(app, inputParams); + + // Assert + expect(result.allowedIpV4AddressRanges).toEqual([ + "192.168.1.0/24", + "10.0.0.0/8", + ]); + expect(result.allowedSignUpEmailDomains).toEqual([ + "example.com", + "test.com", + ]); + }); + + test("ブール値パラメータが正しく処理される", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + const inputParams = { + enableMistral: true, + selfSignUpEnabled: false, + enableRagReplicas: true, + enableBedrockCrossRegionInference: false, + enableLambdaSnapStart: true, + }; + + // Act + const result = resolveBedrockChatParameters(app, inputParams); + + // Assert + expect(result.enableMistral).toBe(true); + expect(result.selfSignUpEnabled).toBe(false); + expect(result.enableRagReplicas).toBe(true); + expect(result.enableBedrockCrossRegionInference).toBe(false); + expect(result.enableLambdaSnapStart).toBe(true); + }); + + test("identityProvidersが配列でない場合、デフォルト値(空配列)が適用される", () => { + // Arrange + const app = new App({ + autoSynth: false, + context: { + identityProviders: "invalid", // 配列ではなく文字列 + }, + }); + + // Act + const result = resolveBedrockChatParameters(app); + + // Assert + expect(result.identityProviders).toEqual([]); + }); + + test("identityProvidersが無効なサービスを含む場合でも、バリデーションはパスする", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + const inputParams = { + identityProviders: [{ service: "invalid", secretName: "Secret" }], + }; + + // Act + const result = resolveBedrockChatParameters(app, inputParams); + + // Assert + expect(result.identityProviders).toEqual([ + { service: "invalid", secretName: "Secret" }, + ]); + // 注: 実際のバリデーションはidentityProvider関数内で行われる + }); + }); + + test("cdk.jsonのcontextプロパティの値を模倣したテスト", () => { + // Arrange + const app = new App({ + autoSynth: false, + context: { + enableMistral: false, + bedrockRegion: "us-east-1", + allowedIpV4AddressRanges: ["0.0.0.0/1", "128.0.0.0/1"], + allowedIpV6AddressRanges: [ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ], + identityProviders: [], + userPoolDomainPrefix: "", + allowedSignUpEmailDomains: [], + autoJoinUserGroups: ["CreatingBotAllowed"], + selfSignUpEnabled: true, + publishedApiAllowedIpV4AddressRanges: ["0.0.0.0/1", "128.0.0.0/1"], + publishedApiAllowedIpV6AddressRanges: [ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ], + enableRagReplicas: true, + enableBedrockCrossRegionInference: true, + enableLambdaSnapStart: true, + alternateDomainName: "", + hostedZoneId: "", + }, + }); + + // Act + const result = resolveBedrockChatParameters(app); + + // Assert + expect(result.bedrockRegion).toBe("us-east-1"); + expect(result.enableMistral).toBe(false); + expect(result.allowedIpV4AddressRanges).toEqual([ + "0.0.0.0/1", + "128.0.0.0/1", + ]); + expect(result.allowedIpV6AddressRanges).toEqual([ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ]); + expect(result.identityProviders).toEqual([]); + expect(result.userPoolDomainPrefix).toBe(""); + expect(result.allowedSignUpEmailDomains).toEqual([]); + expect(result.autoJoinUserGroups).toEqual(["CreatingBotAllowed"]); + expect(result.selfSignUpEnabled).toBe(true); + expect(result.publishedApiAllowedIpV4AddressRanges).toEqual([ + "0.0.0.0/1", + "128.0.0.0/1", + ]); + expect(result.publishedApiAllowedIpV6AddressRanges).toEqual([ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ]); + expect(result.enableRagReplicas).toBe(true); + expect(result.enableBedrockCrossRegionInference).toBe(true); + expect(result.enableLambdaSnapStart).toBe(true); + expect(result.alternateDomainName).toBe(""); + expect(result.hostedZoneId).toBe(""); + }); +}); + +describe("resolveApiPublishParameters", () => { + describe("パラメータソースの選択", () => { + test("parametersInputが指定されている場合、それが使用される", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + const inputParams = { + bedrockRegion: "eu-west-1", + publishedApiThrottleRateLimit: 100, + publishedApiAllowedOrigins: '["https://example.com"]', + }; + + // Act + const result = resolveApiPublishParameters(app, inputParams); + + // Assert + expect(result.bedrockRegion).toBe("eu-west-1"); + expect(result.publishedApiThrottleRateLimit).toBe(100); + expect(result.publishedApiAllowedOrigins).toBe('["https://example.com"]'); + }); + + test("parametersInputが未指定の場合、コンテキストからパラメータが取得される", () => { + // Arrange + const app = new App({ + autoSynth: false, + context: { + bedrockRegion: "ap-northeast-1", + publishedApiThrottleRateLimit: 200, + publishedApiAllowedOrigins: '["https://test.com"]', + }, + }); + + // Act + const result = resolveApiPublishParameters(app); + + // Assert + expect(result.bedrockRegion).toBe("ap-northeast-1"); + expect(result.publishedApiThrottleRateLimit).toBe(200); + expect(result.publishedApiAllowedOrigins).toBe('["https://test.com"]'); + }); + }); + + describe("パラメータのバリデーション", () => { + test("必須パラメータが欠けている場合でも、デフォルト値が適用される", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + + // Act + const result = resolveApiPublishParameters(app); + + // Assert + expect(result.bedrockRegion).toBe("us-east-1"); // デフォルト値 + expect(result.publishedApiAllowedOrigins).toBe('["*"]'); // デフォルト値 + expect(result.publishedApiThrottleRateLimit).toBeUndefined(); // オプショナル + }); + + test("数値パラメータが文字列として提供された場合、数値に変換される", () => { + // Arrange + const app = new App({ + autoSynth: false, + context: { + publishedApiThrottleRateLimit: "100", + publishedApiThrottleBurstLimit: "200", + publishedApiQuotaLimit: "1000", + }, + }); + + // Act + const result = resolveApiPublishParameters(app); + + // Assert + expect(result.publishedApiThrottleRateLimit).toBe(100); + expect(result.publishedApiThrottleBurstLimit).toBe(200); + expect(result.publishedApiQuotaLimit).toBe(1000); + }); + + test("すべてのパラメータが指定されている場合、正しく解析される", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + const inputParams = { + bedrockRegion: "us-west-2", + publishedApiThrottleRateLimit: 100, + publishedApiThrottleBurstLimit: 200, + publishedApiQuotaLimit: 1000, + publishedApiQuotaPeriod: "DAY" as "DAY" | "WEEK" | "MONTH", + publishedApiDeploymentStage: "prod", + publishedApiId: "api123", + publishedApiAllowedOrigins: '["https://example.com", "https://test.com"]', + }; + + // Act + const result = resolveApiPublishParameters(app, inputParams); + + // Assert + expect(result.bedrockRegion).toBe("us-west-2"); + expect(result.publishedApiThrottleRateLimit).toBe(100); + expect(result.publishedApiThrottleBurstLimit).toBe(200); + expect(result.publishedApiQuotaLimit).toBe(1000); + expect(result.publishedApiQuotaPeriod).toBe("DAY"); + expect(result.publishedApiDeploymentStage).toBe("prod"); + expect(result.publishedApiId).toBe("api123"); + expect(result.publishedApiAllowedOrigins).toBe('["https://example.com", "https://test.com"]'); + }); + + test("無効なQuotaPeriodが指定された場合、ZodErrorがスローされる", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + const invalidParams = { + publishedApiQuotaPeriod: "YEAR" as any, // 無効な値 + }; + + // Act & Assert + expect(() => { + resolveApiPublishParameters(app, invalidParams as any); + }).toThrow(ZodError); + }); + }); +}); + +describe("resolveBedrockCustomBotParameters", () => { + describe("パラメータソースの選択", () => { + test("parametersInputが指定されている場合、それが使用される", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + const inputParams = { + bedrockRegion: "eu-west-1", + }; + + // Act + const result = resolveBedrockCustomBotParameters(app, inputParams); + + // Assert + expect(result.bedrockRegion).toBe("eu-west-1"); + }); + + test("parametersInputが未指定の場合、コンテキストからパラメータが取得される", () => { + // Arrange + const app = new App({ + autoSynth: false, + context: { + bedrockRegion: "ap-northeast-1", + }, + }); + + // Act + const result = resolveBedrockCustomBotParameters(app); + + // Assert + expect(result.bedrockRegion).toBe("ap-northeast-1"); + }); + }); + + describe("パラメータのバリデーション", () => { + test("必須パラメータが欠けている場合でも、デフォルト値が適用される", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + + // Act + const result = resolveBedrockCustomBotParameters(app); + + // Assert + expect(result.bedrockRegion).toBe("us-east-1"); // デフォルト値 + }); + + test("無効なパラメータが指定された場合、ZodErrorがスローされる", () => { + // Arrange + const app = new App({ + autoSynth: false, + }); + const invalidParams = { + bedrockRegion: 123, // 文字列ではなく数値 + }; + + // Act & Assert + expect(() => { + resolveBedrockCustomBotParameters(app, invalidParams as any); + }).toThrow(ZodError); + }); + }); +}); \ No newline at end of file From 1fba8b7ec1dd30af4328c9e1718ff36d4206e340 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Mon, 10 Mar 2025 23:33:31 +0900 Subject: [PATCH 05/22] feat(cdk): specify envName by context parameter --- cdk/bin/api-publish.ts | 4 +- cdk/bin/bedrock-chat.ts | 17 +- cdk/bin/bedrock-custom-bot.ts | 4 +- cdk/lib/utils/parameter-models.ts | 55 ++- cdk/test/utils/parameter-models.test.ts | 536 +++++++++++++++--------- 5 files changed, 410 insertions(+), 206 deletions(-) diff --git a/cdk/bin/api-publish.ts b/cdk/bin/api-publish.ts index b4081ba82..8425232b0 100644 --- a/cdk/bin/api-publish.ts +++ b/cdk/bin/api-publish.ts @@ -3,12 +3,12 @@ import "source-map-support/register"; import * as cdk from "aws-cdk-lib"; import { ApiPublishmentStack } from "../lib/api-publishment-stack"; import * as apigateway from "aws-cdk-lib/aws-apigateway"; -import { getApiPublishParameters } from "../lib/utils/parameter-models"; +import { resolveApiPublishParameters } from "../lib/utils/parameter-models"; const app = new cdk.App(); // Get parameters specific to API publishing -const params = getApiPublishParameters(app); +const params = resolveApiPublishParameters(app); // Parse allowed origins const publishedApiAllowedOrigins = JSON.parse( diff --git a/cdk/bin/bedrock-chat.ts b/cdk/bin/bedrock-chat.ts index 962d856df..d53d75ce7 100644 --- a/cdk/bin/bedrock-chat.ts +++ b/cdk/bin/bedrock-chat.ts @@ -6,11 +6,24 @@ import { BedrockRegionResourcesStack } from "../lib/bedrock-region-resources"; import { FrontendWafStack } from "../lib/frontend-waf-stack"; import { LogRetentionChecker } from "../rules/log-retention-checker"; import { getBedrockChatParameters } from "../lib/utils/parameter-models"; +import { bedrockChatParams } from "../parameter"; const app = new cdk.App(); -// Get parameters specific to the Bedrock Chat application -const params = getBedrockChatParameters(app); +// Specify env name by "envName" context variable +// ex) cdk synth -c envName=foo +// If you don't specify the envName context variable, "default" is used. +const params = getBedrockChatParameters( + app, + app.node.tryGetContext("envName"), + bedrockChatParams +); + +// // Another way, you can iterate over params map to declare multiple environments in single App. +// for (const [k] of bedrockChatParams) { +// const params = getBedrockChatParameters(app, k, bedrockChatParams); +// // Include stack declaration this scope... +// } // WAF for frontend // 2023/9: Currently, the WAF for CloudFront needs to be created in the North America region (us-east-1), so the stacks are separated diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index bf1dd9296..6466e46ce 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -10,12 +10,12 @@ import { getCrawlingFilters, } from "../lib/utils/bedrock-knowledge-base-args"; import { CrawlingFilters } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/web-crawler-data-source"; -import { getBedrockCustomBotParameters } from "../lib/utils/parameter-models"; +import { resolveBedrockCustomBotParameters } from "../lib/utils/parameter-models"; const app = new cdk.App(); // Get parameters specific to Bedrock Custom Bot -const params = getBedrockCustomBotParameters(app); +const params = resolveBedrockCustomBotParameters(app); const PK: string = process.env.PK!; const SK: string = process.env.SK!; diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index 87e187a12..38193dc74 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -14,6 +14,10 @@ const BaseParametersSchema = z.object({ * Parameters schema for the main Bedrock Chat application */ const BedrockChatParametersSchema = BaseParametersSchema.extend({ + // CDK Environments + envName: z.string().default("default"), + envPrefix: z.string().default(""), + // Bedrock configuration enableMistral: z.boolean().default(false), enableBedrockCrossRegionInference: z.boolean().default(true), @@ -97,9 +101,10 @@ export type BedrockCustomBotParameters = z.infer< >; /** - * Parse and validate parameters for the main Bedrock Chat application + * Parse and validate parameters for the main Bedrock Chat application. + * If you omit parametersInput, context parameters are used. * @param app CDK App instance - * @param parametersInput Optional input parameters that override context values + * @param parametersInput (optional) Input parameters that should be used instead of context parameters * @returns Validated parameters object */ export function resolveBedrockChatParameters( @@ -115,6 +120,8 @@ export function resolveBedrockChatParameters( const identityProviders = app.node.tryGetContext("identityProviders"); const contextParams = { + envName: "default", + envPrefix: "", bedrockRegion: app.node.tryGetContext("bedrockRegion"), enableMistral: app.node.tryGetContext("enableMistral"), allowedIpV4AddressRanges: app.node.tryGetContext( @@ -151,6 +158,50 @@ export function resolveBedrockChatParameters( return BedrockChatParametersSchema.parse(contextParams); } +/** + * Get Bedrock Chat parameters based on environment name. + * If you omit envName, "default" is used. + * If you omit parametersInput, context parameters are used. + * @param app CDK App instance + * @param envName (optional) Environment name. Used as map key if provided + * @param paramsMap (optional) Map of parameters. If not provided, use context parameters + * @returns Validated parameters object + */ +export function getBedrockChatParameters( + app: App, + envName: string | undefined, + paramsMap: Map +): BedrockChatParameters { + if (envName == undefined) { + if (paramsMap.has("default")) { + // Use parameter.ts instead of context parameters + const params = paramsMap.get("default") || {}; + return resolveBedrockChatParameters(app, { + envName: "default", + envPrefix: "", + ...params, + }); + } else { + // Use CDK context parameters (cdk.json or -c options) + return resolveBedrockChatParameters(app); + } + } else { + // Lookup envName in parameter.ts + if (!paramsMap.has(envName)) { + throw new Error(`Environment ${envName} not found in parameter.ts`); + } + + const params = paramsMap.get(envName) || {}; + const envPrefix = envName === "default" ? "" : envName; + + return resolveBedrockChatParameters(app, { + envName, + envPrefix, + ...params, + }); + } +} + /** * Parse and validate parameters for API publishing * @param app CDK App instance diff --git a/cdk/test/utils/parameter-models.test.ts b/cdk/test/utils/parameter-models.test.ts index e457dfc9e..33ace27f0 100644 --- a/cdk/test/utils/parameter-models.test.ts +++ b/cdk/test/utils/parameter-models.test.ts @@ -1,74 +1,77 @@ import { App } from "aws-cdk-lib"; -import { +import { resolveBedrockChatParameters, resolveApiPublishParameters, - resolveBedrockCustomBotParameters + resolveBedrockCustomBotParameters, + BedrockChatParametersInput, + getBedrockChatParameters, } from "../../lib/utils/parameter-models"; import { ZodError } from "zod"; +/** + * テストヘルパー関数: App インスタンスを作成 + */ +function createTestApp(context = {}) { + return new App({ + autoSynth: false, + context, + }); +} + describe("resolveBedrockChatParameters", () => { - describe("パラメータソースの選択", () => { - test("parametersInputが指定されている場合、それが使用される", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + describe("Parameter Source Selection", () => { + test("should use parametersInput when provided", () => { + // Given + const app = createTestApp(); const inputParams = { bedrockRegion: "eu-west-1", enableMistral: true, }; - // Act + // When const result = resolveBedrockChatParameters(app, inputParams); - // Assert + // Then expect(result.bedrockRegion).toBe("eu-west-1"); expect(result.enableMistral).toBe(true); }); - test("parametersInputが未指定の場合、コンテキストからパラメータが取得される", () => { - // Arrange - const app = new App({ - autoSynth: false, - context: { - bedrockRegion: "ap-northeast-1", - enableMistral: true, - }, + test("should get parameters from context when parametersInput is not provided", () => { + // Given + const app = createTestApp({ + bedrockRegion: "ap-northeast-1", + enableMistral: true, }); - // Act + // When const result = resolveBedrockChatParameters(app); - // Assert + // Then expect(result.bedrockRegion).toBe("ap-northeast-1"); expect(result.enableMistral).toBe(true); }); }); - describe("パラメータのバリデーション", () => { - test("必須パラメータが欠けている場合でも、デフォルト値が適用される", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + describe("Parameter Validation", () => { + test("should apply default values when required parameters are missing", () => { + // Given + const app = createTestApp(); - // Act + // When const result = resolveBedrockChatParameters(app); - // Assert - expect(result.bedrockRegion).toBe("us-east-1"); // デフォルト値 - expect(result.enableMistral).toBe(false); // デフォルト値 + // Then + expect(result.bedrockRegion).toBe("us-east-1"); // default value + expect(result.enableMistral).toBe(false); // default value expect(result.allowedIpV4AddressRanges).toEqual([ "0.0.0.0/1", "128.0.0.0/1", - ]); // デフォルト値 + ]); // default value }); - test("すべてのパラメータが指定されている場合、正しく解析される", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + test("should correctly parse all parameters when specified", () => { + // Given + const app = createTestApp(); const inputParams = { bedrockRegion: "us-west-2", enableMistral: true, @@ -88,10 +91,10 @@ describe("resolveBedrockChatParameters", () => { hostedZoneId: "Z1234567890", }; - // Act + // When const result = resolveBedrockChatParameters(app, inputParams); - // Assert + // Then expect(result.bedrockRegion).toBe("us-west-2"); expect(result.enableMistral).toBe(true); expect(result.allowedIpV4AddressRanges).toEqual(["192.168.0.0/16"]); @@ -116,37 +119,33 @@ describe("resolveBedrockChatParameters", () => { expect(result.hostedZoneId).toBe("Z1234567890"); }); - test("無効なパラメータが指定された場合、ZodErrorがスローされる", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + test("should throw ZodError when invalid parameter is specified", () => { + // Given + const app = createTestApp(); const invalidParams = { - bedrockRegion: 123, // 文字列ではなく数値 + bedrockRegion: 123, // number instead of string }; - // Act & Assert + // When/Then expect(() => { resolveBedrockChatParameters(app, invalidParams as any); }).toThrow(ZodError); }); }); - describe("特殊なパラメータ処理", () => { - test("配列パラメータが正しく処理される", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + describe("Special Parameter Handling", () => { + test("should correctly process array parameters", () => { + // Given + const app = createTestApp(); const inputParams = { allowedIpV4AddressRanges: ["192.168.1.0/24", "10.0.0.0/8"], allowedSignUpEmailDomains: ["example.com", "test.com"], }; - // Act + // When const result = resolveBedrockChatParameters(app, inputParams); - // Assert + // Then expect(result.allowedIpV4AddressRanges).toEqual([ "192.168.1.0/24", "10.0.0.0/8", @@ -157,11 +156,9 @@ describe("resolveBedrockChatParameters", () => { ]); }); - test("ブール値パラメータが正しく処理される", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + test("should correctly process boolean parameters", () => { + // Given + const app = createTestApp(); const inputParams = { enableMistral: true, selfSignUpEnabled: false, @@ -170,10 +167,10 @@ describe("resolveBedrockChatParameters", () => { enableLambdaSnapStart: true, }; - // Act + // When const result = resolveBedrockChatParameters(app, inputParams); - // Assert + // Then expect(result.enableMistral).toBe(true); expect(result.selfSignUpEnabled).toBe(false); expect(result.enableRagReplicas).toBe(true); @@ -181,76 +178,68 @@ describe("resolveBedrockChatParameters", () => { expect(result.enableLambdaSnapStart).toBe(true); }); - test("identityProvidersが配列でない場合、デフォルト値(空配列)が適用される", () => { - // Arrange - const app = new App({ - autoSynth: false, - context: { - identityProviders: "invalid", // 配列ではなく文字列 - }, + test("should apply default value (empty array) when identityProviders is not an array", () => { + // Given + const app = createTestApp({ + identityProviders: "invalid", // string instead of array }); - // Act + // When const result = resolveBedrockChatParameters(app); - // Assert + // Then expect(result.identityProviders).toEqual([]); }); - test("identityProvidersが無効なサービスを含む場合でも、バリデーションはパスする", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + test("should pass validation even when identityProviders contains invalid service", () => { + // Given + const app = createTestApp(); const inputParams = { identityProviders: [{ service: "invalid", secretName: "Secret" }], }; - // Act + // When const result = resolveBedrockChatParameters(app, inputParams); - // Assert + // Then expect(result.identityProviders).toEqual([ { service: "invalid", secretName: "Secret" }, ]); - // 注: 実際のバリデーションはidentityProvider関数内で行われる + // Note: Actual validation is performed in identityProvider function }); }); - test("cdk.jsonのcontextプロパティの値を模倣したテスト", () => { - // Arrange - const app = new App({ - autoSynth: false, - context: { - enableMistral: false, - bedrockRegion: "us-east-1", - allowedIpV4AddressRanges: ["0.0.0.0/1", "128.0.0.0/1"], - allowedIpV6AddressRanges: [ - "0000:0000:0000:0000:0000:0000:0000:0000/1", - "8000:0000:0000:0000:0000:0000:0000:0000/1", - ], - identityProviders: [], - userPoolDomainPrefix: "", - allowedSignUpEmailDomains: [], - autoJoinUserGroups: ["CreatingBotAllowed"], - selfSignUpEnabled: true, - publishedApiAllowedIpV4AddressRanges: ["0.0.0.0/1", "128.0.0.0/1"], - publishedApiAllowedIpV6AddressRanges: [ - "0000:0000:0000:0000:0000:0000:0000:0000/1", - "8000:0000:0000:0000:0000:0000:0000:0000/1", - ], - enableRagReplicas: true, - enableBedrockCrossRegionInference: true, - enableLambdaSnapStart: true, - alternateDomainName: "", - hostedZoneId: "", - }, + test("should correctly parse parameters mimicking cdk.json context properties", () => { + // Given + const app = createTestApp({ + enableMistral: false, + bedrockRegion: "us-east-1", + allowedIpV4AddressRanges: ["0.0.0.0/1", "128.0.0.0/1"], + allowedIpV6AddressRanges: [ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ], + identityProviders: [], + userPoolDomainPrefix: "", + allowedSignUpEmailDomains: [], + autoJoinUserGroups: ["CreatingBotAllowed"], + selfSignUpEnabled: true, + publishedApiAllowedIpV4AddressRanges: ["0.0.0.0/1", "128.0.0.0/1"], + publishedApiAllowedIpV6AddressRanges: [ + "0000:0000:0000:0000:0000:0000:0000:0000/1", + "8000:0000:0000:0000:0000:0000:0000:0000/1", + ], + enableRagReplicas: true, + enableBedrockCrossRegionInference: true, + enableLambdaSnapStart: true, + alternateDomainName: "", + hostedZoneId: "", }); - // Act + // When const result = resolveBedrockChatParameters(app); - // Assert + // Then expect(result.bedrockRegion).toBe("us-east-1"); expect(result.enableMistral).toBe(false); expect(result.allowedIpV4AddressRanges).toEqual([ @@ -282,90 +271,234 @@ describe("resolveBedrockChatParameters", () => { }); }); -describe("resolveApiPublishParameters", () => { - describe("パラメータソースの選択", () => { - test("parametersInputが指定されている場合、それが使用される", () => { - // Arrange - const app = new App({ - autoSynth: false, +describe("getBedrockChatParameters", () => { + let app: App; + let paramsMap: Map; + + beforeEach(() => { + app = createTestApp(); + paramsMap = new Map(); + }); + + describe("Environment Name Handling", () => { + test("should use params from paramsMap when default exists and envName is undefined", () => { + // Given + const defaultParams: BedrockChatParametersInput = { + bedrockRegion: "us-west-2", + enableMistral: true, + }; + paramsMap.set("default", defaultParams); + + // When + const result = getBedrockChatParameters(app, undefined, paramsMap); + + // Then + expect(result.envName).toBe("default"); + expect(result.envPrefix).toBe(""); + expect(result.bedrockRegion).toBe("us-west-2"); + expect(result.enableMistral).toBe(true); + }); + + test("should use CDK context when default doesn't exist in paramsMap and envName is undefined", () => { + // Given + app = createTestApp({ + bedrockRegion: "eu-west-1", + enableMistral: true, }); + + // When + const result = getBedrockChatParameters(app, undefined, paramsMap); + + // Then + expect(result.envName).toBe("default"); + expect(result.envPrefix).toBe(""); + expect(result.bedrockRegion).toBe("eu-west-1"); + expect(result.enableMistral).toBe(true); + }); + + test("should throw error when envName doesn't exist in paramsMap", () => { + // Given + const nonExistentEnvName = "nonexistent"; + + // When/Then + expect(() => { + getBedrockChatParameters(app, nonExistentEnvName, paramsMap); + }).toThrow(`Environment ${nonExistentEnvName} not found in parameter.ts`); + }); + + test("should use params with empty envPrefix when envName is default", () => { + // Given + const defaultParams: BedrockChatParametersInput = { + bedrockRegion: "ap-northeast-1", + }; + paramsMap.set("default", defaultParams); + + // When + const result = getBedrockChatParameters(app, "default", paramsMap); + + // Then + expect(result.envName).toBe("default"); + expect(result.envPrefix).toBe(""); + expect(result.bedrockRegion).toBe("ap-northeast-1"); + }); + + test("should use params with envName as envPrefix for non-default env", () => { + // Given + const devParams: BedrockChatParametersInput = { + bedrockRegion: "ap-southeast-1", + }; + paramsMap.set("dev", devParams); + + // When + const result = getBedrockChatParameters(app, "dev", paramsMap); + + // Then + expect(result.envName).toBe("dev"); + expect(result.envPrefix).toBe("dev"); + expect(result.bedrockRegion).toBe("ap-southeast-1"); + }); + }); + + describe("Parameter Validation", () => { + test("should apply default values for optional parameters", () => { + // Given + const minimalParams: BedrockChatParametersInput = { + bedrockRegion: "us-east-1", + }; + paramsMap.set("minimal", minimalParams); + + // When + const result = getBedrockChatParameters(app, "minimal", paramsMap); + + // Then + expect(result.bedrockRegion).toBe("us-east-1"); + expect(result.enableMistral).toBe(false); + expect(result.enableBedrockCrossRegionInference).toBe(true); + expect(result.enableRagReplicas).toBe(true); + expect(result.enableLambdaSnapStart).toBe(true); + expect(result.selfSignUpEnabled).toBe(true); + expect(result.autoJoinUserGroups).toEqual(["CreatingBotAllowed"]); + expect(result.allowedSignUpEmailDomains).toEqual([]); + expect(result.identityProviders).toEqual([]); + }); + + test("should override default values with provided values", () => { + // Given + const customParams: BedrockChatParametersInput = { + bedrockRegion: "us-east-2", + enableMistral: true, + enableBedrockCrossRegionInference: false, + enableRagReplicas: false, + enableLambdaSnapStart: false, + selfSignUpEnabled: false, + autoJoinUserGroups: ["CustomGroup"], + allowedSignUpEmailDomains: ["example.com"], + }; + paramsMap.set("custom", customParams); + + // When + const result = getBedrockChatParameters(app, "custom", paramsMap); + + // Then + expect(result.bedrockRegion).toBe("us-east-2"); + expect(result.enableMistral).toBe(true); + expect(result.enableBedrockCrossRegionInference).toBe(false); + expect(result.enableRagReplicas).toBe(false); + expect(result.enableLambdaSnapStart).toBe(false); + expect(result.selfSignUpEnabled).toBe(false); + expect(result.autoJoinUserGroups).toEqual(["CustomGroup"]); + expect(result.allowedSignUpEmailDomains).toEqual(["example.com"]); + }); + + test("should use default values when CDK context is empty", () => { + // Given + const emptyParamsMap = new Map(); + + // When + const result = getBedrockChatParameters(app, undefined, emptyParamsMap); + + // Then + expect(result.bedrockRegion).toBe("us-east-1"); + expect(result.enableMistral).toBe(false); + expect(result.enableBedrockCrossRegionInference).toBe(true); + expect(result.enableRagReplicas).toBe(true); + expect(result.enableLambdaSnapStart).toBe(true); + }); + }); +}); + +describe("resolveApiPublishParameters", () => { + describe("Parameter Source Selection", () => { + test("should use parametersInput when provided", () => { + // Given + const app = createTestApp(); const inputParams = { bedrockRegion: "eu-west-1", publishedApiThrottleRateLimit: 100, publishedApiAllowedOrigins: '["https://example.com"]', }; - // Act + // When const result = resolveApiPublishParameters(app, inputParams); - // Assert + // Then expect(result.bedrockRegion).toBe("eu-west-1"); expect(result.publishedApiThrottleRateLimit).toBe(100); expect(result.publishedApiAllowedOrigins).toBe('["https://example.com"]'); }); - test("parametersInputが未指定の場合、コンテキストからパラメータが取得される", () => { - // Arrange - const app = new App({ - autoSynth: false, - context: { - bedrockRegion: "ap-northeast-1", - publishedApiThrottleRateLimit: 200, - publishedApiAllowedOrigins: '["https://test.com"]', - }, + test("should get parameters from context when parametersInput is not provided", () => { + // Given + const app = createTestApp({ + bedrockRegion: "ap-northeast-1", + publishedApiThrottleRateLimit: 200, + publishedApiAllowedOrigins: '["https://test.com"]', }); - // Act + // When const result = resolveApiPublishParameters(app); - // Assert + // Then expect(result.bedrockRegion).toBe("ap-northeast-1"); expect(result.publishedApiThrottleRateLimit).toBe(200); expect(result.publishedApiAllowedOrigins).toBe('["https://test.com"]'); }); }); - describe("パラメータのバリデーション", () => { - test("必須パラメータが欠けている場合でも、デフォルト値が適用される", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + describe("Parameter Validation", () => { + test("should apply default values when required parameters are missing", () => { + // Given + const app = createTestApp(); - // Act + // When const result = resolveApiPublishParameters(app); - // Assert - expect(result.bedrockRegion).toBe("us-east-1"); // デフォルト値 - expect(result.publishedApiAllowedOrigins).toBe('["*"]'); // デフォルト値 - expect(result.publishedApiThrottleRateLimit).toBeUndefined(); // オプショナル + // Then + expect(result.bedrockRegion).toBe("us-east-1"); // default value + expect(result.publishedApiAllowedOrigins).toBe('["*"]'); // default value + expect(result.publishedApiThrottleRateLimit).toBeUndefined(); // optional }); - test("数値パラメータが文字列として提供された場合、数値に変換される", () => { - // Arrange - const app = new App({ - autoSynth: false, - context: { - publishedApiThrottleRateLimit: "100", - publishedApiThrottleBurstLimit: "200", - publishedApiQuotaLimit: "1000", - }, + test("should convert string numeric parameters to numbers", () => { + // Given + const app = createTestApp({ + publishedApiThrottleRateLimit: "100", + publishedApiThrottleBurstLimit: "200", + publishedApiQuotaLimit: "1000", }); - // Act + // When const result = resolveApiPublishParameters(app); - // Assert + // Then expect(result.publishedApiThrottleRateLimit).toBe(100); expect(result.publishedApiThrottleBurstLimit).toBe(200); expect(result.publishedApiQuotaLimit).toBe(1000); }); - test("すべてのパラメータが指定されている場合、正しく解析される", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + test("should correctly parse all parameters when specified", () => { + // Given + const app = createTestApp(); const inputParams = { bedrockRegion: "us-west-2", publishedApiThrottleRateLimit: 100, @@ -374,13 +507,14 @@ describe("resolveApiPublishParameters", () => { publishedApiQuotaPeriod: "DAY" as "DAY" | "WEEK" | "MONTH", publishedApiDeploymentStage: "prod", publishedApiId: "api123", - publishedApiAllowedOrigins: '["https://example.com", "https://test.com"]', + publishedApiAllowedOrigins: + '["https://example.com", "https://test.com"]', }; - // Act + // When const result = resolveApiPublishParameters(app, inputParams); - // Assert + // Then expect(result.bedrockRegion).toBe("us-west-2"); expect(result.publishedApiThrottleRateLimit).toBe(100); expect(result.publishedApiThrottleBurstLimit).toBe(200); @@ -388,88 +522,94 @@ describe("resolveApiPublishParameters", () => { expect(result.publishedApiQuotaPeriod).toBe("DAY"); expect(result.publishedApiDeploymentStage).toBe("prod"); expect(result.publishedApiId).toBe("api123"); - expect(result.publishedApiAllowedOrigins).toBe('["https://example.com", "https://test.com"]'); + expect(result.publishedApiAllowedOrigins).toBe( + '["https://example.com", "https://test.com"]' + ); }); - test("無効なQuotaPeriodが指定された場合、ZodErrorがスローされる", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + test("should throw ZodError when invalid QuotaPeriod is specified", () => { + // Given + const app = createTestApp(); const invalidParams = { - publishedApiQuotaPeriod: "YEAR" as any, // 無効な値 + publishedApiQuotaPeriod: "YEAR" as any, // invalid value }; - // Act & Assert + // When/Then expect(() => { resolveApiPublishParameters(app, invalidParams as any); }).toThrow(ZodError); }); + + test("should handle invalid publishedApiAllowedOrigins format", () => { + // Given + const app = createTestApp(); + const invalidParams = { + publishedApiAllowedOrigins: 'invalid json format', + }; + + // When + const result = resolveApiPublishParameters(app, invalidParams); + + // Then + // Note: The function doesn't validate JSON format, it just passes the string through + expect(result.publishedApiAllowedOrigins).toBe('invalid json format'); + }); }); }); describe("resolveBedrockCustomBotParameters", () => { - describe("パラメータソースの選択", () => { - test("parametersInputが指定されている場合、それが使用される", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + describe("Parameter Source Selection", () => { + test("should use parametersInput when provided", () => { + // Given + const app = createTestApp(); const inputParams = { bedrockRegion: "eu-west-1", }; - // Act + // When const result = resolveBedrockCustomBotParameters(app, inputParams); - // Assert + // Then expect(result.bedrockRegion).toBe("eu-west-1"); }); - test("parametersInputが未指定の場合、コンテキストからパラメータが取得される", () => { - // Arrange - const app = new App({ - autoSynth: false, - context: { - bedrockRegion: "ap-northeast-1", - }, + test("should get parameters from context when parametersInput is not provided", () => { + // Given + const app = createTestApp({ + bedrockRegion: "ap-northeast-1", }); - // Act + // When const result = resolveBedrockCustomBotParameters(app); - // Assert + // Then expect(result.bedrockRegion).toBe("ap-northeast-1"); }); }); - describe("パラメータのバリデーション", () => { - test("必須パラメータが欠けている場合でも、デフォルト値が適用される", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + describe("Parameter Validation", () => { + test("should apply default values when required parameters are missing", () => { + // Given + const app = createTestApp(); - // Act + // When const result = resolveBedrockCustomBotParameters(app); - // Assert - expect(result.bedrockRegion).toBe("us-east-1"); // デフォルト値 + // Then + expect(result.bedrockRegion).toBe("us-east-1"); // default value }); - test("無効なパラメータが指定された場合、ZodErrorがスローされる", () => { - // Arrange - const app = new App({ - autoSynth: false, - }); + test("should throw ZodError when invalid parameter is specified", () => { + // Given + const app = createTestApp(); const invalidParams = { - bedrockRegion: 123, // 文字列ではなく数値 + bedrockRegion: 123, // number instead of string }; - // Act & Assert + // When/Then expect(() => { resolveBedrockCustomBotParameters(app, invalidParams as any); }).toThrow(ZodError); }); }); -}); \ No newline at end of file +}); From 18c5784c77057fd83ef01598735810b7ce22321b Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Wed, 12 Mar 2025 20:06:24 +0900 Subject: [PATCH 06/22] refactor(cdk): unify parameter handling and add environment variable support Move envName and envPrefix to BaseParameterSchema and add functionality to retrieve parameters from environment variables. This allows handling context parameters from tryGetContext and environment variables using the same mechanism. - Add getEnvVar helper function to parameter-models.ts - Extend BedrockCustomBotParametersSchema with required parameters - Update resolveBedrockCustomBotParameters to retrieve parameters from environment variables - Similarly update resolveApiPublishParameters - Modify bin/bedrock-custom-bot.ts and bin/api-publish.ts to use the new parameter handling - Simplify debug logging and standardize to JSON format - Update function comments to clarify that values are retrieved from environment variables --- cdk/bin/api-publish.ts | 30 ++- cdk/bin/bedrock-chat.ts | 81 ++++---- cdk/bin/bedrock-custom-bot.ts | 183 ++++++++---------- cdk/lib/bedrock-chat-stack.ts | 11 +- cdk/lib/constructs/api-publish-codebuild.ts | 4 + .../bedrock-custom-bot-codebuild.ts | 4 + cdk/lib/constructs/usage-analysis.ts | 5 +- .../constructs/webacl-for-published-api.ts | 4 +- cdk/lib/frontend-waf-stack.ts | 4 +- cdk/lib/utils/parameter-models.ts | 71 +++++-- cdk/test/utils/parameter-models.test.ts | 74 +++++-- 11 files changed, 284 insertions(+), 187 deletions(-) diff --git a/cdk/bin/api-publish.ts b/cdk/bin/api-publish.ts index 8425232b0..f715a0e3c 100644 --- a/cdk/bin/api-publish.ts +++ b/cdk/bin/api-publish.ts @@ -9,42 +9,34 @@ const app = new cdk.App(); // Get parameters specific to API publishing const params = resolveApiPublishParameters(app); +const sepHyphen = params.envPrefix ? "-" : ""; // Parse allowed origins const publishedApiAllowedOrigins = JSON.parse( params.publishedApiAllowedOrigins || '["*"]' ); -console.log( - `PUBLISHED_API_THROTTLE_RATE_LIMIT: ${params.publishedApiThrottleRateLimit}` -); -console.log( - `PUBLISHED_API_THROTTLE_BURST_LIMIT: ${params.publishedApiThrottleBurstLimit}` -); -console.log(`PUBLISHED_API_QUOTA_LIMIT: ${params.publishedApiQuotaLimit}`); -console.log(`PUBLISHED_API_QUOTA_PERIOD: ${params.publishedApiQuotaPeriod}`); -console.log( - `PUBLISHED_API_DEPLOYMENT_STAGE: ${params.publishedApiDeploymentStage}` -); -console.log(`PUBLISHED_API_ID: ${params.publishedApiId}`); -console.log(`PUBLISHED_API_ALLOWED_ORIGINS: ${publishedApiAllowedOrigins}`); +// Log all parameters at once for debugging +console.log("API Publish Parameters:", JSON.stringify(params)); -const webAclArn = cdk.Fn.importValue("PublishedApiWebAclArn"); +const webAclArn = cdk.Fn.importValue( + `${params.envPrefix}${sepHyphen}PublishedApiWebAclArn` +); const conversationTableName = cdk.Fn.importValue( - "BedrockClaudeChatConversationTableName" + `${params.envPrefix}${sepHyphen}BedrockClaudeChatConversationTableName` ); const tableAccessRoleArn = cdk.Fn.importValue( - "BedrockClaudeChatTableAccessRoleArn" + `${params.envPrefix}${sepHyphen}BedrockClaudeChatTableAccessRoleArn` ); const largeMessageBucketName = cdk.Fn.importValue( - "BedrockClaudeChatLargeMessageBucketName" + `${params.envPrefix}${sepHyphen}BedrockClaudeChatLargeMessageBucketName` ); // NOTE: DO NOT change the stack id naming rule. const publishedApi = new ApiPublishmentStack( app, - `ApiPublishmentStack${params.publishedApiId}`, + `${params.envPrefix}${sepHyphen}ApiPublishmentStack${params.publishedApiId}`, { env: { region: process.env.CDK_DEFAULT_REGION, @@ -81,3 +73,5 @@ const publishedApi = new ApiPublishmentStack( }, } ); + +cdk.Tags.of(app).add("CDKEnvironment", params.envName); diff --git a/cdk/bin/bedrock-chat.ts b/cdk/bin/bedrock-chat.ts index d53d75ce7..ffc8d8124 100644 --- a/cdk/bin/bedrock-chat.ts +++ b/cdk/bin/bedrock-chat.ts @@ -25,17 +25,23 @@ const params = getBedrockChatParameters( // // Include stack declaration this scope... // } +const sepHyphen = params.envPrefix ? "-" : ""; + // WAF for frontend // 2023/9: Currently, the WAF for CloudFront needs to be created in the North America region (us-east-1), so the stacks are separated // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-wafv2-webacl.html -const waf = new FrontendWafStack(app, `FrontendWafStack`, { - env: { - // account: process.env.CDK_DEFAULT_ACCOUNT, - region: "us-east-1", - }, - allowedIpV4AddressRanges: params.allowedIpV4AddressRanges, - allowedIpV6AddressRanges: params.allowedIpV6AddressRanges, -}); +const waf = new FrontendWafStack( + app, + `${params.envPrefix}${sepHyphen}FrontendWafStack`, + { + env: { + // account: process.env.CDK_DEFAULT_ACCOUNT, + region: "us-east-1", + }, + allowedIpV4AddressRanges: params.allowedIpV4AddressRanges, + allowedIpV6AddressRanges: params.allowedIpV6AddressRanges, + } +); // The region of the LLM model called by the converse API and the region of Guardrail must be in the same region. // CustomBotStack contains Knowledge Bases is deployed in the same region as the LLM model, and source bucket must be in the same region as Knowledge Bases. @@ -43,7 +49,7 @@ const waf = new FrontendWafStack(app, `FrontendWafStack`, { // Ref: https://docs.aws.amazon.com/bedrock/latest/userguide/s3-data-source-connector.html const bedrockRegionResources = new BedrockRegionResourcesStack( app, - `BedrockRegionResourcesStack`, + `${params.envPrefix}${sepHyphen}BedrockRegionResourcesStack`, { env: { // account: process.env.CDK_DEFAULT_ACCOUNT, @@ -53,33 +59,38 @@ const bedrockRegionResources = new BedrockRegionResourcesStack( } ); -const chat = new BedrockChatStack(app, `BedrockChatStack`, { - env: { - // account: process.env.CDK_DEFAULT_ACCOUNT, - region: process.env.CDK_DEFAULT_REGION, - }, - crossRegionReferences: true, - bedrockRegion: params.bedrockRegion, - webAclId: waf.webAclArn.value, - enableIpV6: waf.ipV6Enabled, - identityProviders: params.identityProviders, - userPoolDomainPrefix: params.userPoolDomainPrefix, - publishedApiAllowedIpV4AddressRanges: - params.publishedApiAllowedIpV4AddressRanges, - publishedApiAllowedIpV6AddressRanges: - params.publishedApiAllowedIpV6AddressRanges, - allowedSignUpEmailDomains: params.allowedSignUpEmailDomains, - autoJoinUserGroups: params.autoJoinUserGroups, - enableMistral: params.enableMistral, - selfSignUpEnabled: params.selfSignUpEnabled, - documentBucket: bedrockRegionResources.documentBucket, - useStandbyReplicas: params.enableRagReplicas, - enableBedrockCrossRegionInference: params.enableBedrockCrossRegionInference, - enableLambdaSnapStart: params.enableLambdaSnapStart, - alternateDomainName: params.alternateDomainName, - hostedZoneId: params.hostedZoneId, -}); +const chat = new BedrockChatStack( + app, + `${params.envPrefix}${sepHyphen}BedrockChatStack`, + { + env: { + // account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, + crossRegionReferences: true, + bedrockRegion: params.bedrockRegion, + webAclId: waf.webAclArn.value, + enableIpV6: waf.ipV6Enabled, + identityProviders: params.identityProviders, + userPoolDomainPrefix: params.userPoolDomainPrefix, + publishedApiAllowedIpV4AddressRanges: + params.publishedApiAllowedIpV4AddressRanges, + publishedApiAllowedIpV6AddressRanges: + params.publishedApiAllowedIpV6AddressRanges, + allowedSignUpEmailDomains: params.allowedSignUpEmailDomains, + autoJoinUserGroups: params.autoJoinUserGroups, + enableMistral: params.enableMistral, + selfSignUpEnabled: params.selfSignUpEnabled, + documentBucket: bedrockRegionResources.documentBucket, + useStandbyReplicas: params.enableRagReplicas, + enableBedrockCrossRegionInference: params.enableBedrockCrossRegionInference, + enableLambdaSnapStart: params.enableLambdaSnapStart, + alternateDomainName: params.alternateDomainName, + hostedZoneId: params.hostedZoneId, + } +); chat.addDependency(waf); chat.addDependency(bedrockRegionResources); cdk.Aspects.of(chat).add(new LogRetentionChecker()); +cdk.Tags.of(app).add("CDKEnvironment", params.envName); diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index 6466e46ce..4913289a7 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -16,125 +16,118 @@ const app = new cdk.App(); // Get parameters specific to Bedrock Custom Bot const params = resolveBedrockCustomBotParameters(app); +const sepHyphen = params.envPrefix ? "-" : ""; -const PK: string = process.env.PK!; -const SK: string = process.env.SK!; -const BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: string = - process.env.BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME!; -const KNOWLEDGE: string = process.env.KNOWLEDGE!; -const BEDROCK_KNOWLEDGE_BASE: string = process.env.BEDROCK_KNOWLEDGE_BASE!; -const BEDROCK_GUARDRAILS: string = process.env.BEDROCK_GUARDRAILS!; -const USE_STAND_BY_REPLICAS: string = process.env.USE_STAND_BY_REPLICAS!; - -console.log("PK: ", PK); -console.log("SK: ", SK); +// Log basic parameters for debugging console.log( - "BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: ", - BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME + "Bedrock Custom Bot Parameters:", + JSON.stringify({ + envName: params.envName, + envPrefix: params.envPrefix, + pk: params.pk, + sk: params.sk, + documentBucketName: params.documentBucketName, + useStandByReplicas: params.useStandByReplicas, + bedrockRegion: params.bedrockRegion, + }) ); -console.log("KNOWLEDGE: ", KNOWLEDGE); -console.log("BEDROCK_KNOWLEDGE_BASE: ", BEDROCK_KNOWLEDGE_BASE); -console.log("BEDROCK_GUARDRAILS: ", BEDROCK_GUARDRAILS); -console.log("USE_STAND_BY_REPLICAS: ", USE_STAND_BY_REPLICAS); -const ownerUserId: string = PK; -const botId: string = SK.split("#")[2]; -const knowledgeBase = JSON.parse(BEDROCK_KNOWLEDGE_BASE); -const knowledge = JSON.parse(KNOWLEDGE); -const guardrails = JSON.parse(BEDROCK_GUARDRAILS); -const existingS3Urls: string[] = knowledge.s3_urls.L.map( - (s3Url: any) => s3Url.S -); -const sourceUrls: string[] = knowledge.source_urls.L.map( - (sourceUrl: any) => sourceUrl.S -); -const useStandbyReplicas: boolean = USE_STAND_BY_REPLICAS === "true"; +// Parse JSON strings into objects +const knowledgeBase = JSON.parse(params.knowledgeBase); +const knowledge = JSON.parse(params.knowledge); +const guardrails = JSON.parse(params.guardrails); -console.log("ownerUserId: ", ownerUserId); -console.log("botId: ", botId); -console.log("knowledgeBase: ", knowledgeBase); -console.log("knowledge: ", knowledge); -console.log("guardrails: ", guardrails); -console.log("existingS3Urls: ", existingS3Urls); -console.log("sourceUrls: ", sourceUrls); +// Extract data from parsed objects +const ownerUserId = params.pk; +const botId = params.sk.split("#")[2]; +const existingS3Urls = knowledge.s3_urls.L.map((s3Url: any) => s3Url.S); +const sourceUrls = knowledge.source_urls.L.map((sourceUrl: any) => sourceUrl.S); +const useStandbyReplicas = params.useStandByReplicas === true; + +console.log( + "Parsed Configuration:", + JSON.stringify({ + ownerUserId, + botId, + existingS3Urls, + sourceUrls, + useStandbyReplicas, + }) +); +// Process knowledge base configuration const embeddingsModel = getEmbeddingModel(knowledgeBase.embeddings_model.S); const parsingModel = getParsingModel(knowledgeBase.parsing_model.S); const crawlingScope = getCrowlingScope(knowledgeBase.web_crawling_scope.S); const crawlingFilters: CrawlingFilters = getCrawlingFilters( knowledgeBase.web_crawling_filters.M ); -const existKnowledgeBaseId: string | undefined = knowledgeBase - .exist_knowledge_base_id.S - ? knowledgeBase.exist_knowledge_base_id.S - : undefined; -const maxTokens: number | undefined = knowledgeBase.chunking_configuration.M - .max_tokens +const existKnowledgeBaseId = knowledgeBase.exist_knowledge_base_id?.S; +const maxTokens = knowledgeBase.chunking_configuration.M.max_tokens ? Number(knowledgeBase.chunking_configuration.M.max_tokens.N) : undefined; -const instruction: string | undefined = knowledgeBase.instruction - ? knowledgeBase.instruction.S - : undefined; +const instruction = knowledgeBase.instruction?.S; const analyzer = knowledgeBase.open_search.M.analyzer.M ? getAnalyzer(knowledgeBase.open_search.M.analyzer.M) : undefined; -const overlapPercentage: number | undefined = knowledgeBase - .chunking_configuration.M.overlap_percentage +const overlapPercentage = knowledgeBase.chunking_configuration.M + .overlap_percentage ? Number(knowledgeBase.chunking_configuration.M.overlap_percentage.N) : undefined; -const overlapTokens: number | undefined = knowledgeBase.chunking_configuration.M - .overlap_tokens +const overlapTokens = knowledgeBase.chunking_configuration.M.overlap_tokens ? Number(knowledgeBase.chunking_configuration.M.overlap_tokens.N) : undefined; -const maxParentTokenSize: number | undefined = knowledgeBase - .chunking_configuration.M.max_parent_token_size +const maxParentTokenSize = knowledgeBase.chunking_configuration.M + .max_parent_token_size ? Number(knowledgeBase.chunking_configuration.M.max_parent_token_size.N) : undefined; -const maxChildTokenSize: number | undefined = knowledgeBase - .chunking_configuration.M.max_child_token_size +const maxChildTokenSize = knowledgeBase.chunking_configuration.M + .max_child_token_size ? Number(knowledgeBase.chunking_configuration.M.max_child_token_size.N) : undefined; -const bufferSize: number | undefined = knowledgeBase.chunking_configuration.M - .buffer_size +const bufferSize = knowledgeBase.chunking_configuration.M.buffer_size ? Number(knowledgeBase.chunking_configuration.M.buffer_size.N) : undefined; -const breakpointPercentileThreshold: number | undefined = knowledgeBase - .chunking_configuration.M.breakpoint_percentile_threshold +const breakpointPercentileThreshold = knowledgeBase.chunking_configuration.M + .breakpoint_percentile_threshold ? Number( knowledgeBase.chunking_configuration.M.breakpoint_percentile_threshold.N ) : undefined; -const is_guardrail_enabled: boolean | undefined = - guardrails.is_guardrail_enabled - ? Boolean(guardrails.is_guardrail_enabled.BOOL) - : undefined; -const hateThreshold: number | undefined = guardrails.hate_threshold + +// Process guardrails configuration +const is_guardrail_enabled = guardrails.is_guardrail_enabled + ? Boolean(guardrails.is_guardrail_enabled.BOOL) + : undefined; +const hateThreshold = guardrails.hate_threshold ? Number(guardrails.hate_threshold.N) : undefined; -const insultsThreshold: number | undefined = guardrails.insults_threshold +const insultsThreshold = guardrails.insults_threshold ? Number(guardrails.insults_threshold.N) : undefined; -const sexualThreshold: number | undefined = guardrails.sexual_threshold +const sexualThreshold = guardrails.sexual_threshold ? Number(guardrails.sexual_threshold.N) : undefined; -const violenceThreshold: number | undefined = guardrails.violence_threshold +const violenceThreshold = guardrails.violence_threshold ? Number(guardrails.violence_threshold.N) : undefined; -const misconductThreshold: number | undefined = guardrails.misconduct_threshold +const misconductThreshold = guardrails.misconduct_threshold ? Number(guardrails.misconduct_threshold.N) : undefined; -const groundingThreshold: number | undefined = guardrails.grounding_threshold +const groundingThreshold = guardrails.grounding_threshold ? Number(guardrails.grounding_threshold.N) : undefined; -const relevanceThreshold: number | undefined = guardrails.relevance_threshold +const relevanceThreshold = guardrails.relevance_threshold ? Number(guardrails.relevance_threshold.N) : undefined; -const guardrailArn: number | undefined = guardrails.guardrail_arn +const guardrailArn = guardrails.guardrail_arn ? Number(guardrails.guardrail_arn.N) : undefined; -const guardrailVersion: number | undefined = guardrails.guardrail_version +const guardrailVersion = guardrails.guardrail_version ? Number(guardrails.guardrail_version.N) : undefined; + +// Get chunking strategy const chunkingStrategy = getChunkingStrategy( knowledgeBase.chunking_configuration.M.chunking_strategy.S, knowledgeBase.embeddings_model.S, @@ -149,40 +142,27 @@ const chunkingStrategy = getChunkingStrategy( } ); -console.log("embeddingsModel: ", embeddingsModel); -console.log("chunkingStrategy: ", chunkingStrategy); -console.log("existKnowledgeBaseId: ", existKnowledgeBaseId); -console.log("maxTokens: ", maxTokens); -console.log("instruction: ", instruction); -console.log("is_guardrail_enabled: ", is_guardrail_enabled); -console.log("hateThreshold: ", hateThreshold); -console.log("insultsThreshold: ", insultsThreshold); -console.log("sexualThreshold: ", sexualThreshold); -console.log("violenceThreshold: ", violenceThreshold); -console.log("misconductThreshold: ", misconductThreshold); -console.log("relevanceThreshold: ", relevanceThreshold); -console.log("guardrailArn: ", guardrailArn); -console.log("guardrailVersion: ", guardrailVersion); -console.log("parsingModel: ", parsingModel); -console.log("crawlingScope: ", crawlingScope); - -if (analyzer) { - console.log( - "Analyzer: ", - JSON.stringify(knowledgeBase.open_search.M.analyzer, null, 2) - ); -} else { - console.log("Analyzer is undefined or null."); -} - -console.log("overlapPercentage: ", overlapPercentage); +console.log( + "Knowledge Base Configuration:", + JSON.stringify({ + embeddingsModel: embeddingsModel.toString(), + chunkingStrategy: chunkingStrategy.toString(), + existKnowledgeBaseId, + maxTokens, + instruction, + is_guardrail_enabled, + parsingModel: parsingModel?.toString(), + crawlingScope: crawlingScope?.toString(), + analyzer: analyzer ? "configured" : "undefined", + }) +); +// Create the stack const bedrockCustomBotStack = new BedrockCustomBotStack( app, - `BrChatKbStack${botId}`, + `${params.envPrefix}${sepHyphen}BrChatKbStack${botId}`, { env: { - // account: process.env.CDK_DEFAULT_ACCOUNT, region: params.bedrockRegion, }, ownerUserId, @@ -192,8 +172,7 @@ const bedrockCustomBotStack = new BedrockCustomBotStack( crawlingScope, crawlingFilters, existKnowledgeBaseId, - bedrockClaudeChatDocumentBucketName: - BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME, + bedrockClaudeChatDocumentBucketName: params.documentBucketName, chunkingStrategy, existingS3Urls, sourceUrls, @@ -216,3 +195,5 @@ const bedrockCustomBotStack = new BedrockCustomBotStack( useStandbyReplicas, } ); + +cdk.Tags.of(app).add("CDKEnvironment", params.envName); diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index 21a22c8f5..da8b28a7f 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -25,6 +25,8 @@ import * as path from "path"; import { BedrockCustomBotCodebuild } from "./constructs/bedrock-custom-bot-codebuild"; export interface BedrockChatStackProps extends StackProps { + readonly envName: string; + readonly envPrefix?: string; readonly bedrockRegion: string; readonly webAclId: string; readonly identityProviders: TIdentityProvider[]; @@ -51,6 +53,7 @@ export class BedrockChatStack extends cdk.Stack { ...props, }); + const sepHyphen = props.envPrefix ? "-" : ""; const idp = identityProvider(props.identityProviders); const accessLogBucket = new Bucket(this, "AccessLogBucket", { @@ -242,19 +245,19 @@ export class BedrockChatStack extends cdk.Stack { // Outputs for API publication new CfnOutput(this, "PublishedApiWebAclArn", { value: webAclForPublishedApi.webAclArn, - exportName: "PublishedApiWebAclArn", + exportName: `${props.envPrefix}${sepHyphen}PublishedApiWebAclArn`, }); new CfnOutput(this, "ConversationTableName", { value: database.table.tableName, - exportName: "BedrockClaudeChatConversationTableName", + exportName: `${props.envPrefix}${sepHyphen}BedrockClaudeChatConversationTableName`, }); new CfnOutput(this, "TableAccessRoleArn", { value: database.tableAccessRole.roleArn, - exportName: "BedrockClaudeChatTableAccessRoleArn", + exportName: `${props.envPrefix}${sepHyphen}BedrockClaudeChatTableAccessRoleArn`, }); new CfnOutput(this, "LargeMessageBucketName", { value: largeMessageBucket.bucketName, - exportName: "BedrockClaudeChatLargeMessageBucketName", + exportName: `${props.envPrefix}${sepHyphen}BedrockClaudeChatLargeMessageBucketName`, }); } } diff --git a/cdk/lib/constructs/api-publish-codebuild.ts b/cdk/lib/constructs/api-publish-codebuild.ts index 6cdec448b..8a2a1fdd1 100644 --- a/cdk/lib/constructs/api-publish-codebuild.ts +++ b/cdk/lib/constructs/api-publish-codebuild.ts @@ -7,6 +7,8 @@ import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; import { NagSuppressions } from "cdk-nag"; export interface ApiPublishCodebuildProps { + readonly envName?: string; + readonly envPrefix?: string; readonly sourceBucket: s3.Bucket; } @@ -29,6 +31,8 @@ export class ApiPublishCodebuild extends Construct { privileged: true, }, environmentVariables: { + ENV_NAME: { value: props.envName }, + ENV_PREFIX: { value: props.envPrefix }, // Need to be overridden when invoke the project // PUBLISHED_API_THROTTLE_RATE_LIMIT: { value: undefined }, // PUBLISHED_API_THROTTLE_BURST_LIMIT: { value: undefined }, diff --git a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts index 44a8d3385..142e5a048 100644 --- a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts +++ b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts @@ -5,6 +5,8 @@ import * as iam from "aws-cdk-lib/aws-iam"; import { NagSuppressions } from "cdk-nag"; export interface BedrockCustomBotCodebuildProps { + readonly envName?: string; + readonly envPrefix?: string; readonly sourceBucket: s3.Bucket; } @@ -28,6 +30,8 @@ export class BedrockCustomBotCodebuild extends Construct { privileged: true, }, environmentVariables: { + ENV_NAME: { value: props.envName }, + ENV_PREFIX: { value: props.envPrefix }, PK: { value: "" }, SK: { value: "" }, BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: { diff --git a/cdk/lib/constructs/usage-analysis.ts b/cdk/lib/constructs/usage-analysis.ts index 5f2e5afc2..366b80dc4 100644 --- a/cdk/lib/constructs/usage-analysis.ts +++ b/cdk/lib/constructs/usage-analysis.ts @@ -14,6 +14,7 @@ import * as iam from "aws-cdk-lib/aws-iam"; import * as logs from "aws-cdk-lib/aws-logs"; export interface UsageAnalysisProps { + envPrefix?: string; sourceDatabase: Database; accessLogBucket?: s3.Bucket; } @@ -31,7 +32,9 @@ export class UsageAnalysis extends Construct { const GLUE_DATABASE_NAME = `${Stack.of( this ).stackName.toLowerCase()}_usage_analysis`; - const DDB_EXPORT_TABLE_NAME = "ddb_export"; + + const sepUnderscore = props.envPrefix ? "_" : ""; + const DDB_EXPORT_TABLE_NAME = `${props.envPrefix}${sepUnderscore}ddb_export`; // Bucket to export DynamoDB data const ddbBucket = new s3.Bucket(this, "DdbBucket", { diff --git a/cdk/lib/constructs/webacl-for-published-api.ts b/cdk/lib/constructs/webacl-for-published-api.ts index 3de38f652..19ca05693 100644 --- a/cdk/lib/constructs/webacl-for-published-api.ts +++ b/cdk/lib/constructs/webacl-for-published-api.ts @@ -3,6 +3,7 @@ import * as wafv2 from "aws-cdk-lib/aws-wafv2"; import { CfnOutput } from "aws-cdk-lib"; export interface WebAclForPublishedApiProps { + envPrefix?: string; readonly allowedIpV4AddressRanges: string[]; readonly allowedIpV6AddressRanges: string[]; } @@ -12,6 +13,7 @@ export class WebAclForPublishedApi extends Construct { constructor(scope: Construct, id: string, props: WebAclForPublishedApiProps) { super(scope, id); + const sepHyphen = props.envPrefix ? "-" : ""; const rules: wafv2.CfnWebACL.RuleProperty[] = []; if (props.allowedIpV4AddressRanges.length > 0) { @@ -58,7 +60,7 @@ export class WebAclForPublishedApi extends Construct { if (rules.length > 0) { const webAcl = new wafv2.CfnWebACL(this, "WebAcl", { defaultAction: { block: {} }, - name: `ApiWebAcl-${id}`, + name: `${props.envPrefix}${sepHyphen}ApiWebAcl-${id}`, scope: "REGIONAL", visibilityConfig: { cloudWatchMetricsEnabled: true, diff --git a/cdk/lib/frontend-waf-stack.ts b/cdk/lib/frontend-waf-stack.ts index 8ee2a64ae..8d1a9118e 100644 --- a/cdk/lib/frontend-waf-stack.ts +++ b/cdk/lib/frontend-waf-stack.ts @@ -4,6 +4,7 @@ import * as wafv2 from "aws-cdk-lib/aws-wafv2"; import { Construct } from "constructs"; interface FrontendWafStackProps extends StackProps { + readonly envPrefix?: string; readonly allowedIpV4AddressRanges: string[]; readonly allowedIpV6AddressRanges: string[]; } @@ -25,6 +26,7 @@ export class FrontendWafStack extends Stack { constructor(scope: Construct, id: string, props: FrontendWafStackProps) { super(scope, id, props); + const sepHyphen = props.envPrefix ? "-" : ""; const rules: wafv2.CfnWebACL.RuleProperty[] = []; // create Ipset for ACL @@ -83,7 +85,7 @@ export class FrontendWafStack extends Stack { if (rules.length > 0) { const webAcl = new wafv2.CfnWebACL(this, "WebAcl", { defaultAction: { block: {} }, - name: "FrontendWebAcl", + name: `${props.envPrefix}${sepHyphen}FrontendWebAcl`, scope: "CLOUDFRONT", visibilityConfig: { cloudWatchMetricsEnabled: true, diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index 38193dc74..2bcfe95af 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -6,18 +6,29 @@ import { App } from "aws-cdk-lib"; * Base parameters schema that is common across all entry points */ const BaseParametersSchema = z.object({ + // CDK Environments + envName: z.string().default("default"), + envPrefix: z.string().default(""), + // Bedrock configuration bedrockRegion: z.string().default("us-east-1"), }); +/** + * Helper function to get environment variables with fallback + * @param name Environment variable name + * @param defaultValue Default value if environment variable is not set + * @returns The environment variable value or default value + */ +function getEnvVar(name: string, defaultValue?: string): string | undefined { + const value = process.env[name]; + return value !== undefined ? value : defaultValue; +} + /** * Parameters schema for the main Bedrock Chat application */ const BedrockChatParametersSchema = BaseParametersSchema.extend({ - // CDK Environments - envName: z.string().default("default"), - envPrefix: z.string().default(""), - // Bedrock configuration enableMistral: z.boolean().default(false), enableBedrockCrossRegionInference: z.boolean().default(true), @@ -75,7 +86,16 @@ const ApiPublishParametersSchema = BaseParametersSchema.extend({ /** * Parameters schema for Bedrock Custom Bot */ -const BedrockCustomBotParametersSchema = BaseParametersSchema; +const BedrockCustomBotParametersSchema = BaseParametersSchema.extend({ + // Bot configuration + pk: z.string(), + sk: z.string(), + documentBucketName: z.string(), + knowledge: z.string(), + knowledgeBase: z.string(), + guardrails: z.string(), + useStandByReplicas: z.boolean().default(false), +}); /** * Type definitions for each parameter set @@ -102,7 +122,7 @@ export type BedrockCustomBotParameters = z.infer< /** * Parse and validate parameters for the main Bedrock Chat application. - * If you omit parametersInput, context parameters are used. + * If you omit parametersInput, context parameters and environment variables are used. * @param app CDK App instance * @param parametersInput (optional) Input parameters that should be used instead of context parameters * @returns Validated parameters object @@ -116,12 +136,16 @@ export function resolveBedrockChatParameters( return BedrockChatParametersSchema.parse(parametersInput); } + // Get environment variables + const envName = getEnvVar("ENV_NAME", "default"); + const envPrefix = getEnvVar("ENV_PREFIX", ""); + // Otherwise, get parameters from context const identityProviders = app.node.tryGetContext("identityProviders"); const contextParams = { - envName: "default", - envPrefix: "", + envName, + envPrefix, bedrockRegion: app.node.tryGetContext("bedrockRegion"), enableMistral: app.node.tryGetContext("enableMistral"), allowedIpV4AddressRanges: app.node.tryGetContext( @@ -161,7 +185,7 @@ export function resolveBedrockChatParameters( /** * Get Bedrock Chat parameters based on environment name. * If you omit envName, "default" is used. - * If you omit parametersInput, context parameters are used. + * If you omit parametersInput, context parameters and environment variables are used. * @param app CDK App instance * @param envName (optional) Environment name. Used as map key if provided * @param paramsMap (optional) Map of parameters. If not provided, use context parameters @@ -203,7 +227,8 @@ export function getBedrockChatParameters( } /** - * Parse and validate parameters for API publishing + * Parse and validate parameters for API publishing. + * If you omit parametersInput, context parameters and environment variables are used. * @param app CDK App instance * @param parametersInput Optional input parameters that override context values * @returns Validated parameters object @@ -217,7 +242,11 @@ export function resolveApiPublishParameters( return ApiPublishParametersSchema.parse(parametersInput); } - // Otherwise, get parameters from context + // Get environment variables + const envName = getEnvVar("ENV_NAME", "default"); + const envPrefix = getEnvVar("ENV_PREFIX", ""); + + // Get parameters from context const publishedApiThrottleRateLimit = app.node.tryGetContext( "publishedApiThrottleRateLimit" ); @@ -232,6 +261,8 @@ export function resolveApiPublishParameters( ); const contextParams = { + envName, + envPrefix, bedrockRegion: app.node.tryGetContext("bedrockRegion"), publishedApiThrottleRateLimit: publishedApiThrottleRateLimit ? Number(publishedApiThrottleRateLimit) @@ -254,7 +285,8 @@ export function resolveApiPublishParameters( } /** - * Parse and validate parameters for Bedrock Custom Bot + * Parse and validate parameters for Bedrock Custom Bot. + * If you omit parametersInput, context parameters and environment variables are used. * @param app CDK App instance * @param parametersInput Optional input parameters that override context values * @returns Validated parameters object @@ -268,9 +300,22 @@ export function resolveBedrockCustomBotParameters( return BedrockCustomBotParametersSchema.parse(parametersInput); } - // Otherwise, get parameters from context + // Get environment variables + const envName = getEnvVar("ENV_NAME", "default"); + const envPrefix = getEnvVar("ENV_PREFIX", ""); + + // Get parameters from context and environment variables const contextParams = { + envName, + envPrefix, bedrockRegion: app.node.tryGetContext("bedrockRegion"), + pk: getEnvVar("PK"), + sk: getEnvVar("SK"), + documentBucketName: getEnvVar("BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME"), + knowledge: getEnvVar("KNOWLEDGE"), + knowledgeBase: getEnvVar("BEDROCK_KNOWLEDGE_BASE"), + guardrails: getEnvVar("BEDROCK_GUARDRAILS"), + useStandByReplicas: getEnvVar("USE_STAND_BY_REPLICAS") === "true", }; return BedrockCustomBotParametersSchema.parse(contextParams); diff --git a/cdk/test/utils/parameter-models.test.ts b/cdk/test/utils/parameter-models.test.ts index 33ace27f0..a8586169e 100644 --- a/cdk/test/utils/parameter-models.test.ts +++ b/cdk/test/utils/parameter-models.test.ts @@ -564,6 +564,13 @@ describe("resolveBedrockCustomBotParameters", () => { const app = createTestApp(); const inputParams = { bedrockRegion: "eu-west-1", + pk: "test-pk", + sk: "test-sk", + documentBucketName: "test-bucket", + knowledge: '{"test": "knowledge"}', + knowledgeBase: '{"test": "kb"}', + guardrails: '{"test": "guardrails"}', + useStandByReplicas: true }; // When @@ -571,32 +578,67 @@ describe("resolveBedrockCustomBotParameters", () => { // Then expect(result.bedrockRegion).toBe("eu-west-1"); + expect(result.pk).toBe("test-pk"); + expect(result.sk).toBe("test-sk"); + expect(result.documentBucketName).toBe("test-bucket"); + expect(result.knowledge).toBe('{"test": "knowledge"}'); + expect(result.knowledgeBase).toBe('{"test": "kb"}'); + expect(result.guardrails).toBe('{"test": "guardrails"}'); + expect(result.useStandByReplicas).toBe(true); }); - test("should get parameters from context when parametersInput is not provided", () => { + test("should get parameters from context and environment variables when parametersInput is not provided", () => { // Given const app = createTestApp({ bedrockRegion: "ap-northeast-1", }); + + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + ENV_NAME: "test-env", + ENV_PREFIX: "test-prefix", + PK: "env-pk", + SK: "env-sk", + BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: "env-bucket", + KNOWLEDGE: '{"env": "knowledge"}', + BEDROCK_KNOWLEDGE_BASE: '{"env": "kb"}', + BEDROCK_GUARDRAILS: '{"env": "guardrails"}', + USE_STAND_BY_REPLICAS: "true" + }; - // When - const result = resolveBedrockCustomBotParameters(app); - - // Then - expect(result.bedrockRegion).toBe("ap-northeast-1"); + try { + // When + const result = resolveBedrockCustomBotParameters(app); + + // Then + expect(result.bedrockRegion).toBe("ap-northeast-1"); + expect(result.envName).toBe("test-env"); + expect(result.envPrefix).toBe("test-prefix"); + expect(result.pk).toBe("env-pk"); + expect(result.sk).toBe("env-sk"); + expect(result.documentBucketName).toBe("env-bucket"); + expect(result.knowledge).toBe('{"env": "knowledge"}'); + expect(result.knowledgeBase).toBe('{"env": "kb"}'); + expect(result.guardrails).toBe('{"env": "guardrails"}'); + expect(result.useStandByReplicas).toBe(true); + } finally { + // Restore original environment + process.env = originalEnv; + } }); }); describe("Parameter Validation", () => { - test("should apply default values when required parameters are missing", () => { + test("should throw ZodError when required parameters are missing", () => { // Given const app = createTestApp(); - - // When - const result = resolveBedrockCustomBotParameters(app); - - // Then - expect(result.bedrockRegion).toBe("us-east-1"); // default value + + // When/Then + expect(() => { + resolveBedrockCustomBotParameters(app); + }).toThrow(); }); test("should throw ZodError when invalid parameter is specified", () => { @@ -604,6 +646,12 @@ describe("resolveBedrockCustomBotParameters", () => { const app = createTestApp(); const invalidParams = { bedrockRegion: 123, // number instead of string + pk: "test-pk", + sk: "test-sk", + documentBucketName: "test-bucket", + knowledge: '{"test": "knowledge"}', + knowledgeBase: '{"test": "kb"}', + guardrails: '{"test": "guardrails"}' }; // When/Then From 62b00066a36b1a33efd59df12a82d7a548643292 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Thu, 13 Mar 2025 08:55:59 +0900 Subject: [PATCH 07/22] fix(cdk): passing envName and prefix --- cdk/bin/bedrock-chat.ts | 2 ++ cdk/lib/bedrock-chat-stack.ts | 4 ++++ cdk/lib/constructs/api-publish-codebuild.ts | 2 +- cdk/lib/constructs/bedrock-custom-bot-codebuild.ts | 2 +- 4 files changed, 8 insertions(+), 2 deletions(-) diff --git a/cdk/bin/bedrock-chat.ts b/cdk/bin/bedrock-chat.ts index ffc8d8124..c56058456 100644 --- a/cdk/bin/bedrock-chat.ts +++ b/cdk/bin/bedrock-chat.ts @@ -67,6 +67,8 @@ const chat = new BedrockChatStack( // account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, }, + envName: params.envName, + envPrefix: params.envPrefix, crossRegionReferences: true, bedrockRegion: params.bedrockRegion, webAclId: waf.webAclArn.value, diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index da8b28a7f..4fe318617 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -113,6 +113,8 @@ export class BedrockChatStack extends cdk.Stack { "ApiPublishCodebuild", { sourceBucket, + envName: props.envName, + envPrefix: props.envPrefix, } ); // CodeBuild used for KnowledgeBase @@ -121,6 +123,8 @@ export class BedrockChatStack extends cdk.Stack { "BedrockKnowledgeBaseCodebuild", { sourceBucket, + envName: props.envName, + envPrefix: props.envPrefix, } ); diff --git a/cdk/lib/constructs/api-publish-codebuild.ts b/cdk/lib/constructs/api-publish-codebuild.ts index 8a2a1fdd1..13b6dc5f3 100644 --- a/cdk/lib/constructs/api-publish-codebuild.ts +++ b/cdk/lib/constructs/api-publish-codebuild.ts @@ -7,7 +7,7 @@ import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; import { NagSuppressions } from "cdk-nag"; export interface ApiPublishCodebuildProps { - readonly envName?: string; + readonly envName: string; readonly envPrefix?: string; readonly sourceBucket: s3.Bucket; } diff --git a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts index 142e5a048..85ebeb862 100644 --- a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts +++ b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts @@ -5,7 +5,7 @@ import * as iam from "aws-cdk-lib/aws-iam"; import { NagSuppressions } from "cdk-nag"; export interface BedrockCustomBotCodebuildProps { - readonly envName?: string; + readonly envName: string; readonly envPrefix?: string; readonly sourceBucket: s3.Bucket; } From 4857c633ced42aae8e67c3d1f181b49b09a3f888 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Thu, 13 Mar 2025 09:24:34 +0900 Subject: [PATCH 08/22] fix(cdk): passing envName and prefix --- cdk/lib/utils/parameter-models.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index 2bcfe95af..37a97f618 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -137,8 +137,8 @@ export function resolveBedrockChatParameters( } // Get environment variables - const envName = getEnvVar("ENV_NAME", "default"); - const envPrefix = getEnvVar("ENV_PREFIX", ""); + const envName = app.node.tryGetContext("envName") || "default"; + const envPrefix = envName === "default" ? "" : envName; // Otherwise, get parameters from context const identityProviders = app.node.tryGetContext("identityProviders"); From afea78796dd6ae2553712c213630c5246ef91e18 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Thu, 13 Mar 2025 10:33:02 +0900 Subject: [PATCH 09/22] fix(cdk): envPrefix is required --- cdk/bin/bedrock-chat.ts | 1 + cdk/lib/bedrock-chat-stack.ts | 4 +++- cdk/lib/constructs/usage-analysis.ts | 2 +- cdk/lib/constructs/webacl-for-published-api.ts | 2 +- cdk/lib/frontend-waf-stack.ts | 2 +- 5 files changed, 7 insertions(+), 4 deletions(-) diff --git a/cdk/bin/bedrock-chat.ts b/cdk/bin/bedrock-chat.ts index c56058456..c5ad70183 100644 --- a/cdk/bin/bedrock-chat.ts +++ b/cdk/bin/bedrock-chat.ts @@ -38,6 +38,7 @@ const waf = new FrontendWafStack( // account: process.env.CDK_DEFAULT_ACCOUNT, region: "us-east-1", }, + envPrefix: params.envPrefix, allowedIpV4AddressRanges: params.allowedIpV4AddressRanges, allowedIpV6AddressRanges: params.allowedIpV6AddressRanges, } diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index 4fe318617..22aaea7e2 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -26,7 +26,7 @@ import { BedrockCustomBotCodebuild } from "./constructs/bedrock-custom-bot-codeb export interface BedrockChatStackProps extends StackProps { readonly envName: string; - readonly envPrefix?: string; + readonly envPrefix: string; readonly bedrockRegion: string; readonly webAclId: string; readonly identityProviders: TIdentityProvider[]; @@ -162,6 +162,7 @@ export class BedrockChatStack extends cdk.Stack { }); const usageAnalysis = new UsageAnalysis(this, "UsageAnalysis", { + envPrefix: props.envPrefix, accessLogBucket, sourceDatabase: database, }); @@ -234,6 +235,7 @@ export class BedrockChatStack extends cdk.Stack { this, "WebAclForPublishedApi", { + envPrefix: props.envPrefix, allowedIpV4AddressRanges: props.publishedApiAllowedIpV4AddressRanges, allowedIpV6AddressRanges: props.publishedApiAllowedIpV6AddressRanges, } diff --git a/cdk/lib/constructs/usage-analysis.ts b/cdk/lib/constructs/usage-analysis.ts index 366b80dc4..6e9ebf34a 100644 --- a/cdk/lib/constructs/usage-analysis.ts +++ b/cdk/lib/constructs/usage-analysis.ts @@ -14,7 +14,7 @@ import * as iam from "aws-cdk-lib/aws-iam"; import * as logs from "aws-cdk-lib/aws-logs"; export interface UsageAnalysisProps { - envPrefix?: string; + envPrefix: string; sourceDatabase: Database; accessLogBucket?: s3.Bucket; } diff --git a/cdk/lib/constructs/webacl-for-published-api.ts b/cdk/lib/constructs/webacl-for-published-api.ts index 19ca05693..fa10f9d4b 100644 --- a/cdk/lib/constructs/webacl-for-published-api.ts +++ b/cdk/lib/constructs/webacl-for-published-api.ts @@ -3,7 +3,7 @@ import * as wafv2 from "aws-cdk-lib/aws-wafv2"; import { CfnOutput } from "aws-cdk-lib"; export interface WebAclForPublishedApiProps { - envPrefix?: string; + envPrefix: string; readonly allowedIpV4AddressRanges: string[]; readonly allowedIpV6AddressRanges: string[]; } diff --git a/cdk/lib/frontend-waf-stack.ts b/cdk/lib/frontend-waf-stack.ts index 8d1a9118e..21eb27235 100644 --- a/cdk/lib/frontend-waf-stack.ts +++ b/cdk/lib/frontend-waf-stack.ts @@ -4,7 +4,7 @@ import * as wafv2 from "aws-cdk-lib/aws-wafv2"; import { Construct } from "constructs"; interface FrontendWafStackProps extends StackProps { - readonly envPrefix?: string; + readonly envPrefix: string; readonly allowedIpV4AddressRanges: string[]; readonly allowedIpV6AddressRanges: string[]; } From 9cdcb2151b82f2d456b3b616bd8032602edd9847 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Thu, 13 Mar 2025 21:27:08 +0900 Subject: [PATCH 10/22] wip --- cdk/lib/bedrock-chat-stack.ts | 2 + cdk/lib/constructs/api-publish-codebuild.ts | 22 ++---- .../bedrock-custom-bot-codebuild.ts | 12 +--- cdk/lib/utils/parameter-models.ts | 6 +- cdk/test/utils/parameter-models.test.ts | 71 ++++++++++++------- 5 files changed, 58 insertions(+), 55 deletions(-) diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index 22aaea7e2..962bcf859 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -115,6 +115,7 @@ export class BedrockChatStack extends cdk.Stack { sourceBucket, envName: props.envName, envPrefix: props.envPrefix, + bedrockRegion: props.bedrockRegion, } ); // CodeBuild used for KnowledgeBase @@ -125,6 +126,7 @@ export class BedrockChatStack extends cdk.Stack { sourceBucket, envName: props.envName, envPrefix: props.envPrefix, + bedrockRegion: props.bedrockRegion, } ); diff --git a/cdk/lib/constructs/api-publish-codebuild.ts b/cdk/lib/constructs/api-publish-codebuild.ts index 13b6dc5f3..bbf4c43c3 100644 --- a/cdk/lib/constructs/api-publish-codebuild.ts +++ b/cdk/lib/constructs/api-publish-codebuild.ts @@ -3,12 +3,12 @@ import * as codebuild from "aws-cdk-lib/aws-codebuild"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as iam from "aws-cdk-lib/aws-iam"; import * as logs from "aws-cdk-lib/aws-logs"; -import * as secretsmanager from "aws-cdk-lib/aws-secretsmanager"; import { NagSuppressions } from "cdk-nag"; export interface ApiPublishCodebuildProps { readonly envName: string; - readonly envPrefix?: string; + readonly envPrefix: string; + readonly bedrockRegion: string; readonly sourceBucket: s3.Bucket; } @@ -33,14 +33,7 @@ export class ApiPublishCodebuild extends Construct { environmentVariables: { ENV_NAME: { value: props.envName }, ENV_PREFIX: { value: props.envPrefix }, - // Need to be overridden when invoke the project - // PUBLISHED_API_THROTTLE_RATE_LIMIT: { value: undefined }, - // PUBLISHED_API_THROTTLE_BURST_LIMIT: { value: undefined }, - // PUBLISHED_API_QUOTA_LIMIT: { value: undefined }, - // PUBLISHED_API_QUOTA_PERIOD: { value: undefined }, - PUBLISHED_API_DEPLOYMENT_STAGE: { value: "api" }, - PUBLISHED_API_ID: { value: "xy1234" }, - PUBLISHED_API_ALLOWED_ORIGINS: { value: '["*"]' }, + BEDROCK_REGION: { value: props.bedrockRegion }, }, buildSpec: codebuild.BuildSpec.fromObject({ version: "0.2", @@ -57,14 +50,7 @@ export class ApiPublishCodebuild extends Construct { "npm ci", // Replace cdk's entrypoint. This is a workaround to avoid the issue that cdk synthesize all stacks. "sed -i 's|bin/bedrock-chat.ts|bin/api-publish.ts|' cdk.json", - `npx cdk deploy --require-approval never ApiPublishmentStack$PUBLISHED_API_ID \\ - -c publishedApiThrottleRateLimit=$PUBLISHED_API_THROTTLE_RATE_LIMIT \\ - -c publishedApiThrottleBurstLimit=$PUBLISHED_API_THROTTLE_BURST_LIMIT \\ - -c publishedApiQuotaLimit=$PUBLISHED_API_QUOTA_LIMIT \\ - -c publishedApiQuotaPeriod=$PUBLISHED_API_QUOTA_PERIOD \\ - -c publishedApiDeploymentStage=$PUBLISHED_API_DEPLOYMENT_STAGE \\ - -c publishedApiId=$PUBLISHED_API_ID \\ - -c publishedApiAllowedOrigins=$PUBLISHED_API_ALLOWED_ORIGINS`, + "npx cdk deploy --require-approval never ApiPublishmentStack$PUBLISHED_API_ID", ], }, }, diff --git a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts index 85ebeb862..4bb1940ed 100644 --- a/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts +++ b/cdk/lib/constructs/bedrock-custom-bot-codebuild.ts @@ -6,7 +6,8 @@ import { NagSuppressions } from "cdk-nag"; export interface BedrockCustomBotCodebuildProps { readonly envName: string; - readonly envPrefix?: string; + readonly envPrefix: string; + readonly bedrockRegion: string; readonly sourceBucket: s3.Bucket; } @@ -32,14 +33,7 @@ export class BedrockCustomBotCodebuild extends Construct { environmentVariables: { ENV_NAME: { value: props.envName }, ENV_PREFIX: { value: props.envPrefix }, - PK: { value: "" }, - SK: { value: "" }, - BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: { - value: "", - }, - KNOWLEDGE: { value: "" }, - BEDROCK_KNOWLEDGE_BASE: { value: "" }, - BEDROCK_GUARDRAILS: { value: "" }, + BEDROCK_REGION: { value: props.bedrockRegion }, }, buildSpec: codebuild.BuildSpec.fromObject({ version: "0.2", diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index 37a97f618..b3331e737 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -78,7 +78,7 @@ const ApiPublishParametersSchema = BaseParametersSchema.extend({ publishedApiThrottleBurstLimit: z.number().optional(), publishedApiQuotaLimit: z.number().optional(), publishedApiQuotaPeriod: z.enum(["DAY", "WEEK", "MONTH"]).optional(), - publishedApiDeploymentStage: z.string().optional(), + publishedApiDeploymentStage: z.string().default("api"), publishedApiId: z.string().optional(), publishedApiAllowedOrigins: z.string().default('["*"]'), }); @@ -228,9 +228,9 @@ export function getBedrockChatParameters( /** * Parse and validate parameters for API publishing. - * If you omit parametersInput, context parameters and environment variables are used. + * If you omit parametersInput, environment variables are used. * @param app CDK App instance - * @param parametersInput Optional input parameters that override context values + * @param parametersInput Optional input parameters that override environment values * @returns Validated parameters object */ export function resolveApiPublishParameters( diff --git a/cdk/test/utils/parameter-models.test.ts b/cdk/test/utils/parameter-models.test.ts index a8586169e..32bb62785 100644 --- a/cdk/test/utils/parameter-models.test.ts +++ b/cdk/test/utils/parameter-models.test.ts @@ -447,21 +447,31 @@ describe("resolveApiPublishParameters", () => { expect(result.publishedApiAllowedOrigins).toBe('["https://example.com"]'); }); - test("should get parameters from context when parametersInput is not provided", () => { + test("should get parameters from environment variables when parametersInput is not provided", () => { // Given - const app = createTestApp({ - bedrockRegion: "ap-northeast-1", - publishedApiThrottleRateLimit: 200, - publishedApiAllowedOrigins: '["https://test.com"]', - }); + const app = createTestApp(); + + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + BEDROCK_REGION: "us-east-1", // Changed to match default value + PUBLISHED_API_THROTTLE_RATE_LIMIT: "200", + PUBLISHED_API_ALLOWED_ORIGINS: '["https://test.com"]', + }; - // When - const result = resolveApiPublishParameters(app); + try { + // When + const result = resolveApiPublishParameters(app); - // Then - expect(result.bedrockRegion).toBe("ap-northeast-1"); - expect(result.publishedApiThrottleRateLimit).toBe(200); - expect(result.publishedApiAllowedOrigins).toBe('["https://test.com"]'); + // Then + expect(result.bedrockRegion).toBe("us-east-1"); + expect(result.publishedApiThrottleRateLimit).toBe(200); + expect(result.publishedApiAllowedOrigins).toBe('["https://test.com"]'); + } finally { + // Restore original environment + process.env = originalEnv; + } }); }); @@ -481,19 +491,29 @@ describe("resolveApiPublishParameters", () => { test("should convert string numeric parameters to numbers", () => { // Given - const app = createTestApp({ - publishedApiThrottleRateLimit: "100", - publishedApiThrottleBurstLimit: "200", - publishedApiQuotaLimit: "1000", - }); + const app = createTestApp(); + + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + PUBLISHED_API_THROTTLE_RATE_LIMIT: "100", + PUBLISHED_API_THROTTLE_BURST_LIMIT: "200", + PUBLISHED_API_QUOTA_LIMIT: "1000", + }; - // When - const result = resolveApiPublishParameters(app); + try { + // When + const result = resolveApiPublishParameters(app); - // Then - expect(result.publishedApiThrottleRateLimit).toBe(100); - expect(result.publishedApiThrottleBurstLimit).toBe(200); - expect(result.publishedApiQuotaLimit).toBe(1000); + // Then + expect(result.publishedApiThrottleRateLimit).toBe(100); + expect(result.publishedApiThrottleBurstLimit).toBe(200); + expect(result.publishedApiQuotaLimit).toBe(1000); + } finally { + // Restore original environment + process.env = originalEnv; + } }); test("should correctly parse all parameters when specified", () => { @@ -605,7 +625,8 @@ describe("resolveBedrockCustomBotParameters", () => { KNOWLEDGE: '{"env": "knowledge"}', BEDROCK_KNOWLEDGE_BASE: '{"env": "kb"}', BEDROCK_GUARDRAILS: '{"env": "guardrails"}', - USE_STAND_BY_REPLICAS: "true" + USE_STAND_BY_REPLICAS: "true", + BEDROCK_REGION: "us-east-1" // This will be overridden by context }; try { @@ -613,7 +634,7 @@ describe("resolveBedrockCustomBotParameters", () => { const result = resolveBedrockCustomBotParameters(app); // Then - expect(result.bedrockRegion).toBe("ap-northeast-1"); + expect(result.bedrockRegion).toBe("ap-northeast-1"); // From context expect(result.envName).toBe("test-env"); expect(result.envPrefix).toBe("test-prefix"); expect(result.pk).toBe("env-pk"); From 475dd86e3d1cc71e9568e9646c88daa08a940efe Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Thu, 13 Mar 2025 22:46:54 +0900 Subject: [PATCH 11/22] remove unnecessary arguments --- cdk/bin/api-publish.ts | 2 +- cdk/bin/bedrock-custom-bot.ts | 2 +- cdk/lib/utils/parameter-models.ts | 135 ++++++-------- cdk/test/utils/parameter-models.test.ts | 231 +++++++++++------------- 4 files changed, 168 insertions(+), 202 deletions(-) diff --git a/cdk/bin/api-publish.ts b/cdk/bin/api-publish.ts index f715a0e3c..d88b63a5f 100644 --- a/cdk/bin/api-publish.ts +++ b/cdk/bin/api-publish.ts @@ -8,7 +8,7 @@ import { resolveApiPublishParameters } from "../lib/utils/parameter-models"; const app = new cdk.App(); // Get parameters specific to API publishing -const params = resolveApiPublishParameters(app); +const params = resolveApiPublishParameters(); const sepHyphen = params.envPrefix ? "-" : ""; // Parse allowed origins diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index 4913289a7..1f49ac30b 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -15,7 +15,7 @@ import { resolveBedrockCustomBotParameters } from "../lib/utils/parameter-models const app = new cdk.App(); // Get parameters specific to Bedrock Custom Bot -const params = resolveBedrockCustomBotParameters(app); +const params = resolveBedrockCustomBotParameters(); const sepHyphen = params.envPrefix ? "-" : ""; // Log basic parameters for debugging diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index b3331e737..d2969f976 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -74,9 +74,27 @@ const BedrockChatParametersSchema = BaseParametersSchema.extend({ */ const ApiPublishParametersSchema = BaseParametersSchema.extend({ // API publishing configuration - publishedApiThrottleRateLimit: z.number().optional(), - publishedApiThrottleBurstLimit: z.number().optional(), - publishedApiQuotaLimit: z.number().optional(), + publishedApiThrottleRateLimit: z + .string() + .optional() + .refine((val) => !val || !isNaN(Number(val)), { + message: "Must be a valid number", + }) + .transform((val) => (val ? Number(val) : val)), + publishedApiThrottleBurstLimit: z + .string() + .optional() + .refine((val) => !val || !isNaN(Number(val)), { + message: "Must be a valid number", + }) + .transform((val) => (val ? Number(val) : val)), + publishedApiQuotaLimit: z + .string() + .optional() + .refine((val) => !val || !isNaN(Number(val)), { + message: "Must be a valid number", + }) + .transform((val) => (val ? Number(val) : val)), publishedApiQuotaPeriod: z.enum(["DAY", "WEEK", "MONTH"]).optional(), publishedApiDeploymentStage: z.string().default("api"), publishedApiId: z.string().optional(), @@ -94,7 +112,11 @@ const BedrockCustomBotParametersSchema = BaseParametersSchema.extend({ knowledge: z.string(), knowledgeBase: z.string(), guardrails: z.string(), - useStandByReplicas: z.boolean().default(false), + useStandByReplicas: z + .string() + .optional() + .transform((val) => val === "true") + .default("false"), }); /** @@ -228,95 +250,52 @@ export function getBedrockChatParameters( /** * Parse and validate parameters for API publishing. - * If you omit parametersInput, environment variables are used. - * @param app CDK App instance - * @param parametersInput Optional input parameters that override environment values - * @returns Validated parameters object + * This function is executed by CDK in CodeBuild launched via the API. + * Therefore, this is not intend to be set values using cdk.json or parameter.ts. + * @returns Validated parameters object from environment variables */ -export function resolveApiPublishParameters( - app: App, - parametersInput?: ApiPublishParametersInput -): ApiPublishParameters { - // If parametersInput is provided, use it directly - if (parametersInput) { - return ApiPublishParametersSchema.parse(parametersInput); - } - - // Get environment variables - const envName = getEnvVar("ENV_NAME", "default"); - const envPrefix = getEnvVar("ENV_PREFIX", ""); - - // Get parameters from context - const publishedApiThrottleRateLimit = app.node.tryGetContext( - "publishedApiThrottleRateLimit" - ); - const publishedApiThrottleBurstLimit = app.node.tryGetContext( - "publishedApiThrottleBurstLimit" - ); - const publishedApiQuotaLimit = app.node.tryGetContext( - "publishedApiQuotaLimit" - ); - const publishedApiAllowedOrigins = app.node.tryGetContext( - "publishedApiAllowedOrigins" - ); - - const contextParams = { - envName, - envPrefix, - bedrockRegion: app.node.tryGetContext("bedrockRegion"), - publishedApiThrottleRateLimit: publishedApiThrottleRateLimit - ? Number(publishedApiThrottleRateLimit) - : undefined, - publishedApiThrottleBurstLimit: publishedApiThrottleBurstLimit - ? Number(publishedApiThrottleBurstLimit) - : undefined, - publishedApiQuotaLimit: publishedApiQuotaLimit - ? Number(publishedApiQuotaLimit) - : undefined, - publishedApiQuotaPeriod: app.node.tryGetContext("publishedApiQuotaPeriod"), - publishedApiDeploymentStage: app.node.tryGetContext( - "publishedApiDeploymentStage" +export function resolveApiPublishParameters(): ApiPublishParameters { + // Get parameters from environment variables + const envVars = { + envName: getEnvVar("ENV_NAME", "default"), + envPrefix: getEnvVar("ENV_PREFIX", ""), + bedrockRegion: getEnvVar("BEDROCK_REGION"), + publishedApiThrottleRateLimit: getEnvVar( + "PUBLISHED_API_THROTTLE_RATE_LIMIT" + ), + publishedApiThrottleBurstLimit: getEnvVar( + "PUBLISHED_API_THROTTLE_BURST_LIMIT" ), - publishedApiId: app.node.tryGetContext("publishedApiId"), - publishedApiAllowedOrigins: publishedApiAllowedOrigins || '["*"]', + publishedApiQuotaLimit: getEnvVar("PUBLISHED_API_QUOTA_LIMIT"), + publishedApiQuotaPeriod: getEnvVar("PUBLISHED_API_QUOTA_PERIOD"), + publishedApiDeploymentStage: getEnvVar("PUBLISHED_API_DEPLOYMENT_STAGE"), + publishedApiId: getEnvVar("PUBLISHED_API_ID"), + publishedApiAllowedOrigins: getEnvVar("PUBLISHED_API_ALLOWED_ORIGINS"), }; - return ApiPublishParametersSchema.parse(contextParams); + return ApiPublishParametersSchema.parse(envVars); } /** * Parse and validate parameters for Bedrock Custom Bot. - * If you omit parametersInput, context parameters and environment variables are used. - * @param app CDK App instance - * @param parametersInput Optional input parameters that override context values - * @returns Validated parameters object + * This function is executed by CDK in CodeBuild launched via the API. + * Therefore, this is not intend to be set values using cdk.json or parameter.ts. + * @returns Validated parameters object from environment variables */ -export function resolveBedrockCustomBotParameters( - app: App, - parametersInput?: BedrockCustomBotParametersInput -): BedrockCustomBotParameters { - // If parametersInput is provided, use it directly - if (parametersInput) { - return BedrockCustomBotParametersSchema.parse(parametersInput); - } - - // Get environment variables - const envName = getEnvVar("ENV_NAME", "default"); - const envPrefix = getEnvVar("ENV_PREFIX", ""); - - // Get parameters from context and environment variables - const contextParams = { - envName, - envPrefix, - bedrockRegion: app.node.tryGetContext("bedrockRegion"), +export function resolveBedrockCustomBotParameters(): BedrockCustomBotParameters { + // Get parameters from environment variables + const envVars = { + envName: getEnvVar("ENV_NAME", "default"), + envPrefix: getEnvVar("ENV_PREFIX", ""), + bedrockRegion: getEnvVar("BEDROCK_REGION"), pk: getEnvVar("PK"), sk: getEnvVar("SK"), documentBucketName: getEnvVar("BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME"), knowledge: getEnvVar("KNOWLEDGE"), knowledgeBase: getEnvVar("BEDROCK_KNOWLEDGE_BASE"), guardrails: getEnvVar("BEDROCK_GUARDRAILS"), - useStandByReplicas: getEnvVar("USE_STAND_BY_REPLICAS") === "true", + useStandByReplicas: getEnvVar("USE_STAND_BY_REPLICAS"), }; - return BedrockCustomBotParametersSchema.parse(contextParams); + return BedrockCustomBotParametersSchema.parse(envVars); } diff --git a/cdk/test/utils/parameter-models.test.ts b/cdk/test/utils/parameter-models.test.ts index 32bb62785..dd1f15160 100644 --- a/cdk/test/utils/parameter-models.test.ts +++ b/cdk/test/utils/parameter-models.test.ts @@ -429,40 +429,20 @@ describe("getBedrockChatParameters", () => { describe("resolveApiPublishParameters", () => { describe("Parameter Source Selection", () => { - test("should use parametersInput when provided", () => { + test("should get parameters from environment variables", () => { // Given - const app = createTestApp(); - const inputParams = { - bedrockRegion: "eu-west-1", - publishedApiThrottleRateLimit: 100, - publishedApiAllowedOrigins: '["https://example.com"]', - }; - - // When - const result = resolveApiPublishParameters(app, inputParams); - - // Then - expect(result.bedrockRegion).toBe("eu-west-1"); - expect(result.publishedApiThrottleRateLimit).toBe(100); - expect(result.publishedApiAllowedOrigins).toBe('["https://example.com"]'); - }); - - test("should get parameters from environment variables when parametersInput is not provided", () => { - // Given - const app = createTestApp(); - // Mock environment variables const originalEnv = process.env; process.env = { ...originalEnv, - BEDROCK_REGION: "us-east-1", // Changed to match default value + BEDROCK_REGION: "us-east-1", PUBLISHED_API_THROTTLE_RATE_LIMIT: "200", PUBLISHED_API_ALLOWED_ORIGINS: '["https://test.com"]', }; try { // When - const result = resolveApiPublishParameters(app); + const result = resolveApiPublishParameters(); // Then expect(result.bedrockRegion).toBe("us-east-1"); @@ -478,10 +458,8 @@ describe("resolveApiPublishParameters", () => { describe("Parameter Validation", () => { test("should apply default values when required parameters are missing", () => { // Given - const app = createTestApp(); - // When - const result = resolveApiPublishParameters(app); + const result = resolveApiPublishParameters(); // Then expect(result.bedrockRegion).toBe("us-east-1"); // default value @@ -491,8 +469,6 @@ describe("resolveApiPublishParameters", () => { test("should convert string numeric parameters to numbers", () => { // Given - const app = createTestApp(); - // Mock environment variables const originalEnv = process.env; process.env = { @@ -504,7 +480,7 @@ describe("resolveApiPublishParameters", () => { try { // When - const result = resolveApiPublishParameters(app); + const result = resolveApiPublishParameters(); // Then expect(result.publishedApiThrottleRateLimit).toBe(100); @@ -518,107 +494,122 @@ describe("resolveApiPublishParameters", () => { test("should correctly parse all parameters when specified", () => { // Given - const app = createTestApp(); - const inputParams = { - bedrockRegion: "us-west-2", - publishedApiThrottleRateLimit: 100, - publishedApiThrottleBurstLimit: 200, - publishedApiQuotaLimit: 1000, - publishedApiQuotaPeriod: "DAY" as "DAY" | "WEEK" | "MONTH", - publishedApiDeploymentStage: "prod", - publishedApiId: "api123", - publishedApiAllowedOrigins: - '["https://example.com", "https://test.com"]', + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + BEDROCK_REGION: "us-west-2", + PUBLISHED_API_THROTTLE_RATE_LIMIT: "100", + PUBLISHED_API_THROTTLE_BURST_LIMIT: "200", + PUBLISHED_API_QUOTA_LIMIT: "1000", }; - // When - const result = resolveApiPublishParameters(app, inputParams); + try { + // When + const result = resolveApiPublishParameters(); - // Then - expect(result.bedrockRegion).toBe("us-west-2"); - expect(result.publishedApiThrottleRateLimit).toBe(100); - expect(result.publishedApiThrottleBurstLimit).toBe(200); - expect(result.publishedApiQuotaLimit).toBe(1000); - expect(result.publishedApiQuotaPeriod).toBe("DAY"); - expect(result.publishedApiDeploymentStage).toBe("prod"); - expect(result.publishedApiId).toBe("api123"); - expect(result.publishedApiAllowedOrigins).toBe( - '["https://example.com", "https://test.com"]' - ); + // Then + expect(result.publishedApiThrottleRateLimit).toBe(100); + expect(result.publishedApiThrottleBurstLimit).toBe(200); + expect(result.publishedApiQuotaLimit).toBe(1000); + } finally { + // Restore original environment + process.env = originalEnv; + } + }); + + test("should correctly parse all parameters when specified", () => { + // Given + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + BEDROCK_REGION: "us-west-2", + PUBLISHED_API_THROTTLE_RATE_LIMIT: "100", + PUBLISHED_API_THROTTLE_BURST_LIMIT: "200", + PUBLISHED_API_QUOTA_LIMIT: "1000", + PUBLISHED_API_QUOTA_PERIOD: "DAY", + PUBLISHED_API_DEPLOYMENT_STAGE: "prod", + PUBLISHED_API_ID: "api123", + PUBLISHED_API_ALLOWED_ORIGINS: '["https://example.com", "https://test.com"]', + }; + + try { + // When + const result = resolveApiPublishParameters(); + + // Then + expect(result.bedrockRegion).toBe("us-west-2"); + expect(result.publishedApiThrottleRateLimit).toBe(100); + expect(result.publishedApiThrottleBurstLimit).toBe(200); + expect(result.publishedApiQuotaLimit).toBe(1000); + expect(result.publishedApiQuotaPeriod).toBe("DAY"); + expect(result.publishedApiDeploymentStage).toBe("prod"); + expect(result.publishedApiId).toBe("api123"); + expect(result.publishedApiAllowedOrigins).toBe( + '["https://example.com", "https://test.com"]' + ); + } finally { + // Restore original environment + process.env = originalEnv; + } }); test("should throw ZodError when invalid QuotaPeriod is specified", () => { // Given - const app = createTestApp(); - const invalidParams = { - publishedApiQuotaPeriod: "YEAR" as any, // invalid value + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + PUBLISHED_API_QUOTA_PERIOD: "YEAR", // invalid value }; - // When/Then - expect(() => { - resolveApiPublishParameters(app, invalidParams as any); - }).toThrow(ZodError); + try { + // When/Then + expect(() => { + resolveApiPublishParameters(); + }).toThrow(ZodError); + } finally { + // Restore original environment + process.env = originalEnv; + } }); test("should handle invalid publishedApiAllowedOrigins format", () => { // Given - const app = createTestApp(); - const invalidParams = { - publishedApiAllowedOrigins: 'invalid json format', + // Mock environment variables + const originalEnv = process.env; + process.env = { + ...originalEnv, + PUBLISHED_API_ALLOWED_ORIGINS: 'invalid json format', }; - // When - const result = resolveApiPublishParameters(app, invalidParams); - - // Then - // Note: The function doesn't validate JSON format, it just passes the string through - expect(result.publishedApiAllowedOrigins).toBe('invalid json format'); + try { + // When + const result = resolveApiPublishParameters(); + + // Then + // Note: The function doesn't validate JSON format, it just passes the string through + expect(result.publishedApiAllowedOrigins).toBe('invalid json format'); + } finally { + // Restore original environment + process.env = originalEnv; + } }); }); }); describe("resolveBedrockCustomBotParameters", () => { describe("Parameter Source Selection", () => { - test("should use parametersInput when provided", () => { + test("should get parameters from environment variables", () => { // Given - const app = createTestApp(); - const inputParams = { - bedrockRegion: "eu-west-1", - pk: "test-pk", - sk: "test-sk", - documentBucketName: "test-bucket", - knowledge: '{"test": "knowledge"}', - knowledgeBase: '{"test": "kb"}', - guardrails: '{"test": "guardrails"}', - useStandByReplicas: true - }; - - // When - const result = resolveBedrockCustomBotParameters(app, inputParams); - - // Then - expect(result.bedrockRegion).toBe("eu-west-1"); - expect(result.pk).toBe("test-pk"); - expect(result.sk).toBe("test-sk"); - expect(result.documentBucketName).toBe("test-bucket"); - expect(result.knowledge).toBe('{"test": "knowledge"}'); - expect(result.knowledgeBase).toBe('{"test": "kb"}'); - expect(result.guardrails).toBe('{"test": "guardrails"}'); - expect(result.useStandByReplicas).toBe(true); - }); - - test("should get parameters from context and environment variables when parametersInput is not provided", () => { - // Given - const app = createTestApp({ - bedrockRegion: "ap-northeast-1", - }); - // Mock environment variables const originalEnv = process.env; process.env = { ...originalEnv, ENV_NAME: "test-env", ENV_PREFIX: "test-prefix", + BEDROCK_REGION: "us-east-1", PK: "env-pk", SK: "env-sk", BEDROCK_CLAUDE_CHAT_DOCUMENT_BUCKET_NAME: "env-bucket", @@ -626,15 +617,14 @@ describe("resolveBedrockCustomBotParameters", () => { BEDROCK_KNOWLEDGE_BASE: '{"env": "kb"}', BEDROCK_GUARDRAILS: '{"env": "guardrails"}', USE_STAND_BY_REPLICAS: "true", - BEDROCK_REGION: "us-east-1" // This will be overridden by context }; try { // When - const result = resolveBedrockCustomBotParameters(app); + const result = resolveBedrockCustomBotParameters(); // Then - expect(result.bedrockRegion).toBe("ap-northeast-1"); // From context + expect(result.bedrockRegion).toBe("us-east-1"); expect(result.envName).toBe("test-env"); expect(result.envPrefix).toBe("test-prefix"); expect(result.pk).toBe("env-pk"); @@ -654,31 +644,28 @@ describe("resolveBedrockCustomBotParameters", () => { describe("Parameter Validation", () => { test("should throw ZodError when required parameters are missing", () => { // Given - const app = createTestApp(); - // When/Then expect(() => { - resolveBedrockCustomBotParameters(app); + resolveBedrockCustomBotParameters(); }).toThrow(); }); test("should throw ZodError when invalid parameter is specified", () => { // Given - const app = createTestApp(); - const invalidParams = { - bedrockRegion: 123, // number instead of string - pk: "test-pk", - sk: "test-sk", - documentBucketName: "test-bucket", - knowledge: '{"test": "knowledge"}', - knowledgeBase: '{"test": "kb"}', - guardrails: '{"test": "guardrails"}' - }; + // Mock environment variables with invalid type + const originalEnv = process.env; + // Clear all environment variables to ensure required ones are missing + process.env = {}; - // When/Then - expect(() => { - resolveBedrockCustomBotParameters(app, invalidParams as any); - }).toThrow(ZodError); + try { + // When/Then + expect(() => { + resolveBedrockCustomBotParameters(); + }).toThrow(); + } finally { + // Restore original environment + process.env = originalEnv; + } }); }); }); From 461c1247b699270358def3a77a0cac48f5dfa147 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Thu, 13 Mar 2025 23:15:29 +0900 Subject: [PATCH 12/22] simplify type casting --- cdk/lib/utils/parameter-models.ts | 35 ++++++++++++------------------- cdk/test/cdk.test.ts | 10 +++++++++ 2 files changed, 23 insertions(+), 22 deletions(-) diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index d2969f976..43cb0bd25 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -54,7 +54,11 @@ const BedrockChatParametersSchema = BaseParametersSchema.extend({ ]), // Authentication and user management - identityProviders: z.array(z.custom()).default([]), + identityProviders: z + .unknown() + .transform(val => Array.isArray(val) ? val as TIdentityProvider[] : []) + .pipe(z.array(z.custom())) + .default([]), userPoolDomainPrefix: z.string().default(""), allowedSignUpEmailDomains: z.array(z.string()).default([]), autoJoinUserGroups: z.array(z.string()).default(["CreatingBotAllowed"]), @@ -77,24 +81,15 @@ const ApiPublishParametersSchema = BaseParametersSchema.extend({ publishedApiThrottleRateLimit: z .string() .optional() - .refine((val) => !val || !isNaN(Number(val)), { - message: "Must be a valid number", - }) - .transform((val) => (val ? Number(val) : val)), + .transform((val) => (val ? Number(val) : undefined)), publishedApiThrottleBurstLimit: z .string() .optional() - .refine((val) => !val || !isNaN(Number(val)), { - message: "Must be a valid number", - }) - .transform((val) => (val ? Number(val) : val)), + .transform((val) => (val ? Number(val) : undefined)), publishedApiQuotaLimit: z .string() .optional() - .refine((val) => !val || !isNaN(Number(val)), { - message: "Must be a valid number", - }) - .transform((val) => (val ? Number(val) : val)), + .transform((val) => (val ? Number(val) : undefined)), publishedApiQuotaPeriod: z.enum(["DAY", "WEEK", "MONTH"]).optional(), publishedApiDeploymentStage: z.string().default("api"), publishedApiId: z.string().optional(), @@ -177,9 +172,7 @@ export function resolveBedrockChatParameters( "allowedIpV6AddressRanges" ), // 配列でない場合は空配列を使用 - identityProviders: Array.isArray(identityProviders) - ? identityProviders - : [], + identityProviders: app.node.tryGetContext("identityProviders"), userPoolDomainPrefix: app.node.tryGetContext("userPoolDomainPrefix"), allowedSignUpEmailDomains: app.node.tryGetContext( "allowedSignUpEmailDomains" @@ -255,10 +248,9 @@ export function getBedrockChatParameters( * @returns Validated parameters object from environment variables */ export function resolveApiPublishParameters(): ApiPublishParameters { - // Get parameters from environment variables const envVars = { - envName: getEnvVar("ENV_NAME", "default"), - envPrefix: getEnvVar("ENV_PREFIX", ""), + envName: getEnvVar("ENV_NAME"), + envPrefix: getEnvVar("ENV_PREFIX"), bedrockRegion: getEnvVar("BEDROCK_REGION"), publishedApiThrottleRateLimit: getEnvVar( "PUBLISHED_API_THROTTLE_RATE_LIMIT" @@ -283,10 +275,9 @@ export function resolveApiPublishParameters(): ApiPublishParameters { * @returns Validated parameters object from environment variables */ export function resolveBedrockCustomBotParameters(): BedrockCustomBotParameters { - // Get parameters from environment variables const envVars = { - envName: getEnvVar("ENV_NAME", "default"), - envPrefix: getEnvVar("ENV_PREFIX", ""), + envName: getEnvVar("ENV_NAME"), + envPrefix: getEnvVar("ENV_PREFIX"), bedrockRegion: getEnvVar("BEDROCK_REGION"), pk: getEnvVar("PK"), sk: getEnvVar("SK"), diff --git a/cdk/test/cdk.test.ts b/cdk/test/cdk.test.ts index b79aafc6a..4f98413a9 100644 --- a/cdk/test/cdk.test.ts +++ b/cdk/test/cdk.test.ts @@ -36,6 +36,8 @@ describe("Bedrock Chat Stack Test", () => { env: { region: "us-west-2", }, + envName: "test", + envPrefix: "test-", bedrockRegion: "us-east-1", crossRegionReferences: true, webAclId: "", @@ -106,6 +108,8 @@ describe("Bedrock Chat Stack Test", () => { env: { region: "us-west-2", }, + envName: "test", + envPrefix: "test-", bedrockRegion: "us-east-1", crossRegionReferences: true, webAclId: "", @@ -173,6 +177,8 @@ describe("Bedrock Chat Stack Test", () => { env: { region: "us-west-2", }, + envName: "test", + envPrefix: "test-", bedrockRegion: "us-east-1", crossRegionReferences: true, webAclId: "", @@ -219,6 +225,8 @@ describe("Bedrock Chat Stack Test", () => { env: { region: "us-east-1", }, + envName: "test", + envPrefix: "test-", bedrockRegion: "us-east-1", crossRegionReferences: true, webAclId: "", @@ -299,6 +307,8 @@ describe("Bedrock Chat Stack Test", () => { env: { region: "us-east-1", }, + envName: "test", + envPrefix: "test-", bedrockRegion: "us-east-1", crossRegionReferences: true, webAclId: "", From 0a7d8ee947b2f9ba4fd25a58f69709b461d1676a Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Fri, 14 Mar 2025 09:11:03 +0900 Subject: [PATCH 13/22] remove prefix from custom bot and api pulish. add tag to secret --- backend/app/utils.py | 5 ++- cdk/bin/api-publish.ts | 74 +++++++++++++++++------------------ cdk/bin/bedrock-custom-bot.ts | 71 ++++++++++++++++----------------- cdk/lib/bedrock-chat-stack.ts | 2 + cdk/lib/constructs/api.ts | 4 ++ 5 files changed, 78 insertions(+), 78 deletions(-) diff --git a/backend/app/utils.py b/backend/app/utils.py index 78531d453..e9e500ec9 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -194,6 +194,7 @@ def store_api_key_to_secret_manager( """ secret_name = f"{prefix}/{user_id}/{bot_id}" secret_value = json.dumps({"api_key": api_key}) + env_name = os.environ.get("ENV_NAME", "default") try: secrets_client = boto3.client("secretsmanager") @@ -216,7 +217,9 @@ def store_api_key_to_secret_manager( # Create new secret if it doesn't exist logger.info(f"Creating new secret: {secret_name}") response = secrets_client.create_secret( - Name=secret_name, SecretString=secret_value + Name=secret_name, + SecretString=secret_value, + Tags=[{"Key": "CDKEnvironment", "Value": env_name}], ) logger.info(f"Created new secret: {secret_name}") return response["ARN"] diff --git a/cdk/bin/api-publish.ts b/cdk/bin/api-publish.ts index d88b63a5f..d86051e4d 100644 --- a/cdk/bin/api-publish.ts +++ b/cdk/bin/api-publish.ts @@ -34,44 +34,40 @@ const largeMessageBucketName = cdk.Fn.importValue( ); // NOTE: DO NOT change the stack id naming rule. -const publishedApi = new ApiPublishmentStack( - app, - `${params.envPrefix}${sepHyphen}ApiPublishmentStack${params.publishedApiId}`, - { - env: { - region: process.env.CDK_DEFAULT_REGION, - }, - bedrockRegion: params.bedrockRegion, - conversationTableName: conversationTableName, - tableAccessRoleArn: tableAccessRoleArn, - webAclArn: webAclArn, - largeMessageBucketName: largeMessageBucketName, - usagePlan: { - throttle: - params.publishedApiThrottleRateLimit !== undefined && - params.publishedApiThrottleBurstLimit !== undefined - ? { - rateLimit: params.publishedApiThrottleRateLimit, - burstLimit: params.publishedApiThrottleBurstLimit, - } - : undefined, - quota: - params.publishedApiQuotaLimit !== undefined && - params.publishedApiQuotaPeriod !== undefined - ? { - limit: params.publishedApiQuotaLimit, - period: apigateway.Period[params.publishedApiQuotaPeriod], - } - : undefined, - }, - deploymentStage: params.publishedApiDeploymentStage, - corsOptions: { - allowOrigins: publishedApiAllowedOrigins, - allowMethods: apigateway.Cors.ALL_METHODS, - allowHeaders: apigateway.Cors.DEFAULT_HEADERS, - allowCredentials: true, - }, - } -); +new ApiPublishmentStack(app, `ApiPublishmentStack${params.publishedApiId}`, { + env: { + region: process.env.CDK_DEFAULT_REGION, + }, + bedrockRegion: params.bedrockRegion, + conversationTableName: conversationTableName, + tableAccessRoleArn: tableAccessRoleArn, + webAclArn: webAclArn, + largeMessageBucketName: largeMessageBucketName, + usagePlan: { + throttle: + params.publishedApiThrottleRateLimit !== undefined && + params.publishedApiThrottleBurstLimit !== undefined + ? { + rateLimit: params.publishedApiThrottleRateLimit, + burstLimit: params.publishedApiThrottleBurstLimit, + } + : undefined, + quota: + params.publishedApiQuotaLimit !== undefined && + params.publishedApiQuotaPeriod !== undefined + ? { + limit: params.publishedApiQuotaLimit, + period: apigateway.Period[params.publishedApiQuotaPeriod], + } + : undefined, + }, + deploymentStage: params.publishedApiDeploymentStage, + corsOptions: { + allowOrigins: publishedApiAllowedOrigins, + allowMethods: apigateway.Cors.ALL_METHODS, + allowHeaders: apigateway.Cors.DEFAULT_HEADERS, + allowCredentials: true, + }, +}); cdk.Tags.of(app).add("CDKEnvironment", params.envName); diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index 1f49ac30b..6755a0b3d 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -16,7 +16,6 @@ const app = new cdk.App(); // Get parameters specific to Bedrock Custom Bot const params = resolveBedrockCustomBotParameters(); -const sepHyphen = params.envPrefix ? "-" : ""; // Log basic parameters for debugging console.log( @@ -158,42 +157,38 @@ console.log( ); // Create the stack -const bedrockCustomBotStack = new BedrockCustomBotStack( - app, - `${params.envPrefix}${sepHyphen}BrChatKbStack${botId}`, - { - env: { - region: params.bedrockRegion, - }, - ownerUserId, - botId, - embeddingsModel, - parsingModel, - crawlingScope, - crawlingFilters, - existKnowledgeBaseId, - bedrockClaudeChatDocumentBucketName: params.documentBucketName, - chunkingStrategy, - existingS3Urls, - sourceUrls, - maxTokens, - instruction, - analyzer, - overlapPercentage, - guardrail: { - is_guardrail_enabled, - hateThreshold, - insultsThreshold, - sexualThreshold, - violenceThreshold, - misconductThreshold, - groundingThreshold, - relevanceThreshold, - guardrailArn, - guardrailVersion, - }, - useStandbyReplicas, - } -); +new BedrockCustomBotStack(app, `BrChatKbStack${botId}`, { + env: { + region: params.bedrockRegion, + }, + ownerUserId, + botId, + embeddingsModel, + parsingModel, + crawlingScope, + crawlingFilters, + existKnowledgeBaseId, + bedrockClaudeChatDocumentBucketName: params.documentBucketName, + chunkingStrategy, + existingS3Urls, + sourceUrls, + maxTokens, + instruction, + analyzer, + overlapPercentage, + guardrail: { + is_guardrail_enabled, + hateThreshold, + insultsThreshold, + sexualThreshold, + violenceThreshold, + misconductThreshold, + groundingThreshold, + relevanceThreshold, + guardrailArn, + guardrailVersion, + }, + useStandbyReplicas, +}); cdk.Tags.of(app).add("CDKEnvironment", params.envName); diff --git a/cdk/lib/bedrock-chat-stack.ts b/cdk/lib/bedrock-chat-stack.ts index 962bcf859..a802bf853 100644 --- a/cdk/lib/bedrock-chat-stack.ts +++ b/cdk/lib/bedrock-chat-stack.ts @@ -170,6 +170,8 @@ export class BedrockChatStack extends cdk.Stack { }); const backendApi = new Api(this, "BackendApi", { + envName: props.envName, + envPrefix: props.envPrefix, database: database.table, auth, bedrockRegion: props.bedrockRegion, diff --git a/cdk/lib/constructs/api.ts b/cdk/lib/constructs/api.ts index f29244713..de3c77a29 100644 --- a/cdk/lib/constructs/api.ts +++ b/cdk/lib/constructs/api.ts @@ -27,6 +27,8 @@ import { excludeDockerImage } from "../constants/docker"; import { PythonFunction } from "@aws-cdk/aws-lambda-python-alpha"; export interface ApiProps { + readonly envName: string; + readonly envPrefix: string; readonly database: ITable; readonly corsAllowOrigins?: string[]; readonly auth: Auth; @@ -209,6 +211,8 @@ export class Api extends Construct { memorySize: 1024, timeout: Duration.minutes(15), environment: { + ENV_NAME: props.envName, + ENV_PREFIX: props.envPrefix, TABLE_NAME: database.tableName, CORS_ALLOW_ORIGINS: allowOrigins.join(","), USER_POOL_ID: props.auth.userPool.userPoolId, From b4d73cac5668d57f5a1ac53ea874c9061bcd063d Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Fri, 14 Mar 2025 20:12:36 +0900 Subject: [PATCH 14/22] add permission --- cdk/lib/constructs/api.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/cdk/lib/constructs/api.ts b/cdk/lib/constructs/api.ts index de3c77a29..ba2c9721b 100644 --- a/cdk/lib/constructs/api.ts +++ b/cdk/lib/constructs/api.ts @@ -187,6 +187,7 @@ export class Api extends Construct { "secretsmanager:RotateSecret", "secretsmanager:CancelRotateSecret", "secretsmanager:UpdateSecret", + "secretsmanager:TagResource", ], resources: [ `arn:aws:secretsmanager:${Stack.of(this).region}:${ From be4ff59f47e1eeb485e04db75f5f6444fcddfe07 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Fri, 14 Mar 2025 22:21:04 +0900 Subject: [PATCH 15/22] organize bedrock-custom-bots.ts parameters --- cdk/bin/bedrock-custom-bot.ts | 359 ++++++++++++++++------------ cdk/lib/bedrock-custom-bot-stack.ts | 141 ++++++----- 2 files changed, 279 insertions(+), 221 deletions(-) diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index 6755a0b3d..348f812d0 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -17,178 +17,223 @@ const app = new cdk.App(); // Get parameters specific to Bedrock Custom Bot const params = resolveBedrockCustomBotParameters(); -// Log basic parameters for debugging -console.log( - "Bedrock Custom Bot Parameters:", - JSON.stringify({ - envName: params.envName, - envPrefix: params.envPrefix, - pk: params.pk, - sk: params.sk, - documentBucketName: params.documentBucketName, - useStandByReplicas: params.useStandByReplicas, - bedrockRegion: params.bedrockRegion, - }) -); - // Parse JSON strings into objects -const knowledgeBase = JSON.parse(params.knowledgeBase); -const knowledge = JSON.parse(params.knowledge); -const guardrails = JSON.parse(params.guardrails); +const knowledgeBaseJson = JSON.parse(params.knowledgeBase); +const knowledgeJson = JSON.parse(params.knowledge); +const guardrailsJson = JSON.parse(params.guardrails); + +// Define interfaces for typed configuration objects +interface BaseConfig { + envName: string; + envPrefix: string; + bedrockRegion: string; + ownerUserId: string; + botId: string; + documentBucketName: string; + useStandbyReplicas: boolean; +} + +interface KnowledgeConfig { + embeddingsModel: ReturnType; + parsingModel: ReturnType | undefined; + existKnowledgeBaseId?: string; + existingS3Urls: string[]; + sourceUrls: string[]; + instruction?: string; + analyzer?: ReturnType; +} + +interface ChunkingConfig { + chunkingStrategy: ReturnType; + maxTokens?: number; + overlapPercentage?: number; + overlapTokens?: number; // not used + maxParentTokenSize?: number; // not used + maxChildTokenSize?: number; // not used + bufferSize?: number; // not used + breakpointPercentileThreshold?: number; // not used +} + +interface GuardrailConfig { + is_guardrail_enabled?: boolean; + hateThreshold?: number; + insultsThreshold?: number; + sexualThreshold?: number; + violenceThreshold?: number; + misconductThreshold?: number; + groundingThreshold?: number; + relevanceThreshold?: number; + guardrailArn?: number; + guardrailVersion?: number; +} + +interface CrawlingConfig { + crawlingScope?: ReturnType; + crawlingFilters: CrawlingFilters; +} + +// Extract and organize configuration by category +const baseConfig: BaseConfig = { + envName: params.envName, + envPrefix: params.envPrefix, + bedrockRegion: params.bedrockRegion, + ownerUserId: params.pk, + botId: params.sk.split("#")[2], + documentBucketName: params.documentBucketName, + useStandbyReplicas: params.useStandByReplicas === true, +}; + +const knowledgeConfig: KnowledgeConfig = { + embeddingsModel: getEmbeddingModel(knowledgeBaseJson.embeddings_model.S), + parsingModel: getParsingModel(knowledgeBaseJson.parsing_model.S), + existKnowledgeBaseId: knowledgeBaseJson.exist_knowledge_base_id?.S, + existingS3Urls: knowledgeJson.s3_urls.L.map((s3Url: any) => s3Url.S), + sourceUrls: knowledgeJson.source_urls.L.map((sourceUrl: any) => sourceUrl.S), + instruction: knowledgeBaseJson.instruction?.S, + analyzer: knowledgeBaseJson.open_search.M.analyzer.M + ? getAnalyzer(knowledgeBaseJson.open_search.M.analyzer.M) + : undefined, +}; + +// Extract chunking configuration +const chunkingParams = { + maxTokens: knowledgeBaseJson.chunking_configuration.M.max_tokens + ? Number(knowledgeBaseJson.chunking_configuration.M.max_tokens.N) + : undefined, + overlapPercentage: knowledgeBaseJson.chunking_configuration.M + .overlap_percentage + ? Number(knowledgeBaseJson.chunking_configuration.M.overlap_percentage.N) + : undefined, + overlapTokens: knowledgeBaseJson.chunking_configuration.M.overlap_tokens + ? Number(knowledgeBaseJson.chunking_configuration.M.overlap_tokens.N) + : undefined, + maxParentTokenSize: knowledgeBaseJson.chunking_configuration.M + .max_parent_token_size + ? Number(knowledgeBaseJson.chunking_configuration.M.max_parent_token_size.N) + : undefined, + maxChildTokenSize: knowledgeBaseJson.chunking_configuration.M + .max_child_token_size + ? Number(knowledgeBaseJson.chunking_configuration.M.max_child_token_size.N) + : undefined, + bufferSize: knowledgeBaseJson.chunking_configuration.M.buffer_size + ? Number(knowledgeBaseJson.chunking_configuration.M.buffer_size.N) + : undefined, + breakpointPercentileThreshold: knowledgeBaseJson.chunking_configuration.M + .breakpoint_percentile_threshold + ? Number( + knowledgeBaseJson.chunking_configuration.M + .breakpoint_percentile_threshold.N + ) + : undefined, +}; + +const chunkingConfig: ChunkingConfig = { + ...chunkingParams, + chunkingStrategy: getChunkingStrategy( + knowledgeBaseJson.chunking_configuration.M.chunking_strategy.S, + knowledgeBaseJson.embeddings_model.S, + chunkingParams + ), +}; -// Extract data from parsed objects -const ownerUserId = params.pk; -const botId = params.sk.split("#")[2]; -const existingS3Urls = knowledge.s3_urls.L.map((s3Url: any) => s3Url.S); -const sourceUrls = knowledge.source_urls.L.map((sourceUrl: any) => sourceUrl.S); -const useStandbyReplicas = params.useStandByReplicas === true; +const crawlingConfig: CrawlingConfig = { + crawlingScope: getCrowlingScope(knowledgeBaseJson.web_crawling_scope.S), + crawlingFilters: getCrawlingFilters(knowledgeBaseJson.web_crawling_filters.M), +}; +const guardrailConfig: GuardrailConfig = { + is_guardrail_enabled: guardrailsJson.is_guardrail_enabled + ? Boolean(guardrailsJson.is_guardrail_enabled.BOOL) + : undefined, + hateThreshold: guardrailsJson.hate_threshold + ? Number(guardrailsJson.hate_threshold.N) + : undefined, + insultsThreshold: guardrailsJson.insults_threshold + ? Number(guardrailsJson.insults_threshold.N) + : undefined, + sexualThreshold: guardrailsJson.sexual_threshold + ? Number(guardrailsJson.sexual_threshold.N) + : undefined, + violenceThreshold: guardrailsJson.violence_threshold + ? Number(guardrailsJson.violence_threshold.N) + : undefined, + misconductThreshold: guardrailsJson.misconduct_threshold + ? Number(guardrailsJson.misconduct_threshold.N) + : undefined, + groundingThreshold: guardrailsJson.grounding_threshold + ? Number(guardrailsJson.grounding_threshold.N) + : undefined, + relevanceThreshold: guardrailsJson.relevance_threshold + ? Number(guardrailsJson.relevance_threshold.N) + : undefined, + guardrailArn: guardrailsJson.guardrail_arn + ? Number(guardrailsJson.guardrail_arn.N) + : undefined, + guardrailVersion: guardrailsJson.guardrail_version + ? Number(guardrailsJson.guardrail_version.N) + : undefined, +}; + +// Log organized configurations for debugging +console.log("Base Configuration:", JSON.stringify(baseConfig)); console.log( - "Parsed Configuration:", + "Knowledge Configuration:", JSON.stringify({ - ownerUserId, - botId, - existingS3Urls, - sourceUrls, - useStandbyReplicas, + ...knowledgeConfig, + embeddingsModel: knowledgeConfig.embeddingsModel.toString(), + parsingModel: knowledgeConfig.parsingModel?.toString(), + analyzer: knowledgeConfig.analyzer ? "configured" : "undefined", }) ); - -// Process knowledge base configuration -const embeddingsModel = getEmbeddingModel(knowledgeBase.embeddings_model.S); -const parsingModel = getParsingModel(knowledgeBase.parsing_model.S); -const crawlingScope = getCrowlingScope(knowledgeBase.web_crawling_scope.S); -const crawlingFilters: CrawlingFilters = getCrawlingFilters( - knowledgeBase.web_crawling_filters.M -); -const existKnowledgeBaseId = knowledgeBase.exist_knowledge_base_id?.S; -const maxTokens = knowledgeBase.chunking_configuration.M.max_tokens - ? Number(knowledgeBase.chunking_configuration.M.max_tokens.N) - : undefined; -const instruction = knowledgeBase.instruction?.S; -const analyzer = knowledgeBase.open_search.M.analyzer.M - ? getAnalyzer(knowledgeBase.open_search.M.analyzer.M) - : undefined; -const overlapPercentage = knowledgeBase.chunking_configuration.M - .overlap_percentage - ? Number(knowledgeBase.chunking_configuration.M.overlap_percentage.N) - : undefined; -const overlapTokens = knowledgeBase.chunking_configuration.M.overlap_tokens - ? Number(knowledgeBase.chunking_configuration.M.overlap_tokens.N) - : undefined; -const maxParentTokenSize = knowledgeBase.chunking_configuration.M - .max_parent_token_size - ? Number(knowledgeBase.chunking_configuration.M.max_parent_token_size.N) - : undefined; -const maxChildTokenSize = knowledgeBase.chunking_configuration.M - .max_child_token_size - ? Number(knowledgeBase.chunking_configuration.M.max_child_token_size.N) - : undefined; -const bufferSize = knowledgeBase.chunking_configuration.M.buffer_size - ? Number(knowledgeBase.chunking_configuration.M.buffer_size.N) - : undefined; -const breakpointPercentileThreshold = knowledgeBase.chunking_configuration.M - .breakpoint_percentile_threshold - ? Number( - knowledgeBase.chunking_configuration.M.breakpoint_percentile_threshold.N - ) - : undefined; - -// Process guardrails configuration -const is_guardrail_enabled = guardrails.is_guardrail_enabled - ? Boolean(guardrails.is_guardrail_enabled.BOOL) - : undefined; -const hateThreshold = guardrails.hate_threshold - ? Number(guardrails.hate_threshold.N) - : undefined; -const insultsThreshold = guardrails.insults_threshold - ? Number(guardrails.insults_threshold.N) - : undefined; -const sexualThreshold = guardrails.sexual_threshold - ? Number(guardrails.sexual_threshold.N) - : undefined; -const violenceThreshold = guardrails.violence_threshold - ? Number(guardrails.violence_threshold.N) - : undefined; -const misconductThreshold = guardrails.misconduct_threshold - ? Number(guardrails.misconduct_threshold.N) - : undefined; -const groundingThreshold = guardrails.grounding_threshold - ? Number(guardrails.grounding_threshold.N) - : undefined; -const relevanceThreshold = guardrails.relevance_threshold - ? Number(guardrails.relevance_threshold.N) - : undefined; -const guardrailArn = guardrails.guardrail_arn - ? Number(guardrails.guardrail_arn.N) - : undefined; -const guardrailVersion = guardrails.guardrail_version - ? Number(guardrails.guardrail_version.N) - : undefined; - -// Get chunking strategy -const chunkingStrategy = getChunkingStrategy( - knowledgeBase.chunking_configuration.M.chunking_strategy.S, - knowledgeBase.embeddings_model.S, - { - maxTokens, - overlapPercentage, - overlapTokens, - maxParentTokenSize, - maxChildTokenSize, - bufferSize, - breakpointPercentileThreshold, - } +console.log( + "Chunking Configuration:", + JSON.stringify({ + ...chunkingConfig, + chunkingStrategy: chunkingConfig.chunkingStrategy.toString(), + }) ); - +console.log("Guardrail Configuration:", JSON.stringify(guardrailConfig)); console.log( - "Knowledge Base Configuration:", + "Crawling Configuration:", JSON.stringify({ - embeddingsModel: embeddingsModel.toString(), - chunkingStrategy: chunkingStrategy.toString(), - existKnowledgeBaseId, - maxTokens, - instruction, - is_guardrail_enabled, - parsingModel: parsingModel?.toString(), - crawlingScope: crawlingScope?.toString(), - analyzer: analyzer ? "configured" : "undefined", + crawlingScope: crawlingConfig.crawlingScope?.toString(), + crawlingFilters: crawlingConfig.crawlingFilters, }) ); // Create the stack -new BedrockCustomBotStack(app, `BrChatKbStack${botId}`, { +new BedrockCustomBotStack(app, `BrChatKbStack${baseConfig.botId}`, { + // Environment configuration env: { - region: params.bedrockRegion, + region: baseConfig.bedrockRegion, }, - ownerUserId, - botId, - embeddingsModel, - parsingModel, - crawlingScope, - crawlingFilters, - existKnowledgeBaseId, - bedrockClaudeChatDocumentBucketName: params.documentBucketName, - chunkingStrategy, - existingS3Urls, - sourceUrls, - maxTokens, - instruction, - analyzer, - overlapPercentage, - guardrail: { - is_guardrail_enabled, - hateThreshold, - insultsThreshold, - sexualThreshold, - violenceThreshold, - misconductThreshold, - groundingThreshold, - relevanceThreshold, - guardrailArn, - guardrailVersion, - }, - useStandbyReplicas, + + // Base configuration + ownerUserId: baseConfig.ownerUserId, + botId: baseConfig.botId, + bedrockClaudeChatDocumentBucketName: baseConfig.documentBucketName, + useStandbyReplicas: baseConfig.useStandbyReplicas, + + // Knowledge base configuration + embeddingsModel: knowledgeConfig.embeddingsModel, + parsingModel: knowledgeConfig.parsingModel, + existKnowledgeBaseId: knowledgeConfig.existKnowledgeBaseId, + existingS3Urls: knowledgeConfig.existingS3Urls, + sourceUrls: knowledgeConfig.sourceUrls, + instruction: knowledgeConfig.instruction, + analyzer: knowledgeConfig.analyzer, + + // Chunking configuration + chunkingStrategy: chunkingConfig.chunkingStrategy, + maxTokens: chunkingConfig.maxTokens, + overlapPercentage: chunkingConfig.overlapPercentage, + + // Crawling configuration + crawlingScope: crawlingConfig.crawlingScope, + crawlingFilters: crawlingConfig.crawlingFilters, + + // Guardrail configuration + guardrail: guardrailConfig, }); -cdk.Tags.of(app).add("CDKEnvironment", params.envName); +cdk.Tags.of(app).add("CDKEnvironment", baseConfig.envName); diff --git a/cdk/lib/bedrock-custom-bot-stack.ts b/cdk/lib/bedrock-custom-bot-stack.ts index 78421c339..aa22ee985 100644 --- a/cdk/lib/bedrock-custom-bot-stack.ts +++ b/cdk/lib/bedrock-custom-bot-stack.ts @@ -1,4 +1,4 @@ -import { CfnOutput, RemovalPolicy, Stack, StackProps} from "aws-cdk-lib"; +import { CfnOutput, RemovalPolicy, Stack, StackProps } from "aws-cdk-lib"; import { Construct } from "constructs"; import { VectorCollection } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearchserverless"; import { @@ -8,27 +8,26 @@ import { import { VectorCollectionStandbyReplicas } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearchserverless"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as iam from "aws-cdk-lib/aws-iam"; -import { - BedrockFoundationModel, -} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; -import { - ChunkingStrategy, -} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/chunking"; -import { - S3DataSource, -} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/s3-data-source"; +import { BedrockFoundationModel } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; +import { ChunkingStrategy } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/chunking"; +import { S3DataSource } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/s3-data-source"; import { WebCrawlerDataSource, CrawlingScope, CrawlingFilters, } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/web-crawler-data-source"; -import { - ParsingStategy -} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/parsing"; +import { ParsingStategy } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/parsing"; -import { KnowledgeBase, IKnowledgeBase } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; +import { + KnowledgeBase, + IKnowledgeBase, +} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; import { aws_bedrock as bedrock } from "aws-cdk-lib"; -import { AwsCustomResource, PhysicalResourceId, AwsCustomResourcePolicy } from 'aws-cdk-lib/custom-resources'; +import { + AwsCustomResource, + PhysicalResourceId, + AwsCustomResourcePolicy, +} from "aws-cdk-lib/custom-resources"; import { getThreshold } from "./utils/bedrock-guardrails"; const BLOCKED_INPUT_MESSAGE = "this input message is blocked"; @@ -48,23 +47,32 @@ interface BedrockGuardrailProps { } interface BedrockCustomBotStackProps extends StackProps { + // Base configuration readonly ownerUserId: string; readonly botId: string; + readonly bedrockClaudeChatDocumentBucketName: string; + readonly useStandbyReplicas?: boolean; + + // Knowledge base configuration readonly embeddingsModel: BedrockFoundationModel; readonly parsingModel?: BedrockFoundationModel; readonly existKnowledgeBaseId: string | undefined; - readonly bedrockClaudeChatDocumentBucketName: string; - readonly chunkingStrategy: ChunkingStrategy; readonly existingS3Urls: string[]; readonly sourceUrls: string[]; - readonly maxTokens?: number; readonly instruction?: string; readonly analyzer?: Analyzer; + + // Chunking configuration + readonly chunkingStrategy: ChunkingStrategy; + readonly maxTokens?: number; readonly overlapPercentage?: number; - readonly guardrail?: BedrockGuardrailProps; - readonly useStandbyReplicas?: boolean; + + // Crawling configuration readonly crawlingScope?: CrawlingScope; readonly crawlingFilters?: CrawlingFilters; + + // Guardrail configuration + readonly guardrail?: BedrockGuardrailProps; } export class BedrockCustomBotStack extends Stack { @@ -73,7 +81,7 @@ export class BedrockCustomBotStack extends Stack { const { docBucketsAndPrefixes } = this.setupBucketsAndPrefixes(props); - let kb: IKnowledgeBase + let kb: IKnowledgeBase; // if knowledge base arn does not exist if (props.existKnowledgeBaseId == undefined) { @@ -105,7 +113,7 @@ export class BedrockCustomBotStack extends Stack { ], analyzer: props.analyzer, }); - + kb = new KnowledgeBase(this, "KB", { embeddingsModel: props.embeddingsModel, vectorStore: vectorCollection, @@ -121,32 +129,39 @@ export class BedrockCustomBotStack extends Stack { knowledgeBase: kb, dataSourceName: bucket.bucketName, chunkingStrategy: props.chunkingStrategy, - parsingStrategy: props.parsingModel ? ParsingStategy.foundationModel({ - parsingModel: props.parsingModel.asIModel(this), - }) : undefined, + parsingStrategy: props.parsingModel + ? ParsingStategy.foundationModel({ + parsingModel: props.parsingModel.asIModel(this), + }) + : undefined, inclusionPrefixes: inclusionPrefixes, }); - }); + }); // Add Web Crawler Data Sources if (props.sourceUrls.length > 0) { - const webCrawlerDataSource = new WebCrawlerDataSource(this, 'WebCrawlerDataSource', { - knowledgeBase: kb, - sourceUrls: props.sourceUrls, - chunkingStrategy: props.chunkingStrategy, - parsingStrategy: props.parsingModel ? ParsingStategy.foundationModel({ - parsingModel: props.parsingModel.asIModel(this), - }) : undefined, - crawlingScope: props.crawlingScope, - filters: { - excludePatterns: props.crawlingFilters?.excludePatterns, - includePatterns: props.crawlingFilters?.includePatterns, + const webCrawlerDataSource = new WebCrawlerDataSource( + this, + "WebCrawlerDataSource", + { + knowledgeBase: kb, + sourceUrls: props.sourceUrls, + chunkingStrategy: props.chunkingStrategy, + parsingStrategy: props.parsingModel + ? ParsingStategy.foundationModel({ + parsingModel: props.parsingModel.asIModel(this), + }) + : undefined, + crawlingScope: props.crawlingScope, + filters: { + excludePatterns: props.crawlingFilters?.excludePatterns, + includePatterns: props.crawlingFilters?.includePatterns, + }, } - + ); + new CfnOutput(this, "DataSourceIdWebCrawler", { + value: webCrawlerDataSource.dataSourceId, }); - new CfnOutput(this, 'DataSourceIdWebCrawler', { - value: webCrawlerDataSource.dataSourceId - }) } if (props.guardrail?.is_guardrail_enabled == true) { @@ -154,7 +169,7 @@ export class BedrockCustomBotStack extends Stack { let contentPolicyConfigFiltersConfig = []; let contextualGroundingFiltersConfig = []; console.log("props.guardrail: ", props.guardrail); - + if ( props.guardrail.hateThreshold != undefined && props.guardrail.hateThreshold > 0 @@ -165,7 +180,7 @@ export class BedrockCustomBotStack extends Stack { type: "HATE", }); } - + if ( props.guardrail.insultsThreshold != undefined && props.guardrail.insultsThreshold > 0 @@ -176,7 +191,7 @@ export class BedrockCustomBotStack extends Stack { type: "INSULTS", }); } - + if ( props.guardrail.sexualThreshold != undefined && props.guardrail.sexualThreshold > 0 @@ -187,7 +202,7 @@ export class BedrockCustomBotStack extends Stack { type: "SEXUAL", }); } - + if ( props.guardrail.violenceThreshold != undefined && props.guardrail.violenceThreshold > 0 @@ -198,7 +213,7 @@ export class BedrockCustomBotStack extends Stack { type: "VIOLENCE", }); } - + if ( props.guardrail.misconductThreshold != undefined && props.guardrail.misconductThreshold > 0 @@ -209,7 +224,7 @@ export class BedrockCustomBotStack extends Stack { type: "MISCONDUCT", }); } - + if ( props.guardrail.groundingThreshold != undefined && props.guardrail.groundingThreshold > 0 @@ -219,7 +234,7 @@ export class BedrockCustomBotStack extends Stack { type: "GROUNDING", }); } - + if ( props.guardrail.relevanceThreshold != undefined && props.guardrail.relevanceThreshold > 0 @@ -229,7 +244,7 @@ export class BedrockCustomBotStack extends Stack { type: "RELEVANCE", }); } - + console.log( "contentPolicyConfigFiltersConfig: ", contentPolicyConfigFiltersConfig @@ -238,7 +253,7 @@ export class BedrockCustomBotStack extends Stack { "contextualGroundingFiltersConfig: ", contextualGroundingFiltersConfig ); - + // Deploy Guardrail if it contains at least one configuration value if ( contentPolicyConfigFiltersConfig.length > 0 || @@ -276,14 +291,12 @@ export class BedrockCustomBotStack extends Stack { value: dataSource.dataSourceId, }); }); - } - else - { + } else { // if knowledgeBaseArn exists - const getKnowledgeBase = new AwsCustomResource(this, 'GetKnowledgeBase', { + const getKnowledgeBase = new AwsCustomResource(this, "GetKnowledgeBase", { onCreate: { - service: 'bedrock-agent', - action: 'GetKnowledgeBase', + service: "bedrock-agent", + action: "GetKnowledgeBase", parameters: { knowledgeBaseId: props.existKnowledgeBaseId, }, @@ -291,20 +304,20 @@ export class BedrockCustomBotStack extends Stack { }, policy: AwsCustomResourcePolicy.fromStatements([ new iam.PolicyStatement({ - actions: [ - 'bedrock:GetKnowledgeBase', + actions: ["bedrock:GetKnowledgeBase"], + resources: [ + `arn:aws:bedrock:${this.region}:${this.account}:knowledge-base/${props.existKnowledgeBaseId}`, ], - resources: [`arn:aws:bedrock:${this.region}:${this.account}:knowledge-base/${props.existKnowledgeBaseId}`], }), - ]), + ]), }); - const executionRoleArn = getKnowledgeBase.getResponseField('roleArn'); + const executionRoleArn = getKnowledgeBase.getResponseField("roleArn"); - kb = KnowledgeBase.fromKnowledgeBaseAttributes(this, 'MyKnowledgeBase', { + kb = KnowledgeBase.fromKnowledgeBaseAttributes(this, "MyKnowledgeBase", { knowledgeBaseId: props.existKnowledgeBaseId, - executionRoleArn: executionRoleArn - }) + executionRoleArn: executionRoleArn, + }); } new CfnOutput(this, "KnowledgeBaseId", { From 7b1235a8cdb50cac2f2e7377041492f5d7eaafcb Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Sat, 15 Mar 2025 00:30:31 +0900 Subject: [PATCH 16/22] log formatted json --- cdk/bin/bedrock-custom-bot.ts | 47 +++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index 348f812d0..388bdef0f 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -175,30 +175,45 @@ const guardrailConfig: GuardrailConfig = { }; // Log organized configurations for debugging -console.log("Base Configuration:", JSON.stringify(baseConfig)); +console.log("Base Configuration:", JSON.stringify(baseConfig, null, 2)); console.log( "Knowledge Configuration:", - JSON.stringify({ - ...knowledgeConfig, - embeddingsModel: knowledgeConfig.embeddingsModel.toString(), - parsingModel: knowledgeConfig.parsingModel?.toString(), - analyzer: knowledgeConfig.analyzer ? "configured" : "undefined", - }) + JSON.stringify( + { + ...knowledgeConfig, + embeddingsModel: knowledgeConfig.embeddingsModel.toString(), + parsingModel: knowledgeConfig.parsingModel?.toString(), + analyzer: knowledgeConfig.analyzer ? "configured" : "undefined", + }, + null, + 2 + ) ); console.log( "Chunking Configuration:", - JSON.stringify({ - ...chunkingConfig, - chunkingStrategy: chunkingConfig.chunkingStrategy.toString(), - }) + JSON.stringify( + { + ...chunkingConfig, + chunkingStrategy: chunkingConfig.chunkingStrategy.toString(), + }, + null, + 2 + ) +); +console.log( + "Guardrail Configuration:", + JSON.stringify(guardrailConfig, null, 2) ); -console.log("Guardrail Configuration:", JSON.stringify(guardrailConfig)); console.log( "Crawling Configuration:", - JSON.stringify({ - crawlingScope: crawlingConfig.crawlingScope?.toString(), - crawlingFilters: crawlingConfig.crawlingFilters, - }) + JSON.stringify( + { + crawlingScope: crawlingConfig.crawlingScope?.toString(), + crawlingFilters: crawlingConfig.crawlingFilters, + }, + null, + 2 + ) ); // Create the stack From f96281a4f9885e543731c0cbe0eadcd60f618a13 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Sat, 15 Mar 2025 13:20:54 +0900 Subject: [PATCH 17/22] use concrete types --- cdk/bin/bedrock-custom-bot.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index 388bdef0f..492b25361 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -9,7 +9,10 @@ import { getCrowlingScope, getCrawlingFilters, } from "../lib/utils/bedrock-knowledge-base-args"; -import { CrawlingFilters } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/web-crawler-data-source"; +import { BedrockFoundationModel } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; +import { ChunkingStrategy } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/chunking"; +import { CrawlingFilters, CrawlingScope } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/web-crawler-data-source"; +import { Analyzer } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearch-vectorindex"; import { resolveBedrockCustomBotParameters } from "../lib/utils/parameter-models"; const app = new cdk.App(); @@ -34,17 +37,17 @@ interface BaseConfig { } interface KnowledgeConfig { - embeddingsModel: ReturnType; - parsingModel: ReturnType | undefined; + embeddingsModel: BedrockFoundationModel; + parsingModel: BedrockFoundationModel | undefined; existKnowledgeBaseId?: string; existingS3Urls: string[]; sourceUrls: string[]; instruction?: string; - analyzer?: ReturnType; + analyzer?: Analyzer | undefined; } interface ChunkingConfig { - chunkingStrategy: ReturnType; + chunkingStrategy: ChunkingStrategy; maxTokens?: number; overlapPercentage?: number; overlapTokens?: number; // not used @@ -68,7 +71,7 @@ interface GuardrailConfig { } interface CrawlingConfig { - crawlingScope?: ReturnType; + crawlingScope?: CrawlingScope | undefined; crawlingFilters: CrawlingFilters; } From 61668f4cd5e126e050e3d8363aa64ebb55b9986e Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Sat, 15 Mar 2025 14:06:21 +0900 Subject: [PATCH 18/22] add doc --- README.md | 103 +++++++++++++++++++++++++++++++++++++++++- docs/ADMINISTRATOR.md | 20 ++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c6290698c..fcbd57315 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,7 @@ [English](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/README.md) | [日本語](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_ja-JP.md) | [한국어](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_ko-KR.md) | [中文](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_zh-CN.md) | [Français](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_fr-FR.md) | [Deutsch](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_de-DE.md) | [Español](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_es-ES.md) | [Italian](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_it-IT.md) | [Norsk](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_nb-NO.md) | [ไทย](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_th-TH.md) | [Bahasa Indonesia](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_id-ID.md) | [Bahasa Melayu](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_ms-MY.md) | [Tiếng Việt](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_vi-VN.md) | [Polski](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_pl-PL.md) -> [!Warning] -> **V2 released. To update, please carefully review the [migration guide](./docs/migration/V1_TO_V2.md).** Without any care, **BOTS FROM V1 WILL BECOME UNUSABLE.** +> [!Warning] > **V2 released. To update, please carefully review the [migration guide](./docs/migration/V1_TO_V2.md).** Without any care, **BOTS FROM V1 WILL BECOME UNUSABLE.** A multilingual chatbot using LLM models provided by [Amazon Bedrock](https://aws.amazon.com/bedrock/) for generative AI. @@ -215,6 +214,106 @@ BedrockChatStack.BackendApiBackendApiUrlXXXXX = https://xxxxx.execute-api.ap-nor BedrockChatStack.FrontendURL = https://xxxxx.cloudfront.net ``` +### Defining Parameters + +You can define parameters for your deployment in two ways: using `cdk.json` or using the type-safe `parameter.ts` file. + +#### Using cdk.json (Traditional Method) + +The traditional way to configure parameters is by editing the `cdk.json` file. This approach is simple but lacks type checking: + +```json +{ + "app": "npx ts-node --prefer-ts-exts bin/bedrock-chat.ts", + "context": { + "bedrockRegion": "us-east-1", + "allowedIpV4AddressRanges": ["0.0.0.0/1", "128.0.0.0/1"], + "enableMistral": false, + "selfSignUpEnabled": true + } +} +``` + +#### Using parameter.ts (Recommended Type-Safe Method) + +For better type safety and developer experience, you can use the `parameter.ts` file to define your parameters: + +```typescript +// Define parameters for the default environment +bedrockChatParams.set("default", { + bedrockRegion: "us-east-1", + allowedIpV4AddressRanges: ["192.168.0.0/16"], + enableMistral: false, + selfSignUpEnabled: true, +}); + +// Define parameters for additional environments +bedrockChatParams.set("dev", { + bedrockRegion: "us-west-2", + allowedIpV4AddressRanges: ["10.0.0.0/8"], + enableRagReplicas: false, // Cost-saving for dev environment +}); + +bedrockChatParams.set("prod", { + bedrockRegion: "us-east-1", + allowedIpV4AddressRanges: ["172.16.0.0/12"], + enableLambdaSnapStart: true, + enableRagReplicas: true, // Enhanced availability for production +}); +``` + +> [!Note] +> Existing users can continue using `cdk.json` without any changes. The `parameter.ts` approach is recommended for new deployments or when you need to manage multiple environments. + +### Deploying Multiple Environments + +You can deploy multiple environments from the same codebase using the `parameter.ts` file and the `-c envName` option. + +#### Prerequisites + +1. Define your environments in `parameter.ts` as shown above +2. Each environment will have its own set of resources with environment-specific prefixes + +#### Deployment Commands + +To deploy a specific environment: + +```bash +# Deploy the dev environment +npx cdk deploy --all -c envName=dev + +# Deploy the prod environment +npx cdk deploy --all -c envName=prod +``` + +If no environment is specified, the "default" environment is used: + +```bash +# Deploy the default environment +npx cdk deploy --all +``` + +#### Important Notes + +1. **Stack Naming**: + - The main stacks for each environment will be prefixed with the environment name (e.g., `dev-BedrockChatStack`, `prod-BedrockChatStack`) + - However, custom bot stacks (`BrChatKbStack*`) and API publishing stacks (`ApiPublishmentStack*`) do not receive environment prefixes as they are created dynamically at runtime + +2. **Resource Naming**: + - Only some resources receive environment prefixes in their names (e.g., `dev_ddb_export` table, `dev-FrontendWebAcl`) + - Most resources maintain their original names but are isolated by being in different stacks + +3. **Environment Identification**: + - All resources are tagged with a `CDKEnvironment` tag containing the environment name + - You can use this tag to identify which environment a resource belongs to + - Example: `CDKEnvironment: dev` or `CDKEnvironment: prod` + +4. **Default Environment Override**: If you define a "default" environment in `parameter.ts`, it will override the settings in `cdk.json`. To continue using `cdk.json`, don't define a "default" environment in `parameter.ts`. + +5. **Environment Requirements**: To create environments other than "default", you must use `parameter.ts`. The `-c envName` option alone is not sufficient without corresponding environment definitions. + +6. **Resource Isolation**: Each environment creates its own set of resources, allowing you to have development, testing, and production environments in the same AWS account without conflicts. + ## Others ### Configure Mistral models support diff --git a/docs/ADMINISTRATOR.md b/docs/ADMINISTRATOR.md index 722717b1d..3f6f98e6e 100644 --- a/docs/ADMINISTRATOR.md +++ b/docs/ADMINISTRATOR.md @@ -35,6 +35,20 @@ The admin user must be a member of group called `Admin`, which can be set up via - In user usages, users who have not used the system at all during the specified period will not be listed. +> [!Important] > **Multi-Environment Database Names** +> +> If you're using multiple environments (dev, prod, etc.), the Athena database name will include the environment prefix. Instead of `bedrockchatstack_usage_analysis`, the database name will be: +> +> - For default environment: `bedrockchatstack_usage_analysis` +> - For named environments: `_bedrockchatstack_usage_analysis` (e.g., `dev_bedrockchatstack_usage_analysis`) +> +> Additionally, the table name will include the environment prefix: +> +> - For default environment: `ddb_export` +> - For named environments: `_ddb_export` (e.g., `dev_ddb_export`) +> +> Make sure to adjust your queries accordingly when working with multiple environments. + ## Download conversation data You can query the conversation logs by Athena, using SQL. To download logs, open Athena Query Editor from management console and run SQL. Followings are some example queries which are useful to analyze use-cases. Feedback can be referred in `MessageMap` attribute. @@ -63,6 +77,9 @@ ORDER BY d.datehour DESC; ``` +> [!Note] +> If using a named environment (e.g., "dev"), replace `bedrockchatstack_usage_analysis.ddb_export` with `dev_bedrockchatstack_usage_analysis.dev_ddb_export` in the query above. + ### Query per User ID Edit `user-id` and `datehour`. `user-id` can be referred on Bot Management screen. @@ -89,3 +106,6 @@ WHERE ORDER BY d.datehour DESC; ``` + +> [!Note] +> If using a named environment (e.g., "dev"), replace `bedrockchatstack_usage_analysis.ddb_export` with `dev_bedrockchatstack_usage_analysis.dev_ddb_export` in the query above. From c2e1cd9d24ade63b1f090e3ea228de60d475355b Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Sat, 15 Mar 2025 14:12:46 +0900 Subject: [PATCH 19/22] fix database name --- cdk/lib/constructs/usage-analysis.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cdk/lib/constructs/usage-analysis.ts b/cdk/lib/constructs/usage-analysis.ts index 6e9ebf34a..50a24347a 100644 --- a/cdk/lib/constructs/usage-analysis.ts +++ b/cdk/lib/constructs/usage-analysis.ts @@ -29,9 +29,11 @@ export class UsageAnalysis extends Construct { constructor(scope: Construct, id: string, props: UsageAnalysisProps) { super(scope, id); - const GLUE_DATABASE_NAME = `${Stack.of( - this - ).stackName.toLowerCase()}_usage_analysis`; + const safeStackName = Stack.of(this) + .stackName.toLowerCase() + .replace("-", "_"); + + const GLUE_DATABASE_NAME = `${safeStackName}_usage_analysis`; const sepUnderscore = props.envPrefix ? "_" : ""; const DDB_EXPORT_TABLE_NAME = `${props.envPrefix}${sepUnderscore}ddb_export`; @@ -62,7 +64,7 @@ export class UsageAnalysis extends Construct { // Workgroup for Athena const wg = new athena.CfnWorkGroup(this, "Wg", { - name: `${Stack.of(this).stackName.toLowerCase()}_wg`, + name: `${Stack.of(this).stackName.toLowerCase().replace("-", "_")}_wg`, description: "Workgroup for Athena", recursiveDeleteOption: true, workGroupConfiguration: { From e0c27e8cc371c7f1e75a694022796ce98aeba1e2 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Sat, 15 Mar 2025 20:21:10 +0900 Subject: [PATCH 20/22] fix wrong comment --- cdk/bin/bedrock-custom-bot.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/cdk/bin/bedrock-custom-bot.ts b/cdk/bin/bedrock-custom-bot.ts index 492b25361..e8a3e3896 100644 --- a/cdk/bin/bedrock-custom-bot.ts +++ b/cdk/bin/bedrock-custom-bot.ts @@ -11,7 +11,10 @@ import { } from "../lib/utils/bedrock-knowledge-base-args"; import { BedrockFoundationModel } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock"; import { ChunkingStrategy } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/chunking"; -import { CrawlingFilters, CrawlingScope } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/web-crawler-data-source"; +import { + CrawlingFilters, + CrawlingScope, +} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/bedrock/data-sources/web-crawler-data-source"; import { Analyzer } from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearch-vectorindex"; import { resolveBedrockCustomBotParameters } from "../lib/utils/parameter-models"; @@ -50,11 +53,11 @@ interface ChunkingConfig { chunkingStrategy: ChunkingStrategy; maxTokens?: number; overlapPercentage?: number; - overlapTokens?: number; // not used - maxParentTokenSize?: number; // not used - maxChildTokenSize?: number; // not used - bufferSize?: number; // not used - breakpointPercentileThreshold?: number; // not used + overlapTokens?: number; + maxParentTokenSize?: number; + maxChildTokenSize?: number; + bufferSize?: number; + breakpointPercentileThreshold?: number; } interface GuardrailConfig { From 3d98bde528739e109c3678fd8fceb78ed87fe75a Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Sat, 15 Mar 2025 20:32:26 +0900 Subject: [PATCH 21/22] fix --- README.md | 9 +++++++-- cdk/lib/constructs/usage-analysis.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index fcbd57315..9c5760c19 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,9 @@ [English](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/README.md) | [日本語](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_ja-JP.md) | [한국어](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_ko-KR.md) | [中文](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_zh-CN.md) | [Français](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_fr-FR.md) | [Deutsch](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_de-DE.md) | [Español](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_es-ES.md) | [Italian](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_it-IT.md) | [Norsk](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_nb-NO.md) | [ไทย](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_th-TH.md) | [Bahasa Indonesia](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_id-ID.md) | [Bahasa Melayu](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_ms-MY.md) | [Tiếng Việt](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_vi-VN.md) | [Polski](https://github.com/aws-samples/bedrock-claude-chat/blob/v2/docs/README_pl-PL.md) -> [!Warning] > **V2 released. To update, please carefully review the [migration guide](./docs/migration/V1_TO_V2.md).** Without any care, **BOTS FROM V1 WILL BECOME UNUSABLE.** +> [!Warning] +> +> **V2 released. To update, please carefully review the [migration guide](./docs/migration/V1_TO_V2.md).** Without any care, **BOTS FROM V1 WILL BECOME UNUSABLE.** A multilingual chatbot using LLM models provided by [Amazon Bedrock](https://aws.amazon.com/bedrock/) for generative AI. @@ -295,15 +297,18 @@ npx cdk deploy --all #### Important Notes -1. **Stack Naming**: +1. **Stack Naming**: + - The main stacks for each environment will be prefixed with the environment name (e.g., `dev-BedrockChatStack`, `prod-BedrockChatStack`) - However, custom bot stacks (`BrChatKbStack*`) and API publishing stacks (`ApiPublishmentStack*`) do not receive environment prefixes as they are created dynamically at runtime 2. **Resource Naming**: + - Only some resources receive environment prefixes in their names (e.g., `dev_ddb_export` table, `dev-FrontendWebAcl`) - Most resources maintain their original names but are isolated by being in different stacks 3. **Environment Identification**: + - All resources are tagged with a `CDKEnvironment` tag containing the environment name - You can use this tag to identify which environment a resource belongs to - Example: `CDKEnvironment: dev` or `CDKEnvironment: prod` diff --git a/cdk/lib/constructs/usage-analysis.ts b/cdk/lib/constructs/usage-analysis.ts index 50a24347a..6e6916b32 100644 --- a/cdk/lib/constructs/usage-analysis.ts +++ b/cdk/lib/constructs/usage-analysis.ts @@ -64,7 +64,7 @@ export class UsageAnalysis extends Construct { // Workgroup for Athena const wg = new athena.CfnWorkGroup(this, "Wg", { - name: `${Stack.of(this).stackName.toLowerCase().replace("-", "_")}_wg`, + name: `${safeStackName}_wg`, description: "Workgroup for Athena", recursiveDeleteOption: true, workGroupConfiguration: { From 59ed3a44c6841eea24d89c51fd7d9cc67770a077 Mon Sep 17 00:00:00 2001 From: Kenji Kono Date: Thu, 20 Mar 2025 00:27:47 +0900 Subject: [PATCH 22/22] chore(cdk): Extract ENV_NAME as a global constant in utils.py. Also fix some comments --- backend/app/usecases/publication.py | 1 - backend/app/utils.py | 5 +++-- cdk/lib/utils/parameter-models.ts | 5 +++-- cdk/test/utils/parameter-models.test.ts | 17 +++++++++-------- 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/backend/app/usecases/publication.py b/backend/app/usecases/publication.py index 2370b044b..a9c54b6c0 100644 --- a/backend/app/usecases/publication.py +++ b/backend/app/usecases/publication.py @@ -81,7 +81,6 @@ def create_bot_publication(user: User, bot_id: str, bot_publish_input: BotPublis environment_variables["PUBLISHED_API_ID"] = published_api_id # Set environment variables. - # NOTE: default values are set in `cdk/lib/constructs/api-publish-codebuild.ts` if bot_publish_input.throttle.rate_limit is not None: environment_variables["PUBLISHED_API_THROTTLE_RATE_LIMIT"] = str( bot_publish_input.throttle.rate_limit diff --git a/backend/app/utils.py b/backend/app/utils.py index e9e500ec9..5f0be0abc 100644 --- a/backend/app/utils.py +++ b/backend/app/utils.py @@ -12,9 +12,11 @@ REGION = os.environ.get("REGION", "us-east-1") BEDROCK_REGION = os.environ.get("BEDROCK_REGION", "us-east-1") +ENV_NAME = os.environ.get("ENV_NAME", "default") logger.debug(f"REGION: {REGION}") logger.debug(f"BEDROCK_REGION: {BEDROCK_REGION}") +logger.debug(f"ENV_NAME: {ENV_NAME}") PUBLISH_API_CODEBUILD_PROJECT_NAME = os.environ.get( "PUBLISH_API_CODEBUILD_PROJECT_NAME", "" @@ -194,7 +196,6 @@ def store_api_key_to_secret_manager( """ secret_name = f"{prefix}/{user_id}/{bot_id}" secret_value = json.dumps({"api_key": api_key}) - env_name = os.environ.get("ENV_NAME", "default") try: secrets_client = boto3.client("secretsmanager") @@ -219,7 +220,7 @@ def store_api_key_to_secret_manager( response = secrets_client.create_secret( Name=secret_name, SecretString=secret_value, - Tags=[{"Key": "CDKEnvironment", "Value": env_name}], + Tags=[{"Key": "CDKEnvironment", "Value": ENV_NAME}], ) logger.info(f"Created new secret: {secret_name}") return response["ARN"] diff --git a/cdk/lib/utils/parameter-models.ts b/cdk/lib/utils/parameter-models.ts index 43cb0bd25..c53853941 100644 --- a/cdk/lib/utils/parameter-models.ts +++ b/cdk/lib/utils/parameter-models.ts @@ -56,7 +56,9 @@ const BedrockChatParametersSchema = BaseParametersSchema.extend({ // Authentication and user management identityProviders: z .unknown() - .transform(val => Array.isArray(val) ? val as TIdentityProvider[] : []) + .transform((val) => + Array.isArray(val) ? (val as TIdentityProvider[]) : [] + ) .pipe(z.array(z.custom())) .default([]), userPoolDomainPrefix: z.string().default(""), @@ -171,7 +173,6 @@ export function resolveBedrockChatParameters( allowedIpV6AddressRanges: app.node.tryGetContext( "allowedIpV6AddressRanges" ), - // 配列でない場合は空配列を使用 identityProviders: app.node.tryGetContext("identityProviders"), userPoolDomainPrefix: app.node.tryGetContext("userPoolDomainPrefix"), allowedSignUpEmailDomains: app.node.tryGetContext( diff --git a/cdk/test/utils/parameter-models.test.ts b/cdk/test/utils/parameter-models.test.ts index dd1f15160..323c777b2 100644 --- a/cdk/test/utils/parameter-models.test.ts +++ b/cdk/test/utils/parameter-models.test.ts @@ -9,7 +9,7 @@ import { import { ZodError } from "zod"; /** - * テストヘルパー関数: App インスタンスを作成 + * Test helper function: create CDK App instance */ function createTestApp(context = {}) { return new App({ @@ -413,10 +413,10 @@ describe("getBedrockChatParameters", () => { test("should use default values when CDK context is empty", () => { // Given const emptyParamsMap = new Map(); - + // When const result = getBedrockChatParameters(app, undefined, emptyParamsMap); - + // Then expect(result.bedrockRegion).toBe("us-east-1"); expect(result.enableMistral).toBe(false); @@ -531,7 +531,8 @@ describe("resolveApiPublishParameters", () => { PUBLISHED_API_QUOTA_PERIOD: "DAY", PUBLISHED_API_DEPLOYMENT_STAGE: "prod", PUBLISHED_API_ID: "api123", - PUBLISHED_API_ALLOWED_ORIGINS: '["https://example.com", "https://test.com"]', + PUBLISHED_API_ALLOWED_ORIGINS: + '["https://example.com", "https://test.com"]', }; try { @@ -581,16 +582,16 @@ describe("resolveApiPublishParameters", () => { const originalEnv = process.env; process.env = { ...originalEnv, - PUBLISHED_API_ALLOWED_ORIGINS: 'invalid json format', + PUBLISHED_API_ALLOWED_ORIGINS: "invalid json format", }; - + try { // When const result = resolveApiPublishParameters(); - + // Then // Note: The function doesn't validate JSON format, it just passes the string through - expect(result.publishedApiAllowedOrigins).toBe('invalid json format'); + expect(result.publishedApiAllowedOrigins).toBe("invalid json format"); } finally { // Restore original environment process.env = originalEnv;