@@ -12,10 +12,8 @@ import {
1212 BlockPublicAccess ,
1313 ObjectOwnership
1414} from "aws-cdk-lib/aws-s3"
15- import * as AWSCDK from "aws-cdk-lib/aws-s3"
1615import { Key } from "aws-cdk-lib/aws-kms"
17- import { PolicyStatement } from "aws-cdk-lib/aws-iam"
18- import { CfnResource } from "aws-cdk-lib"
16+ import { Role , ServicePrincipal , PolicyStatement } from "aws-cdk-lib/aws-iam"
1917import {
2018 CfnGuardrail ,
2119 CfnGuardrailVersion ,
@@ -39,7 +37,7 @@ export class EpsAssistMeStack extends Stack {
3937 public constructor ( scope : App , id : string , props : EpsAssistMeStackProps ) {
4038 super ( scope , id , props )
4139
42- // ==== Context and constants ====
40+ // ==== Context/Parameters ====
4341 const region = Stack . of ( this ) . region
4442 const account = Stack . of ( this ) . account
4543 const logRetentionInDays = Number ( this . node . tryGetContext ( "logRetentionInDays" ) ) || 14
@@ -52,103 +50,30 @@ export class EpsAssistMeStack extends Stack {
5250 this , "cloudWatchLogsKmsKey" , Fn . importValue ( "account-resources:CloudwatchLogsKmsKeyArn" )
5351 )
5452
55- // ==== Access Logs Bucket ====
53+ // ==== S3 Buckets ====
54+ // Access logs bucket for S3
5655 const accessLogBucket = new Bucket ( this , "EpsAssistAccessLogsBucket" , {
5756 encryption : BucketEncryption . KMS ,
5857 encryptionKey : cloudWatchLogsKmsKey ,
5958 removalPolicy : RemovalPolicy . DESTROY ,
6059 blockPublicAccess : BlockPublicAccess . BLOCK_ALL ,
6160 versioned : true ,
62- // objectLockEnabled: true, deployment role lacks s3:PutBucketObjectLockConfiguration permission
6361 objectOwnership : ObjectOwnership . BUCKET_OWNER_ENFORCED
6462 } )
6563
66- // Get the underlying CFN resource
67- const accessLogBucketCfn = accessLogBucket . node . defaultChild as AWSCDK . CfnBucket
68- // Removed replication configuration as deployment role lacks s3:PutReplicationConfiguration permission
69- // accessLogBucketCfn.replicationConfiguration = {
70- // role: `arn:aws:iam::${account}:role/account-resources-s3-replication-role`,
71- // rules: [{
72- // status: "Enabled",
73- // priority: 1,
74- // destination: {
75- // bucket: "arn:aws:s3:::dummy-replication-bucket"
76- // },
77- // deleteMarkerReplication: {status: "Disabled"}
78- // }]
79- // }
80-
81- // TLS-only policy (strictly compliant for cfn-guard)
82- new AWSCDK . CfnBucketPolicy ( this , "AccessLogsBucketTlsPolicy" , {
83- bucket : accessLogBucketCfn . ref ,
84- policyDocument : {
85- Version : "2012-10-17" ,
86- Statement : [
87- {
88- Action : "s3:*" ,
89- Effect : "Deny" ,
90- Principal : "*" ,
91- Resource : "*" ,
92- Condition : {
93- Bool : {
94- "aws:SecureTransport" : false
95- }
96- }
97- }
98- ]
99- }
100- } )
101-
102- // ==== Document Bucket ====
64+ // S3 bucket for Bedrock Knowledge Base documents
10365 const kbDocsBucket = new Bucket ( this , "EpsAssistDocsBucket" , {
10466 encryptionKey : cloudWatchLogsKmsKey ,
10567 encryption : BucketEncryption . KMS ,
10668 removalPolicy : RemovalPolicy . DESTROY ,
10769 blockPublicAccess : BlockPublicAccess . BLOCK_ALL ,
10870 versioned : true ,
109- // objectLockEnabled: true, deployment role lacks s3:PutBucketObjectLockConfiguration permission
11071 objectOwnership : ObjectOwnership . BUCKET_OWNER_ENFORCED ,
11172 serverAccessLogsBucket : accessLogBucket ,
11273 serverAccessLogsPrefix : "s3-access-logs/"
11374 } )
11475
115- // Get the underlying CFN resource
116- const kbDocsBucketCfn = kbDocsBucket . node . defaultChild as AWSCDK . CfnBucket
117- // Removed replication configuration as deployment role lacks s3:PutReplicationConfiguration permission
118- // kbDocsBucketCfn.replicationConfiguration = {
119- // role: `arn:aws:iam::${account}:role/account-resources-s3-replication-role`,
120- // rules: [{
121- // status: "Enabled",
122- // priority: 1,
123- // destination: {
124- // bucket: "arn:aws:s3:::dummy-replication-bucket"
125- // },
126- // deleteMarkerReplication: {status: "Disabled"}
127- // }]
128- // }
129-
130- // TLS-only policy (strictly compliant for cfn-guard)
131- new AWSCDK . CfnBucketPolicy ( this , "KbDocsTlsPolicy" , {
132- bucket : kbDocsBucketCfn . ref ,
133- policyDocument : {
134- Version : "2012-10-17" ,
135- Statement : [
136- {
137- Action : "s3:*" ,
138- Effect : "Deny" ,
139- Principal : "*" ,
140- Resource : "*" ,
141- Condition : {
142- Bool : {
143- "aws:SecureTransport" : false
144- }
145- }
146- }
147- ]
148- }
149- } )
150-
151- // ==== Guardrail ====
76+ // ==== Bedrock Guardrail and Version ====
15277 const guardrail = new CfnGuardrail ( this , "EpsGuardrail" , {
15378 name : "eps-assist-guardrail" ,
15479 description : "Guardrail for EPS Assist Me bot" ,
@@ -172,13 +97,13 @@ export class EpsAssistMeStack extends Stack {
17297 }
17398 } )
17499
100+ // Add metadata to the guardrail for cfn-guard compliance
175101 const guardrailVersion = new CfnGuardrailVersion ( this , "EpsGuardrailVersion" , {
176102 guardrailIdentifier : guardrail . attrGuardrailId ,
177103 description : "Initial version of the EPS Assist Me Guardrail"
178104 } )
179105
180- // ==== OpenSearch Vector Store ====
181- // OpenSearch encryption policy (AWS-owned key)
106+ // ==== OpenSearch Serverless: Security & Collection ====
182107 const osEncryptionPolicy = new ops . CfnSecurityPolicy ( this , "OsEncryptionPolicy" , {
183108 name : "eps-assist-encryption-policy" ,
184109 type : "encryption" ,
@@ -188,32 +113,28 @@ export class EpsAssistMeStack extends Stack {
188113 } )
189114 } )
190115
191- // Create the collection after the encryption policy
116+ // OpenSearch Serverless Collection for EPS Assist
192117 const osCollection = new ops . CfnCollection ( this , "OsCollection" , {
193118 name : "eps-assist-vector-db" ,
194119 description : "EPS Assist Vector Store" ,
195120 type : "VECTORSEARCH"
196121 } )
197-
198- // Add explicit dependency to ensure correct creation order
199122 osCollection . addDependency ( osEncryptionPolicy )
200123
201- // OpenSearch network policy (allow public access for demo purposes)
124+ // OpenSearch Serverless Security Policy for public access
202125 new ops . CfnSecurityPolicy ( this , "OsNetworkPolicy" , {
203126 name : "eps-assist-network-policy" ,
204127 type : "network" ,
205- policy : JSON . stringify ( [
206- {
207- Rules : [
208- { ResourceType : "collection" , Resource : [ "collection/eps-assist-vector-db" ] } ,
209- { ResourceType : "dashboard" , Resource : [ "collection/eps-assist-vector-db" ] }
210- ] ,
211- AllowFromPublic : true
212- }
213- ] )
128+ policy : JSON . stringify ( [ {
129+ Rules : [
130+ { ResourceType : "collection" , Resource : [ "collection/eps-assist-vector-db" ] } ,
131+ { ResourceType : "dashboard" , Resource : [ "collection/eps-assist-vector-db" ] }
132+ ] ,
133+ AllowFromPublic : true
134+ } ] )
214135 } )
215136
216- // ==== Lambda Function: CreateIndex ====
137+ // ==== Lambda Function for Vector Index Creation ====
217138 const createIndexFunction = new LambdaFunction ( this , "CreateIndexFunction" , {
218139 stackName : props . stackName ,
219140 functionName : `${ props . stackName } -CreateIndexFunction` ,
@@ -225,27 +146,24 @@ export class EpsAssistMeStack extends Stack {
225146 additionalPolicies : [ ]
226147 } )
227148
228- // Access policy for Bedrock + Lambda to use the collection and index
149+ // ==== AOSS Access Policy for Lambda & Bedrock ====
229150 new ops . CfnAccessPolicy ( this , "OsAccessPolicy" , {
230151 name : "eps-assist-access-policy" ,
231152 type : "data" ,
232- policy : JSON . stringify ( [
233- {
234- Rules : [
235- { ResourceType : "collection" , Resource : [ "collection/*" ] , Permission : [ "aoss:*" ] } ,
236- { ResourceType : "index" , Resource : [ "index/*/*" ] , Permission : [ "aoss:*" ] }
237- ] ,
238- Principal : [
239- `arn:aws:iam::${ account } :role/${ createIndexFunction . function . role ?. roleName } ` ,
240- `arn:aws:iam::${ account } :root`
241- ]
242- }
243- ] )
153+ policy : JSON . stringify ( [ {
154+ Rules : [
155+ { ResourceType : "collection" , Resource : [ "collection/*" ] , Permission : [ "aoss:*" ] } ,
156+ { ResourceType : "index" , Resource : [ "index/*/*" ] , Permission : [ "aoss:*" ] }
157+ ] ,
158+ Principal : [
159+ `arn:aws:iam::${ account } :role/${ createIndexFunction . function . role ?. roleName } ` ,
160+ `arn:aws:iam::${ account } :root`
161+ ]
162+ } ] )
244163 } )
245164
246- // ==== Trigger Vector Index Creation ====
165+ // ==== Index Creation: Custom Resource Triggers Lambda ====
247166 const endpoint = `${ osCollection . attrId } .${ region } .aoss.amazonaws.com`
248-
249167 const vectorIndex = new cr . AwsCustomResource ( this , "VectorIndex" , {
250168 installLatestAwsSdk : true ,
251169 onCreate : {
@@ -261,8 +179,7 @@ export class EpsAssistMeStack extends Stack {
261179 Endpoint : endpoint
262180 } )
263181 } ,
264- // Use a timestamp to ensure this resource is always updated
265- physicalResourceId : cr . PhysicalResourceId . of ( `VectorIndex-${ Date . now ( ) } ` )
182+ physicalResourceId : cr . PhysicalResourceId . of ( "VectorIndex-eps-assist-os-index" )
266183 } ,
267184 onUpdate : {
268185 service : "Lambda" ,
@@ -277,8 +194,7 @@ export class EpsAssistMeStack extends Stack {
277194 Endpoint : endpoint
278195 } )
279196 } ,
280- // Use a timestamp to ensure this resource is always updated
281- physicalResourceId : cr . PhysicalResourceId . of ( `VectorIndex-${ Date . now ( ) } ` )
197+ physicalResourceId : cr . PhysicalResourceId . of ( "VectorIndex-eps-assist-os-index" )
282198 } ,
283199 onDelete : {
284200 service : "Lambda" ,
@@ -302,31 +218,47 @@ export class EpsAssistMeStack extends Stack {
302218 ] )
303219 } )
304220
305- // ==== Bedrock Knowledge Base ====
306- // Create a service role for Bedrock Knowledge Base
307- // const bedrockKbRole = new Role(this, "BedrockKbRole ", {
308- // assumedBy: new ServicePrincipal("bedrock.amazonaws.com"),
309- // description: "Role for Bedrock Knowledge Base to access OpenSearch and S3 "
310- // })
221+ // ==== Bedrock Execution Role for Knowledge Base ====
222+ // This role allows Bedrock to access S3 documents, use OpenSearch Serverless, and call the embedding model.
223+ const bedrockKbRole = new Role ( this , "EpsAssistMeBedrockExecutionRole " , {
224+ assumedBy : new ServicePrincipal ( "bedrock.amazonaws.com" ) ,
225+ description : "Role for Bedrock Knowledge Base to access S3 and OpenSearch "
226+ } )
311227
312- // // Add permissions to access OpenSearch and S3
313- // bedrockKbRole.addToPolicy(new PolicyStatement({
314- // actions: ["aoss:*"],
315- // resources: [osCollection.attrArn, `${osCollection.attrArn}/*`]
316- // }))
228+ // Allow Bedrock to read/list objects in the docs S3 bucket
229+ bedrockKbRole . addToPolicy ( new PolicyStatement ( {
230+ actions : [ "s3:GetObject" , "s3:ListBucket" ] ,
231+ resources : [
232+ kbDocsBucket . bucketArn ,
233+ `${ kbDocsBucket . bucketArn } /*`
234+ ]
235+ } ) )
317236
318- // bedrockKbRole.addToPolicy(new PolicyStatement({
319- // actions: ["s3:GetObject", "s3:ListBucket"],
320- // resources: [kbDocsBucket.bucketArn, `${kbDocsBucket.bucketArn}/*`]
321- // }))
237+ // Allow Bedrock full access to your OpenSearch Serverless collection and its indexes
238+ // For production, consider narrowing to only what you need
239+ bedrockKbRole . addToPolicy ( new PolicyStatement ( {
240+ actions : [ "aoss:*" ] ,
241+ resources : [
242+ osCollection . attrArn , // Collection itself
243+ `${ osCollection . attrArn } /*` , // All child resources (indexes)
244+ "*" // For initial development, broad access
245+ ]
246+ } ) )
247+
248+ // Allow Bedrock to call the embedding model
249+ bedrockKbRole . addToPolicy ( new PolicyStatement ( {
250+ actions : [ "bedrock:InvokeModel" ] ,
251+ resources : [
252+ `arn:aws:bedrock:${ region } ::foundation-model/amazon.titan-embed-text-v2:0`
253+ ]
254+ } ) )
322255
323- // Use existing Bedrock role that already has trust relationship with Bedrock service
324- // Make sure the Knowledge Base depends on the vector index creation
256+ // ==== Bedrock Knowledge Base Resource ====
257+ // Reference the execution role created above
325258 const kb = new CfnKnowledgeBase ( this , "EpsKb" , {
326259 name : "eps-assist-kb" ,
327260 description : "EPS Assist Knowledge Base" ,
328- // roleArn: bedrockKbRole.roleArn,
329- roleArn : "arn:aws:iam::591291862413:role/AmazonBedrockKnowledgebas-BedrockExecutionRole9C52C-3tluDlUTJ2DW" ,
261+ roleArn : bedrockKbRole . roleArn ,
330262 knowledgeBaseConfiguration : {
331263 type : "VECTOR" ,
332264 vectorKnowledgeBaseConfiguration : {
@@ -346,11 +278,10 @@ export class EpsAssistMeStack extends Stack {
346278 }
347279 }
348280 } )
349- // Ensure the Knowledge Base is created after the vector index
350- kb . node . addDependency ( vectorIndex )
281+ kb . node . addDependency ( vectorIndex ) // Ensure index exists before KB
351282
352- // Attach S3 data source to Knowledge Base
353- new CfnDataSource ( this , "EpsKbDataSource" , {
283+ // ==== S3 DataSource for Knowledge Base ====
284+ const kbDataSource = new CfnDataSource ( this , "EpsKbDataSource" , {
354285 name : "eps-assist-kb-ds" ,
355286 knowledgeBaseId : kb . attrKnowledgeBaseId ,
356287 dataSourceConfiguration : {
@@ -360,6 +291,7 @@ export class EpsAssistMeStack extends Stack {
360291 }
361292 }
362293 } )
294+ kbDataSource . node . addDependency ( kb )
363295
364296 // ==== SlackBot Lambda ====
365297 const lambdaEnv : { [ key : string ] : string } = {
@@ -377,8 +309,6 @@ export class EpsAssistMeStack extends Stack {
377309 SLACK_BOT_TOKEN : slackBotToken ,
378310 SLACK_SIGNING_SECRET : slackSigningSecret
379311 }
380-
381- // SlackBot Lambda function
382312 const slackBotLambda = new LambdaFunction ( this , "SlackBotLambda" , {
383313 stackName : props . stackName ,
384314 functionName : `${ props . stackName } -SlackBotFunction` ,
@@ -390,24 +320,22 @@ export class EpsAssistMeStack extends Stack {
390320 additionalPolicies : [ ]
391321 } )
392322
393- // ==== API Gateway + Slack Route ====
323+ // ==== API Gateway & Slack Route ====
394324 const apiGateway = new RestApiGateway ( this , "EpsAssistApiGateway" , {
395325 stackName : props . stackName ,
396326 logRetentionInDays,
397327 enableMutualTls : false ,
398328 trustStoreKey : "unused" ,
399329 truststoreVersion : "unused"
400330 } )
401-
402- // API Route
331+ // Add SlackBot Lambda to API Gateway
403332 const slackRoute = apiGateway . api . root . addResource ( "slack" ) . addResource ( "ask-eps" )
404333 slackRoute . addMethod ( "POST" , new LambdaIntegration ( slackBotLambda . function , {
405334 credentialsRole : apiGateway . role
406335 } ) )
407-
408336 apiGateway . role . addManagedPolicy ( slackBotLambda . executionPolicy )
409337
410- // Output the SlackBot API endpoint
338+ // ==== Output: SlackBot Endpoint ====
411339 new CfnOutput ( this , "SlackBotEndpoint" , {
412340 value : `https://${ apiGateway . api . domainName ?. domainName } /slack/ask-eps`
413341 } )
0 commit comments