Skip to content

Commit c85a454

Browse files
authored
Merge branch 'main' into event_rule_missing_kms_permissions
2 parents 9729dda + 2e691b6 commit c85a454

13 files changed

Lines changed: 273 additions & 21 deletions

File tree

packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2301,6 +2301,58 @@ integTest('hotswap deployment supports AppSync APIs with many functions',
23012301
}),
23022302
);
23032303

2304+
integTest('hotswap ECS deployment respects properties override', withDefaultFixture(async (fixture) => {
2305+
// Update the CDK context with the new ECS properties
2306+
let ecsMinimumHealthyPercent = 100;
2307+
let ecsMaximumHealthyPercent = 200;
2308+
let cdkJson = JSON.parse(await fs.readFile(path.join(fixture.integTestDir, 'cdk.json'), 'utf8'));
2309+
cdkJson = {
2310+
...cdkJson,
2311+
hotswap: {
2312+
ecs: {
2313+
minimumHealthyPercent: ecsMinimumHealthyPercent,
2314+
maximumHealthyPercent: ecsMaximumHealthyPercent,
2315+
},
2316+
},
2317+
};
2318+
2319+
await fs.writeFile(path.join(fixture.integTestDir, 'cdk.json'), JSON.stringify(cdkJson));
2320+
2321+
// GIVEN
2322+
const stackArn = await fixture.cdkDeploy('ecs-hotswap', {
2323+
captureStderr: false,
2324+
});
2325+
2326+
// WHEN
2327+
await fixture.cdkDeploy('ecs-hotswap', {
2328+
options: [
2329+
'--hotswap',
2330+
],
2331+
modEnv: {
2332+
DYNAMIC_ECS_PROPERTY_VALUE: 'new value',
2333+
},
2334+
});
2335+
2336+
const describeStacksResponse = await fixture.aws.cloudFormation.send(
2337+
new DescribeStacksCommand({
2338+
StackName: stackArn,
2339+
}),
2340+
);
2341+
2342+
const clusterName = describeStacksResponse.Stacks?.[0].Outputs?.find(output => output.OutputKey == 'ClusterName')?.OutputValue!;
2343+
const serviceName = describeStacksResponse.Stacks?.[0].Outputs?.find(output => output.OutputKey == 'ServiceName')?.OutputValue!;
2344+
2345+
// THEN
2346+
const describeServicesResponse = await fixture.aws.ecs.send(
2347+
new DescribeServicesCommand({
2348+
cluster: clusterName,
2349+
services: [serviceName],
2350+
}),
2351+
);
2352+
expect(describeServicesResponse.services?.[0].deploymentConfiguration?.minimumHealthyPercent).toEqual(ecsMinimumHealthyPercent);
2353+
expect(describeServicesResponse.services?.[0].deploymentConfiguration?.maximumPercent).toEqual(ecsMaximumHealthyPercent);
2354+
}));
2355+
23042356
async function listChildren(parent: string, pred: (x: string) => Promise<boolean>) {
23052357
const ret = new Array<string>();
23062358
for (const child of await fs.readdir(parent, { encoding: 'utf-8' })) {

packages/@aws-cdk/aws-kinesisfirehose-alpha/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ your delivery streams via logs and metrics.
181181

182182
Amazon Data Firehose will send logs to CloudWatch when data transformation or data
183183
delivery fails. The CDK will enable logging by default and create a CloudWatch LogGroup
184-
and LogStream for your Delivery Stream.
184+
and LogStream with default settings for your Delivery Stream.
185185

186186
When creating a destination, you can provide an `ILoggingConfig`, which can either be an `EnableLogging` or `DisableLogging` instance.
187187
If you use `EnableLogging`, the CDK will create a CloudWatch LogGroup and LogStream with all CloudFormation default settings for you, or you can optionally

packages/aws-cdk/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -451,6 +451,19 @@ Hotswapping is currently supported for the following changes
451451
- VTL mapping template changes for AppSync Resolvers and Functions.
452452
- Schema changes for AppSync GraphQL Apis.
453453

454+
You can optionally configure the behavior of your hotswap deployments in `cdk.json`. Currently you can only configure ECS hotswap behavior:
455+
456+
```json
457+
{
458+
"hotswap": {
459+
"ecs": {
460+
"minimumHealthyPercent": 100,
461+
"maximumHealthyPercent": 250
462+
}
463+
}
464+
}
465+
```
466+
454467
**⚠ Note #1**: This command deliberately introduces drift in CloudFormation stacks in order to speed up deployments.
455468
For this reason, only use it for development purposes.
456469
**Never use this flag for your production deployments**!

packages/aws-cdk/lib/api/deploy-stack.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import * as uuid from 'uuid';
55
import { ISDK, SdkProvider } from './aws-auth';
66
import { EnvironmentResources } from './environment-resources';
77
import { CfnEvaluationException } from './evaluate-cloudformation-template';
8-
import { HotswapMode, ICON } from './hotswap/common';
8+
import { HotswapMode, HotswapPropertyOverrides, ICON } from './hotswap/common';
99
import { tryHotswapDeployment } from './hotswap-deployments';
1010
import { addMetadataAssetsToManifest } from '../assets';
1111
import { Tag } from '../cdk-toolkit';
@@ -173,6 +173,11 @@ export interface DeployStackOptions {
173173
*/
174174
readonly hotswap?: HotswapMode;
175175

176+
/**
177+
* Extra properties that configure hotswap behavior
178+
*/
179+
readonly hotswapPropertyOverrides?: HotswapPropertyOverrides;
180+
176181
/**
177182
* The extra string to append to the User-Agent header when performing AWS SDK calls.
178183
*
@@ -264,6 +269,7 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
264269
: templateParams.supplyAll(finalParameterValues);
265270

266271
const hotswapMode = options.hotswap ?? HotswapMode.FULL_DEPLOYMENT;
272+
const hotswapPropertyOverrides = options.hotswapPropertyOverrides ?? new HotswapPropertyOverrides();
267273

268274
if (await canSkipDeploy(options, cloudFormationStack, stackParams.hasChanges(cloudFormationStack.parameters))) {
269275
debug(`${deployName}: skipping deployment (use --force to override)`);
@@ -303,7 +309,7 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
303309
// attempt to short-circuit the deployment if possible
304310
try {
305311
const hotswapDeploymentResult = await tryHotswapDeployment(
306-
options.sdkProvider, stackParams.values, cloudFormationStack, stackArtifact, hotswapMode,
312+
options.sdkProvider, stackParams.values, cloudFormationStack, stackArtifact, hotswapMode, hotswapPropertyOverrides,
307313
);
308314
if (hotswapDeploymentResult) {
309315
return hotswapDeploymentResult;

packages/aws-cdk/lib/api/deployments.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { ISDK } from './aws-auth/sdk';
1010
import { CredentialsOptions, SdkForEnvironment, SdkProvider } from './aws-auth/sdk-provider';
1111
import { deployStack, DeployStackResult, destroyStack, DeploymentMethod } from './deploy-stack';
1212
import { EnvironmentResources, EnvironmentResourcesRegistry } from './environment-resources';
13-
import { HotswapMode } from './hotswap/common';
13+
import { HotswapMode, HotswapPropertyOverrides } from './hotswap/common';
1414
import { loadCurrentTemplateWithNestedStacks, loadCurrentTemplate, RootTemplateWithNestedStacks } from './nested-stack-helpers';
1515
import { determineAllowCrossAccountAssetPublishing } from './util/checks';
1616
import { CloudFormationStack, Template, ResourcesToImport, ResourceIdentifierSummaries, stabilizeStack, uploadStackTemplateAssets } from './util/cloudformation';
@@ -182,6 +182,11 @@ export interface DeployStackOptions {
182182
*/
183183
readonly hotswap?: HotswapMode;
184184

185+
/**
186+
* Properties that configure hotswap behavior
187+
*/
188+
readonly hotswapPropertyOverrides?: HotswapPropertyOverrides;
189+
185190
/**
186191
* The extra string to append to the User-Agent header when performing AWS SDK calls.
187192
*
@@ -498,6 +503,7 @@ export class Deployments {
498503
ci: options.ci,
499504
rollback: options.rollback,
500505
hotswap: options.hotswap,
506+
hotswapPropertyOverrides: options.hotswapPropertyOverrides,
501507
extraUserAgent: options.extraUserAgent,
502508
resourcesToImport: options.resourcesToImport,
503509
overrideTemplate: options.overrideTemplate,

packages/aws-cdk/lib/api/hotswap-deployments.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-templa
77
import { print } from '../logging';
88
import { isHotswappableAppSyncChange } from './hotswap/appsync-mapping-templates';
99
import { isHotswappableCodeBuildProjectChange } from './hotswap/code-build-projects';
10-
import { ICON, ChangeHotswapResult, HotswapMode, HotswappableChange, NonHotswappableChange, HotswappableChangeCandidate, ClassifiedResourceChanges, reportNonHotswappableChange, reportNonHotswappableResource } from './hotswap/common';
10+
import { ICON, ChangeHotswapResult, HotswapMode, HotswappableChange, NonHotswappableChange, HotswappableChangeCandidate, HotswapPropertyOverrides, ClassifiedResourceChanges, reportNonHotswappableChange, reportNonHotswappableResource } from './hotswap/common';
1111
import { isHotswappableEcsServiceChange } from './hotswap/ecs-services';
1212
import { isHotswappableLambdaFunctionChange } from './hotswap/lambda-functions';
1313
import { skipChangeForS3DeployCustomResourcePolicy, isHotswappableS3BucketDeploymentChange } from './hotswap/s3-bucket-deployments';
@@ -20,7 +20,10 @@ import { CloudFormationStack } from './util/cloudformation';
2020
const pLimit: typeof import('p-limit') = require('p-limit');
2121

2222
type HotswapDetector = (
23-
logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate
23+
logicalId: string,
24+
change: HotswappableChangeCandidate,
25+
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
26+
hotswapPropertyOverrides: HotswapPropertyOverrides,
2427
) => Promise<ChangeHotswapResult>;
2528

2629
const RESOURCE_DETECTORS: { [key: string]: HotswapDetector } = {
@@ -62,7 +65,7 @@ const RESOURCE_DETECTORS: { [key: string]: HotswapDetector } = {
6265
export async function tryHotswapDeployment(
6366
sdkProvider: SdkProvider, assetParams: { [key: string]: string },
6467
cloudFormationStack: CloudFormationStack, stackArtifact: cxapi.CloudFormationStackArtifact,
65-
hotswapMode: HotswapMode,
68+
hotswapMode: HotswapMode, hotswapPropertyOverrides: HotswapPropertyOverrides,
6669
): Promise<DeployStackResult | undefined> {
6770
// resolve the environment, so we can substitute things like AWS::Region in CFN expressions
6871
const resolvedEnv = await sdkProvider.resolveEnvironment(stackArtifact.environment);
@@ -86,7 +89,7 @@ export async function tryHotswapDeployment(
8689

8790
const stackChanges = cfn_diff.fullDiff(currentTemplate.deployedRootTemplate, stackArtifact.template);
8891
const { hotswappableChanges, nonHotswappableChanges } = await classifyResourceChanges(
89-
stackChanges, evaluateCfnTemplate, sdk, currentTemplate.nestedStacks,
92+
stackChanges, evaluateCfnTemplate, sdk, currentTemplate.nestedStacks, hotswapPropertyOverrides,
9093
);
9194

9295
logNonHotswappableChanges(nonHotswappableChanges, hotswapMode);
@@ -113,6 +116,7 @@ async function classifyResourceChanges(
113116
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
114117
sdk: ISDK,
115118
nestedStackNames: { [nestedStackName: string]: NestedStackTemplates },
119+
hotswapPropertyOverrides: HotswapPropertyOverrides,
116120
): Promise<ClassifiedResourceChanges> {
117121
const resourceDifferences = getStackResourceDifferences(stackChanges);
118122

@@ -131,7 +135,14 @@ async function classifyResourceChanges(
131135
// gather the results of the detector functions
132136
for (const [logicalId, change] of Object.entries(resourceDifferences)) {
133137
if (change.newValue?.Type === 'AWS::CloudFormation::Stack' && change.oldValue?.Type === 'AWS::CloudFormation::Stack') {
134-
const nestedHotswappableResources = await findNestedHotswappableChanges(logicalId, change, nestedStackNames, evaluateCfnTemplate, sdk);
138+
const nestedHotswappableResources = await findNestedHotswappableChanges(
139+
logicalId,
140+
change,
141+
nestedStackNames,
142+
evaluateCfnTemplate,
143+
sdk,
144+
hotswapPropertyOverrides,
145+
);
135146
hotswappableResources.push(...nestedHotswappableResources.hotswappableChanges);
136147
nonHotswappableResources.push(...nestedHotswappableResources.nonHotswappableChanges);
137148

@@ -151,7 +162,7 @@ async function classifyResourceChanges(
151162
const resourceType: string = hotswappableChangeCandidate.newValue.Type;
152163
if (resourceType in RESOURCE_DETECTORS) {
153164
// run detector functions lazily to prevent unhandled promise rejections
154-
promises.push(() => RESOURCE_DETECTORS[resourceType](logicalId, hotswappableChangeCandidate, evaluateCfnTemplate));
165+
promises.push(() => RESOURCE_DETECTORS[resourceType](logicalId, hotswappableChangeCandidate, evaluateCfnTemplate, hotswapPropertyOverrides));
155166
} else {
156167
reportNonHotswappableChange(nonHotswappableResources, hotswappableChangeCandidate, undefined, 'This resource type is not supported for hotswap deployments');
157168
}
@@ -233,6 +244,7 @@ async function findNestedHotswappableChanges(
233244
nestedStackTemplates: { [nestedStackName: string]: NestedStackTemplates },
234245
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
235246
sdk: ISDK,
247+
hotswapPropertyOverrides: HotswapPropertyOverrides,
236248
): Promise<ClassifiedResourceChanges> {
237249
const nestedStack = nestedStackTemplates[logicalId];
238250
if (!nestedStack.physicalName) {
@@ -256,7 +268,12 @@ async function findNestedHotswappableChanges(
256268
nestedStackTemplates[logicalId].deployedTemplate, nestedStackTemplates[logicalId].generatedTemplate,
257269
);
258270

259-
return classifyResourceChanges(nestedDiff, evaluateNestedCfnTemplate, sdk, nestedStackTemplates[logicalId].nestedStackTemplates);
271+
return classifyResourceChanges(
272+
nestedDiff,
273+
evaluateNestedCfnTemplate,
274+
sdk,
275+
nestedStackTemplates[logicalId].nestedStackTemplates,
276+
hotswapPropertyOverrides);
260277
}
261278

262279
/** Returns 'true' if a pair of changes is for the same resource. */

packages/aws-cdk/lib/api/hotswap/common.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,52 @@ export class HotswappableChangeCandidate {
9898

9999
type Exclude = { [key: string]: Exclude | true }
100100

101+
/**
102+
* Represents configuration property overrides for hotswap deployments
103+
*/
104+
export class HotswapPropertyOverrides {
105+
// Each supported resource type will have its own properties. Currently this is ECS
106+
ecsHotswapProperties?: EcsHotswapProperties;
107+
108+
public constructor (ecsHotswapProperties?: EcsHotswapProperties) {
109+
this.ecsHotswapProperties = ecsHotswapProperties;
110+
}
111+
}
112+
113+
/**
114+
* Represents configuration properties for ECS hotswap deployments
115+
*/
116+
export class EcsHotswapProperties {
117+
// The lower limit on the number of your service's tasks that must remain in the RUNNING state during a deployment, as a percentage of the desiredCount
118+
readonly minimumHealthyPercent?: number;
119+
// The upper limit on the number of your service's tasks that are allowed in the RUNNING or PENDING state during a deployment, as a percentage of the desiredCount
120+
readonly maximumHealthyPercent?: number;
121+
122+
public constructor (minimumHealthyPercent?: number, maximumHealthyPercent?: number) {
123+
if (minimumHealthyPercent !== undefined && minimumHealthyPercent < 0 ) {
124+
throw new Error('hotswap-ecs-minimum-healthy-percent can\'t be a negative number');
125+
}
126+
if (maximumHealthyPercent !== undefined && maximumHealthyPercent < 0 ) {
127+
throw new Error('hotswap-ecs-maximum-healthy-percent can\'t be a negative number');
128+
}
129+
// In order to preserve the current behaviour, when minimumHealthyPercent is not defined, it will be set to the currently default value of 0
130+
if (minimumHealthyPercent == undefined) {
131+
this.minimumHealthyPercent = 0;
132+
} else {
133+
this.minimumHealthyPercent = minimumHealthyPercent;
134+
}
135+
this.maximumHealthyPercent = maximumHealthyPercent;
136+
}
137+
138+
/**
139+
* Check if any hotswap properties are defined
140+
* @returns true if all properties are undefined, false otherwise
141+
*/
142+
public isEmpty(): boolean {
143+
return this.minimumHealthyPercent === 0 && this.maximumHealthyPercent === undefined;
144+
}
145+
}
146+
101147
/**
102148
* This function transforms all keys (recursively) in the provided `val` object.
103149
*

packages/aws-cdk/lib/api/hotswap/ecs-services.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
import * as AWS from 'aws-sdk';
2-
import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, lowerCaseFirstCharacter, reportNonHotswappableChange, transformObjectKeys } from './common';
2+
import { ChangeHotswapResult, classifyChanges, HotswappableChangeCandidate, HotswapPropertyOverrides, lowerCaseFirstCharacter, reportNonHotswappableChange, transformObjectKeys } from './common';
33
import { ISDK } from '../aws-auth';
44
import { EvaluateCloudFormationTemplate } from '../evaluate-cloudformation-template';
55

66
export async function isHotswappableEcsServiceChange(
7-
logicalId: string, change: HotswappableChangeCandidate, evaluateCfnTemplate: EvaluateCloudFormationTemplate,
7+
logicalId: string,
8+
change: HotswappableChangeCandidate,
9+
evaluateCfnTemplate: EvaluateCloudFormationTemplate,
10+
hotswapPropertyOverrides: HotswapPropertyOverrides,
811
): Promise<ChangeHotswapResult> {
912
// the only resource change we can evaluate here is an ECS TaskDefinition
1013
if (change.newValue.Type !== 'AWS::ECS::TaskDefinition') {
@@ -83,6 +86,10 @@ export async function isHotswappableEcsServiceChange(
8386
const registerTaskDefResponse = await sdk.ecs().registerTaskDefinition(lowercasedTaskDef).promise();
8487
const taskDefRevArn = registerTaskDefResponse.taskDefinition?.taskDefinitionArn;
8588

89+
let ecsHotswapProperties = hotswapPropertyOverrides.ecsHotswapProperties;
90+
let minimumHealthyPercent = ecsHotswapProperties?.minimumHealthyPercent;
91+
let maximumHealthyPercent = ecsHotswapProperties?.maximumHealthyPercent;
92+
8693
// Step 2 - update the services using that TaskDefinition to point to the new TaskDefinition Revision
8794
const servicePerClusterUpdates: { [cluster: string]: Array<{ promise: Promise<any>; ecsService: EcsService }> } = {};
8895
for (const ecsService of ecsServicesReferencingTaskDef) {
@@ -105,7 +112,8 @@ export async function isHotswappableEcsServiceChange(
105112
cluster: clusterName,
106113
forceNewDeployment: true,
107114
deploymentConfiguration: {
108-
minimumHealthyPercent: 0,
115+
minimumHealthyPercent: minimumHealthyPercent !== undefined ? minimumHealthyPercent : 0,
116+
maximumPercent: maximumHealthyPercent !== undefined ? maximumHealthyPercent : undefined,
109117
},
110118
}).promise(),
111119
ecsService: ecsService,

packages/aws-cdk/lib/cdk-toolkit.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { CloudAssembly, DefaultSelection, ExtendedStackSelection, StackCollectio
1313
import { CloudExecutable } from './api/cxapp/cloud-executable';
1414
import { Deployments } from './api/deployments';
1515
import { GarbageCollector } from './api/garbage-collection/garbage-collector';
16-
import { HotswapMode } from './api/hotswap/common';
16+
import { HotswapMode, HotswapPropertyOverrides, EcsHotswapProperties } from './api/hotswap/common';
1717
import { findCloudWatchLogGroups } from './api/logs/find-cloudwatch-logs';
1818
import { CloudWatchLogEventMonitor } from './api/logs/logs-monitor';
1919
import { createDiffChangeSet, ResourcesToImport } from './api/util/cloudformation';
@@ -237,6 +237,14 @@ export class CdkToolkit {
237237
warning('⚠️ They should only be used for development - never use them for your production Stacks!\n');
238238
}
239239

240+
let hotswapPropertiesFromSettings = this.props.configuration.settings.get(['hotswap']) || {};
241+
242+
let hotswapPropertyOverrides = new HotswapPropertyOverrides();
243+
hotswapPropertyOverrides.ecsHotswapProperties = new EcsHotswapProperties(
244+
hotswapPropertiesFromSettings.ecs?.minimumHealthyPercent,
245+
hotswapPropertiesFromSettings.ecs?.maximumHealthyPercent,
246+
);
247+
240248
const stacks = stackCollection.stackArtifacts;
241249

242250
const stackOutputs: { [key: string]: any } = { };
@@ -347,6 +355,7 @@ export class CdkToolkit {
347355
ci: options.ci,
348356
rollback: options.rollback,
349357
hotswap: options.hotswap,
358+
hotswapPropertyOverrides: hotswapPropertyOverrides,
350359
extraUserAgent: options.extraUserAgent,
351360
assetParallelism: options.assetParallelism,
352361
ignoreNoStacks: options.ignoreNoStacks,

packages/aws-cdk/lib/settings.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,12 @@ export class Settings {
292292
assetParallelism: argv['asset-parallelism'],
293293
assetPrebuild: argv['asset-prebuild'],
294294
ignoreNoStacks: argv['ignore-no-stacks'],
295+
hotswap: {
296+
ecs: {
297+
minimumEcsHealthyPercent: argv.minimumEcsHealthyPercent,
298+
maximumEcsHealthyPercent: argv.maximumEcsHealthyPercent,
299+
},
300+
},
295301
unstable: argv.unstable,
296302
});
297303
}

0 commit comments

Comments
 (0)