Skip to content

Commit c821ca4

Browse files
committed
Create a role for Bedrock KB with permissions it needs
1 parent 8db137d commit c821ca4

File tree

2 files changed

+75
-147
lines changed

2 files changed

+75
-147
lines changed

packages/cdk/stacks/EpsAssistMeStack.ts

Lines changed: 74 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -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"
1615
import {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"
1917
import {
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
})

packages/createIndexFunction/app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ def wait_for_index(opensearch_client, index_name, timeout=120, poll_interval=5):
4949
else:
5050
logger.info(f"Index '{index_name}' does not exist yet...")
5151
except Exception as exc:
52-
logger.info(f"Error checking index status: {exc}")
52+
logger.warning(f"Error checking index status: {exc}")
5353

5454
if time.time() - start > timeout:
5555
logger.error(f"Timed out waiting for index '{index_name}' to become ready.")

0 commit comments

Comments
 (0)