Skip to content
57 changes: 57 additions & 0 deletions packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ if (process.env.PACKAGE_LAYOUT_VERSION === '1') {
aws_ecr_assets: docker,
aws_appsync: appsync,
aws_bedrockagentcore: bedrockagentcore,
aws_events: events,
aws_dynamodb: dynamodb,
aws_bedrock: bedrock,
Stack
} = require('aws-cdk-lib');
}
Expand Down Expand Up @@ -711,6 +714,59 @@ class AgentCoreHotswapStack extends cdk.Stack {
}
}

class CloudControlHotswapStack extends cdk.Stack {
constructor(parent, id, props) {
super(parent, id, props);

// DynamoDB table — a dependency that other resources reference
const table = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'pk', type: dynamodb.AttributeType.STRING },
removalPolicy: cdk.RemovalPolicy.DESTROY,
});

// ElastiCache serverless cache — another dependency resource
const cache = new cdk.CfnResource(this, 'Cache', {
type: 'AWS::ElastiCache::ServerlessCache',
properties: {
Engine: 'valkey',
ServerlessCacheName: `${cdk.Stack.of(this).stackName}-cache`.substring(0, 40).toLowerCase(),
},
});

// SQS Queue — hotswapped via CCAPI, references the DynamoDB table ARN in its tag
const queue = new sqs.Queue(this, 'Queue', {
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
cdk.Tags.of(queue).add('DynamoTableArn', table.tableArn);
cdk.Tags.of(queue).add('DynamicTag', process.env.DYNAMIC_CC_PROPERTY_VALUE ?? 'original');

// Bedrock Agent — hotswapped via CCAPI, references the DynamoDB table name
const agentRole = new iam.Role(this, 'AgentRole', {
assumedBy: new iam.ServicePrincipal('bedrock.amazonaws.com'),
});
const agent = new bedrock.CfnAgent(this, 'Agent', {
agentName: `${cdk.Stack.of(this).stackName}-agent`.substring(0, 40),
agentResourceRoleArn: agentRole.roleArn,
instruction: process.env.DYNAMIC_CC_PROPERTY_VALUE
? `You help query the table ${table.tableName}. ${process.env.DYNAMIC_CC_PROPERTY_VALUE}`
: `You help query the table ${table.tableName}. original`,
foundationModel: 'anthropic.claude-instant-v1',
});

// Events Rule — hotswapped via CCAPI, references the ElastiCache cache ARN
const rule = new events.Rule(this, 'Rule', {
schedule: events.Schedule.rate(cdk.Duration.hours(1)),
description: process.env.DYNAMIC_CC_PROPERTY_VALUE
? `Rule for cache ${cache.getAtt('ARN')}. ${process.env.DYNAMIC_CC_PROPERTY_VALUE}`
: `Rule for cache ${cache.getAtt('ARN')}. original`,
});

new cdk.CfnOutput(this, 'QueueUrl', { value: queue.queueUrl });
new cdk.CfnOutput(this, 'AgentName', { value: agent.ref });
new cdk.CfnOutput(this, 'RuleName', { value: rule.ruleName });
}
}

class DockerStack extends cdk.Stack {
constructor(parent, id, props) {
super(parent, id, props);
Expand Down Expand Up @@ -990,6 +1046,7 @@ switch (stackSet) {
new LambdaHotswapStack(app, `${stackPrefix}-lambda-hotswap`);
new EcsHotswapStack(app, `${stackPrefix}-ecs-hotswap`);
new AgentCoreHotswapStack(app, `${stackPrefix}-agentcore-hotswap`);
new CloudControlHotswapStack(app, `${stackPrefix}-cc-hotswap`);
new AppSyncHotswapStack(app, `${stackPrefix}-appsync-hotswap`);
new DockerStack(app, `${stackPrefix}-docker`);
new DockerInUseStack(app, `${stackPrefix}-docker-in-use`);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { DescribeStacksCommand } from '@aws-sdk/client-cloudformation';
import { integTest, withDefaultFixture } from '../../../lib';

jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime

integTest(
'hotswap deployment supports CloudControl-based resources with attribute resolution',
withDefaultFixture(async (fixture) => {
// GIVEN
const stackName = 'cc-hotswap';
await fixture.cdkDeploy(stackName, {
captureStderr: false,
modEnv: {
DYNAMIC_CC_PROPERTY_VALUE: 'original value',
},
});

// WHEN
const deployOutput = await fixture.cdkDeploy(stackName, {
options: ['--hotswap'],
captureStderr: true,
onlyStderr: true,
modEnv: {
DYNAMIC_CC_PROPERTY_VALUE: 'new value',
},
});

const response = await fixture.aws.cloudFormation.send(
new DescribeStacksCommand({
StackName: fixture.fullStackName(stackName),
}),
);

const queueUrl = response.Stacks?.[0].Outputs?.find((output) => output.OutputKey === 'QueueUrl')?.OutputValue;
const agentName = response.Stacks?.[0].Outputs?.find((output) => output.OutputKey === 'AgentName')?.OutputValue;
const ruleName = response.Stacks?.[0].Outputs?.find((output) => output.OutputKey === 'RuleName')?.OutputValue;

// THEN

// The deployment should not trigger a full deployment, thus the stack's status must remain
// "CREATE_COMPLETE"
expect(response.Stacks?.[0].StackStatus).toEqual('CREATE_COMPLETE');
// Verify hotswap was used
expect(deployOutput).toMatch(/hotswapped!/);
// Verify all three CCAPI-based resources were hotswapped
expect(queueUrl).toBeDefined();
expect(agentName).toBeDefined();
expect(ruleName).toBeDefined();
}),
);
24 changes: 19 additions & 5 deletions packages/@aws-cdk/toolkit-lib/lib/api/aws-auth/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ import type {
GetResourceCommandOutput,
ListResourcesCommandInput,
ListResourcesCommandOutput,
UpdateResourceCommandInput,
UpdateResourceCommandOutput,
} from '@aws-sdk/client-cloudcontrol';
import {
UpdateResourceCommand,

CloudControlClient,
GetResourceCommand,
ListResourcesCommand,
Expand Down Expand Up @@ -115,6 +119,8 @@ import type {
ExecuteStackRefactorCommandOutput,
DescribeEventsCommandOutput,
DescribeEventsCommandInput,
DescribeTypeCommandInput,
DescribeTypeCommandOutput,
GetHookResultCommandInput,
GetHookResultCommandOutput,
} from '@aws-sdk/client-cloudformation';
Expand Down Expand Up @@ -159,6 +165,7 @@ import {
DetectStackResourceDriftCommand,
waitUntilStackRefactorCreateComplete,
waitUntilStackRefactorExecuteComplete,
DescribeTypeCommand,
GetHookResultCommand,
} from '@aws-sdk/client-cloudformation';
import type { OperationEvent } from '@aws-sdk/client-cloudformation/dist-types/models/models_0';
Expand Down Expand Up @@ -450,6 +457,7 @@ export interface IBedrockAgentCoreControlClient {
export interface ICloudControlClient {
listResources(input: ListResourcesCommandInput): Promise<ListResourcesCommandOutput>;
getResource(input: GetResourceCommandInput): Promise<GetResourceCommandOutput>;
updateResource(input: UpdateResourceCommandInput): Promise<UpdateResourceCommandOutput>;
}

export interface ICloudFormationClient {
Expand All @@ -472,6 +480,7 @@ export interface ICloudFormationClient {
describeStackResources(input: DescribeStackResourcesCommandInput): Promise<DescribeStackResourcesCommandOutput>;
detectStackDrift(input: DetectStackDriftCommandInput): Promise<DetectStackDriftCommandOutput>;
detectStackResourceDrift(input: DetectStackResourceDriftCommandInput): Promise<DetectStackResourceDriftCommandOutput>;
describeType(input: DescribeTypeCommandInput): Promise<DescribeTypeCommandOutput>;
executeChangeSet(input: ExecuteChangeSetCommandInput): Promise<ExecuteChangeSetCommandOutput>;
getGeneratedTemplate(input: GetGeneratedTemplateCommandInput): Promise<GetGeneratedTemplateCommandOutput>;
getTemplate(input: GetTemplateCommandInput): Promise<GetTemplateCommandOutput>;
Expand Down Expand Up @@ -578,7 +587,7 @@ export interface ILambdaClient {
input: UpdateFunctionConfigurationCommandInput,
): Promise<UpdateFunctionConfigurationCommandOutput>;
// Waiters
waitUntilFunctionUpdated(delaySeconds: number, input: UpdateFunctionConfigurationCommandInput): Promise<WaiterResult>;
waitUntilFunctionUpdated(minDelaySeconds: number, maxDelaySeconds: number, input: UpdateFunctionConfigurationCommandInput): Promise<WaiterResult>;
}

export interface IRoute53Client {
Expand Down Expand Up @@ -719,6 +728,8 @@ export class SDK {
client.send(new ListResourcesCommand(input)),
getResource: (input: GetResourceCommandInput): Promise<GetResourceCommandOutput> =>
client.send(new GetResourceCommand(input)),
updateResource: (input: UpdateResourceCommandInput): Promise<UpdateResourceCommandOutput> =>
client.send(new UpdateResourceCommand(input)),
};
}

Expand Down Expand Up @@ -766,6 +777,8 @@ export class SDK {
client.send(new DescribeStacksCommand(input)),
describeStackResources: (input: DescribeStackResourcesCommandInput): Promise<DescribeStackResourcesCommandOutput> =>
client.send(new DescribeStackResourcesCommand(input)),
describeType: (input: DescribeTypeCommandInput): Promise<DescribeTypeCommandOutput> =>
client.send(new DescribeTypeCommand(input)),
executeChangeSet: (input: ExecuteChangeSetCommandInput): Promise<ExecuteChangeSetCommandOutput> =>
client.send(new ExecuteChangeSetCommand(input)),
getGeneratedTemplate: (input: GetGeneratedTemplateCommandInput): Promise<GetGeneratedTemplateCommandOutput> =>
Expand Down Expand Up @@ -1020,15 +1033,16 @@ export class SDK {
client.send(new UpdateFunctionConfigurationCommand(input)),
// Waiters
waitUntilFunctionUpdated: (
delaySeconds: number,
minDelaySeconds: number,
maxDelaySeconds: number,
input: UpdateFunctionConfigurationCommandInput,
): Promise<WaiterResult> => {
return waitUntilFunctionUpdatedV2(
{
client,
maxDelay: delaySeconds,
minDelay: delaySeconds,
maxWaitTime: delaySeconds * 60,
maxDelay: maxDelaySeconds,
minDelay: minDelaySeconds,
maxWaitTime: maxDelaySeconds * 60,
},
input,
);
Expand Down
Loading
Loading