Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ jobs:
PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE }}
DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }}
REGRESSION_TESTS_PEM: ${{ secrets.REGRESSION_TESTS_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.SLACK_SIGNING_SECRET }}
SLACK_BOT_TOKEN: ${{ secrets.DEV_SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.DEV_SLACK_SIGNING_SECRET }}

release_qa:
needs: [tag_release, package_code, release_dev]
Expand Down Expand Up @@ -109,5 +109,5 @@ jobs:
PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE }}
DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }}
REGRESSION_TESTS_PEM: ${{ secrets.REGRESSION_TESTS_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.SLACK_SIGNING_SECRET }}
SLACK_BOT_TOKEN: ${{ secrets.QA_SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.QA_SLACK_SIGNING_SECRET }}
4 changes: 2 additions & 2 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,5 +104,5 @@ jobs:
PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE }}
DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }}
REGRESSION_TESTS_PEM: ${{ secrets.REGRESSION_TESTS_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.SLACK_SIGNING_SECRET }}
SLACK_BOT_TOKEN: ${{ secrets.DEV_SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.DEV_SLACK_SIGNING_SECRET }}
16 changes: 8 additions & 8 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ jobs:
PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE }}
DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }}
REGRESSION_TESTS_PEM: ${{ secrets.REGRESSION_TESTS_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.SLACK_SIGNING_SECRET }}
SLACK_BOT_TOKEN: ${{ secrets.DEV_SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.DEV_SLACK_SIGNING_SECRET }}

release_qa:
needs: [tag_release, package_code, release_dev]
Expand Down Expand Up @@ -113,8 +113,8 @@ jobs:
PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE }}
DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }}
REGRESSION_TESTS_PEM: ${{ secrets.REGRESSION_TESTS_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.SLACK_SIGNING_SECRET }}
SLACK_BOT_TOKEN: ${{ secrets.QA_SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.QA_SLACK_SIGNING_SECRET }}

release_int:
needs: [tag_release, package_code, release_qa]
Expand Down Expand Up @@ -142,8 +142,8 @@ jobs:
PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE }}
DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }}
REGRESSION_TESTS_PEM: ${{ secrets.REGRESSION_TESTS_PEM }}
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.SLACK_SIGNING_SECRET }}
SLACK_BOT_TOKEN: ${{ secrets.INT_SLACK_BOT_TOKEN }}
SLACK_SIGNING_SECRET: ${{ secrets.INT_SLACK_SIGNING_SECRET }}

# TODO: uncomment when ready to release to prod
# release_prod:
Expand Down Expand Up @@ -171,8 +171,8 @@ jobs:
# PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE: ${{ secrets.PROD_CLOUD_FORMATION_CHECK_VERSION_ROLE }}
# DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE: ${{ secrets.DEV_CLOUD_FORMATION_EXECUTE_LAMBDA_ROLE }}
# REGRESSION_TESTS_PEM: ${{ secrets.REGRESSION_TESTS_PEM }}
# SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
# SLACK_SIGNING_SECRET: ${{ secrets.SLACK_SIGNING_SECRET }}
# SLACK_BOT_TOKEN: ${{ secrets.PROD_SLACK_BOT_TOKEN }}
# SLACK_SIGNING_SECRET: ${{ secrets.PROD_SLACK_SIGNING_SECRET }}
#
# create_release_notes:
# needs: [tag_release, package_code, get_commit_id, release_int, release_prod]
Expand Down
98 changes: 98 additions & 0 deletions packages/cdk/constructs/DelayResource.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {Construct} from "constructs"
import {Duration, CustomResource} from "aws-cdk-lib"
import {Function, Runtime, Code} from "aws-cdk-lib/aws-lambda"
import {Role, ServicePrincipal, ManagedPolicy} from "aws-cdk-lib/aws-iam"
import {Provider} from "aws-cdk-lib/custom-resources"

export interface DelayResourceProps {
/**
* The delay time in seconds (default: 30 seconds)
*/
readonly delaySeconds?: number

/**
* Optional description for the delay resource
*/
readonly description?: string
}

/**
* a fix for an annoying time sync issue that adds a configurable delay
* to ensure AWS resources are fully available before dependent resources are created
*/
export class DelayResource extends Construct {
public readonly customResource: CustomResource
public readonly delaySeconds: number

constructor(scope: Construct, id: string, props: DelayResourceProps = {}) {
super(scope, id)

this.delaySeconds = props.delaySeconds || 30

// create IAM role for the delay Lambda function
const lambdaExecutionRole = new Role(this, "LambdaExecutionRole", {
assumedBy: new ServicePrincipal("lambda.amazonaws.com"),
description: "Execution role for delay custom resource Lambda function",
managedPolicies: [
ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")
]
})

// create the delay Lambda function with inline Python code
const delayFunction = new Function(this, "DelayFunction", {
runtime: Runtime.PYTHON_3_12,
handler: "index.handler",
role: lambdaExecutionRole,
timeout: Duration.minutes(15), // max Lambda timeout to handle long delays
description: props.description || `Delay resource for ${this.delaySeconds} seconds`,
code: Code.fromInline(`
from time import sleep
import json
import cfnresponse
import uuid

def handler(event, context):
wait_seconds = 0
id = str(uuid.uuid1())

print(f"Received event: {json.dumps(event, default=str)}")

try:
if event["RequestType"] in ["Create", "Update"]:
wait_seconds = int(event["ResourceProperties"].get("WaitSeconds", 0))
print(f"Waiting for {wait_seconds} seconds...")
sleep(wait_seconds)
print(f"Wait complete")

response = {
"TimeWaited": wait_seconds,
"Id": id,
"Status": "SUCCESS"
}

cfnresponse.send(event, context, cfnresponse.SUCCESS, response, f"Waiter-{id}")

except Exception as e:
print(f"Error: {str(e)}")
cfnresponse.send(event, context, cfnresponse.FAILED, {"Error": str(e)}, f"Waiter-{id}")
`)
})

// create the custom resource provider
const provider = new Provider(this, "DelayProvider", {
onEventHandler: delayFunction
})

// create the custom resource that triggers the delay
this.customResource = new CustomResource(this, "DelayCustomResource", {
serviceToken: provider.serviceToken,
properties: {
WaitSeconds: this.delaySeconds,
Description: props.description || `Delay for ${this.delaySeconds} seconds`,
// timestamp to ensure updates trigger when properties change
Timestamp: Date.now()
}
})
}

}
95 changes: 94 additions & 1 deletion packages/cdk/nagSuppressions.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-len */

import {Stack} from "aws-cdk-lib"
import {NagPackSuppression, NagSuppressions} from "cdk-nag"
Expand Down Expand Up @@ -160,7 +161,8 @@ export const nagSuppressions = (stack: Stack) => {
})

const logRetentionHandlers = stack.node.findAll().filter(node =>
node.node.id.startsWith("LogRetention")
node.node.id.startsWith("LogRetention") &&
!node.node.path.includes("DelayProvider")
)

logRetentionHandlers.forEach(handler => {
Expand All @@ -187,6 +189,97 @@ export const nagSuppressions = (stack: Stack) => {
)
})

// Suppress DelayResource IAM and runtime issues
safeAddNagSuppression(
stack,
"/EpsAssistMeStack/VectorIndex/PolicySyncWait/LambdaExecutionRole/Resource",
[
{
id: "AwsSolutions-IAM4",
reason: "DelayResource Lambda uses AWS managed policy for basic Lambda execution role."
}
]
)

safeAddNagSuppression(
stack,
"/EpsAssistMeStack/VectorIndex/IndexReadyWait/LambdaExecutionRole/Resource",
[
{
id: "AwsSolutions-IAM4",
reason: "DelayResource Lambda uses AWS managed policy for basic Lambda execution role."
}
]
)

// Suppress DelayProvider framework ServiceRole issues
safeAddNagSuppression(
stack,
"/EpsAssistMeStack/VectorIndex/PolicySyncWait/DelayProvider/framework-onEvent/ServiceRole/Resource",
[
{
id: "AwsSolutions-IAM4",
reason: "Auto-generated CDK Provider role uses AWS managed policy for Lambda execution."
}
]
)

safeAddNagSuppression(
stack,
"/EpsAssistMeStack/VectorIndex/PolicySyncWait/DelayProvider/framework-onEvent/ServiceRole/DefaultPolicy/Resource",
[
{
id: "AwsSolutions-IAM5",
reason: "Auto-generated CDK Provider role requires wildcard permissions for Lambda invocation."
}
]
)

safeAddNagSuppression(
stack,
"/EpsAssistMeStack/VectorIndex/IndexReadyWait/DelayProvider/framework-onEvent/ServiceRole/Resource",
[
{
id: "AwsSolutions-IAM4",
reason: "Auto-generated CDK Provider role uses AWS managed policy for Lambda execution."
}
]
)

safeAddNagSuppression(
stack,
"/EpsAssistMeStack/VectorIndex/IndexReadyWait/DelayProvider/framework-onEvent/ServiceRole/DefaultPolicy/Resource",
[
{
id: "AwsSolutions-IAM5",
reason: "Auto-generated CDK Provider role requires wildcard permissions for Lambda invocation."
}
]
)

// Suppress DelayFunction runtime version warnings
safeAddNagSuppression(
stack,
"/EpsAssistMeStack/VectorIndex/PolicySyncWait/DelayFunction/Resource",
[
{
id: "AwsSolutions-L1",
reason: "DelayResource uses Python 3.12 which is the latest stable runtime available for the delay functionality."
}
]
)

safeAddNagSuppression(
stack,
"/EpsAssistMeStack/VectorIndex/IndexReadyWait/DelayFunction/Resource",
[
{
id: "AwsSolutions-L1",
reason: "DelayResource uses Python 3.12 which is the latest stable runtime available for the delay functionality."
}
]
)

}

const safeAddNagSuppression = (stack: Stack, path: string, suppressions: Array<NagPackSuppression>) => {
Expand Down
30 changes: 28 additions & 2 deletions packages/cdk/resources/VectorIndex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {Construct} from "constructs"
import {CfnIndex} from "aws-cdk-lib/aws-opensearchserverless"
import {VectorCollection} from "@cdklabs/generative-ai-cdk-constructs/lib/cdk-lib/opensearchserverless"
import {RemovalPolicy} from "aws-cdk-lib"
import {DelayResource} from "../constructs/DelayResource"

export interface VectorIndexProps {
readonly stackName: string
Expand All @@ -11,6 +12,8 @@ export interface VectorIndexProps {
export class VectorIndex extends Construct {
public readonly cfnIndex: CfnIndex
public readonly indexName: string
public readonly policySyncWait: DelayResource
public readonly indexReadyWait: DelayResource

constructor(scope: Construct, id: string, props: VectorIndexProps) {
super(scope, id)
Expand Down Expand Up @@ -64,8 +67,31 @@ export class VectorIndex extends Construct {
}
})

this.cfnIndex = cfnIndex
// a fix for an annoying time sync issue that adds a small delay
// to ensure data access policies are synced before index creation
const policySyncWait = new DelayResource(this, "PolicySyncWait", {
delaySeconds: 15,
description: "Wait for OpenSearch data access policies to sync"
})

policySyncWait.customResource.node.addDependency(props.collection.dataAccessPolicy)

// Index depends on policy sync wait instead of directly on dataAccessPolicy
cfnIndex.node.addDependency(policySyncWait.customResource)

cfnIndex.applyRemovalPolicy(RemovalPolicy.DESTROY)

this.cfnIndex.applyRemovalPolicy(RemovalPolicy.DESTROY)
// a fix for an annoying time sync issue that adds a small delay
// to ensure index is actually available for Bedrock
const indexReadyWait = new DelayResource(this, "IndexReadyWait", {
delaySeconds: 30,
description: "Wait for OpenSearch index to be fully available"
})

indexReadyWait.customResource.node.addDependency(cfnIndex)

this.cfnIndex = cfnIndex
this.policySyncWait = policySyncWait
this.indexReadyWait = indexReadyWait
}
}
2 changes: 1 addition & 1 deletion packages/cdk/stacks/EpsAssistMeStack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class EpsAssistMeStack extends Stack {
account
})

vectorKB.knowledgeBase.node.addDependency(vectorIndex.cfnIndex)
vectorKB.knowledgeBase.node.addDependency(vectorIndex.indexReadyWait.customResource)

// Create runtime policies with resource dependencies
const runtimePolicies = new RuntimePolicies(this, "RuntimePolicies", {
Expand Down