Skip to content

Commit 4acf961

Browse files
committed
feat(ecs): Add forceNewDeployment feature for ecs service
1 parent 3ec6d06 commit 4acf961

File tree

3 files changed

+225
-0
lines changed

3 files changed

+225
-0
lines changed

packages/aws-cdk-lib/aws-ecs/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2234,3 +2234,31 @@ new ecs.ExternalService(this, 'ExternalService', {
22342234
daemon: true,
22352235
});
22362236
```
2237+
2238+
### Force New Deployment
2239+
2240+
You can force a new deployment of a service without changing the task definition or desired count. This can be useful to trigger a deployment when you want to deploy new tasks even if there are no changes to the service configuration.
2241+
2242+
When enabled, ECS will start a new deployment even if there are no changes to the service configuration. This is accomplished by setting a unique timestamp nonce that forces CloudFormation to recognize the service as changed.
2243+
2244+
```ts
2245+
declare const cluster: ecs.Cluster;
2246+
declare const taskDefinition: ecs.TaskDefinition;
2247+
2248+
const service = new ecs.FargateService(this, 'Service', {
2249+
cluster,
2250+
taskDefinition,
2251+
});
2252+
2253+
// Force a new deployment
2254+
service.forceNewDeployment(true);
2255+
```
2256+
2257+
You can also disable force new deployment by calling the method with `false`:
2258+
2259+
```ts
2260+
// Disable force new deployment
2261+
service.forceNewDeployment(false);
2262+
```
2263+
2264+
The `forceNewDeployment` method works with all service types including `FargateService`, `Ec2Service`, and `ExternalService`. Each call to this method generates a unique timestamp nonce, ensuring that multiple services or multiple calls to the same service will have different nonces.

packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -861,6 +861,25 @@ export abstract class BaseService extends Resource
861861
this.node.defaultChild = this.resource;
862862
}
863863

864+
/**
865+
* Forces a new deployment of the service.
866+
*
867+
* This can be used to trigger a deployment without changing the task definition or desired count.
868+
* When enabled, ECS will start a new deployment even if there are no changes to the service configuration.
869+
*
870+
* @param enable - Whether to enable force new deployment
871+
*/
872+
public forceNewDeployment(enable: boolean) {
873+
if (!this.isEcsDeploymentController) {
874+
throw new ValidationError('forceNewDeployment requires the ECS deployment controller.', this);
875+
}
876+
877+
this.resource.forceNewDeployment = {
878+
enableForceNewDeployment: enable,
879+
forceNewDeploymentNonce: enable? new Date().toISOString() : '',
880+
};
881+
}
882+
864883
/**
865884
* Add a deployment lifecycle hook target
866885
* @param target The lifecycle hook target to add

packages/aws-cdk-lib/aws-ecs/test/base-service.test.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,3 +517,181 @@ describe('Blue/Green Deployment', () => {
517517
expect(service.isUsingECSDeploymentController()).toBe(false);
518518
});
519519
});
520+
521+
describe('forceNewDeployment', () => {
522+
let stack: cdk.Stack;
523+
let vpc: ec2.Vpc;
524+
let cluster: ecs.Cluster;
525+
let taskDefinition: ecs.FargateTaskDefinition;
526+
527+
beforeEach(() => {
528+
stack = new cdk.Stack();
529+
vpc = new ec2.Vpc(stack, 'Vpc');
530+
cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc });
531+
taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef');
532+
taskDefinition.addContainer('web', {
533+
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
534+
});
535+
});
536+
537+
test('should enable force new deployment when called with true', () => {
538+
// GIVEN
539+
const service = new ecs.FargateService(stack, 'FargateService', {
540+
cluster,
541+
taskDefinition,
542+
});
543+
544+
// WHEN
545+
service.forceNewDeployment(true);
546+
547+
// THEN
548+
const template = Template.fromStack(stack);
549+
template.hasResourceProperties('AWS::ECS::Service', {
550+
ForceNewDeployment: {
551+
EnableForceNewDeployment: true,
552+
ForceNewDeploymentNonce: Match.stringLikeRegexp('^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$'),
553+
},
554+
});
555+
});
556+
557+
test('should disable force new deployment when called with false', () => {
558+
// GIVEN
559+
const service = new ecs.FargateService(stack, 'FargateService', {
560+
cluster,
561+
taskDefinition,
562+
});
563+
564+
// WHEN
565+
service.forceNewDeployment(false);
566+
567+
// THEN
568+
const template = Template.fromStack(stack);
569+
template.hasResourceProperties('AWS::ECS::Service', {
570+
ForceNewDeployment: {
571+
EnableForceNewDeployment: false,
572+
ForceNewDeploymentNonce: '',
573+
},
574+
});
575+
});
576+
577+
test('should work with EC2 service', () => {
578+
// GIVEN
579+
cluster.addCapacity('DefaultAutoScalingGroup', {
580+
instanceType: new ec2.InstanceType('t3.micro'),
581+
minCapacity: 1,
582+
});
583+
const ec2TaskDefinition = new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef');
584+
ec2TaskDefinition.addContainer('web', {
585+
image: ecs.ContainerImage.fromRegistry('amazon/amazon-ecs-sample'),
586+
memoryLimitMiB: 512,
587+
});
588+
const service = new ecs.Ec2Service(stack, 'Ec2Service', {
589+
cluster,
590+
taskDefinition: ec2TaskDefinition,
591+
});
592+
593+
// WHEN
594+
service.forceNewDeployment(true);
595+
596+
// THEN
597+
const template = Template.fromStack(stack);
598+
template.hasResourceProperties('AWS::ECS::Service', {
599+
ForceNewDeployment: {
600+
EnableForceNewDeployment: true,
601+
ForceNewDeploymentNonce: Match.stringLikeRegexp('^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$'),
602+
},
603+
});
604+
});
605+
606+
test('should work with different deployment controllers', () => {
607+
// GIVEN
608+
const service = new ecs.FargateService(stack, 'FargateService', {
609+
cluster,
610+
taskDefinition,
611+
deploymentController: {
612+
type: ecs.DeploymentControllerType.ECS,
613+
},
614+
});
615+
616+
// WHEN
617+
service.forceNewDeployment(true);
618+
619+
// THEN
620+
const template = Template.fromStack(stack);
621+
template.hasResourceProperties('AWS::ECS::Service', {
622+
ForceNewDeployment: {
623+
EnableForceNewDeployment: true,
624+
ForceNewDeploymentNonce: Match.stringLikeRegexp('^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z$'),
625+
},
626+
});
627+
});
628+
629+
test('should allow multiple calls to forceNewDeployment', () => {
630+
// GIVEN
631+
const service = new ecs.FargateService(stack, 'FargateService', {
632+
cluster,
633+
taskDefinition,
634+
});
635+
636+
// WHEN
637+
service.forceNewDeployment(true);
638+
service.forceNewDeployment(false);
639+
640+
// THEN
641+
const template = Template.fromStack(stack);
642+
template.hasResourceProperties('AWS::ECS::Service', {
643+
ForceNewDeployment: {
644+
EnableForceNewDeployment: false,
645+
ForceNewDeploymentNonce: '',
646+
},
647+
});
648+
});
649+
650+
test('should not set ForceNewDeployment property when not called', () => {
651+
// GIVEN
652+
const service = new ecs.FargateService(stack, 'FargateService', {
653+
cluster,
654+
taskDefinition,
655+
});
656+
657+
// WHEN - not calling forceNewDeployment
658+
659+
// THEN
660+
const template = Template.fromStack(stack);
661+
template.hasResourceProperties('AWS::ECS::Service', {
662+
ForceNewDeployment: Match.absent(),
663+
});
664+
});
665+
666+
test('should throw error when forceNewDeployment is called with non-ECS deployment controller', () => {
667+
// GIVEN
668+
const service = new ecs.FargateService(stack, 'FargateService', {
669+
cluster,
670+
taskDefinition,
671+
deploymentController: {
672+
type: ecs.DeploymentControllerType.CODE_DEPLOY,
673+
},
674+
});
675+
676+
// WHEN & THEN
677+
expect(() => {
678+
service.forceNewDeployment(true);
679+
}).toThrow('forceNewDeployment requires the ECS deployment controller.');
680+
});
681+
682+
test('should throw error when forceNewDeployment is called with EXTERNAL deployment controller', () => {
683+
// GIVEN
684+
const service = new ecs.FargateService(stack, 'FargateService', {
685+
cluster,
686+
taskDefinition,
687+
deploymentController: {
688+
type: ecs.DeploymentControllerType.EXTERNAL,
689+
},
690+
});
691+
692+
// WHEN & THEN
693+
expect(() => {
694+
service.forceNewDeployment(true);
695+
}).toThrow('forceNewDeployment requires the ECS deployment controller.');
696+
});
697+
});

0 commit comments

Comments
 (0)