feat(ecs): Support specifying revision of task definition#27036
feat(ecs): Support specifying revision of task definition#27036mergify[bot] merged 20 commits intoaws:mainfrom
Conversation
lpizzinidev
left a comment
There was a problem hiding this comment.
Thanks for the implementation (and sorry for the late review) 👍
Some minor adjustments are needed in my opinion.
Not sure about the behavior for DeploymentControllerType.CODE_DEPLOY so feel free to clarify that.
Also, are you sure that it is necessary to explicitly set latest?
Looks like the ECS service defaults to the latest active value already.
| * | ||
| * @default - Uses the revision of the passed task definition deployed by CloudFormation | ||
| */ | ||
| readonly taskDefinitionRevision?: string; |
There was a problem hiding this comment.
| readonly taskDefinitionRevision?: string; | |
| readonly taskDefinitionRevision?: number | 'latest'; |
Stricter type definition helps with validation and enables users to specify the correct value.
There was a problem hiding this comment.
Isn't this not allowed by CDK guidelines?
aws-cdk/docs/DESIGN_GUIDELINES.md
Lines 660 to 664 in 94ccec6
There was a problem hiding this comment.
Ive attempted to do what the guidelines suggest and use a class with static methods, though would appreciate a look over how I approached that
| By default, the service will use the revision of the passed task definition generated when the `TaskDefinition` | ||
| is deployed by CloudFormation. In order to specify a specific revision, pass a `taskDefinitionRevision`: | ||
|
|
||
| ```ts | ||
| declare const cluster: ecs.Cluster; | ||
| declare const taskDefinition: ecs.TaskDefinition; | ||
|
|
||
| const service = new ecs.ExternalService(this, 'Service', { | ||
| cluster, | ||
| taskDefinition, | ||
| desiredCount: 5, | ||
| taskDefinitionRevision: '1' | ||
| }); | ||
| ``` | ||
|
|
||
| Or, to always use the latest active revision (for example, when using the CodePipeline EcsDeployAction | ||
| without using the CODE_DEPLOY deployment controller to ensure future service deployments don't revert | ||
| the task revision used by the service): | ||
|
|
||
| ```ts | ||
| declare const cluster: ecs.Cluster; | ||
| declare const taskDefinition: ecs.TaskDefinition; | ||
|
|
||
| const service = new ecs.ExternalService(this, 'Service', { | ||
| cluster, | ||
| taskDefinition, | ||
| desiredCount: 5, | ||
| taskDefinitionRevision: 'latest' | ||
| }); |
There was a problem hiding this comment.
| By default, the service will use the revision of the passed task definition generated when the `TaskDefinition` | |
| is deployed by CloudFormation. In order to specify a specific revision, pass a `taskDefinitionRevision`: | |
| ```ts | |
| declare const cluster: ecs.Cluster; | |
| declare const taskDefinition: ecs.TaskDefinition; | |
| const service = new ecs.ExternalService(this, 'Service', { | |
| cluster, | |
| taskDefinition, | |
| desiredCount: 5, | |
| taskDefinitionRevision: '1' | |
| }); | |
| ``` | |
| Or, to always use the latest active revision (for example, when using the CodePipeline EcsDeployAction | |
| without using the CODE_DEPLOY deployment controller to ensure future service deployments don't revert | |
| the task revision used by the service): | |
| ```ts | |
| declare const cluster: ecs.Cluster; | |
| declare const taskDefinition: ecs.TaskDefinition; | |
| const service = new ecs.ExternalService(this, 'Service', { | |
| cluster, | |
| taskDefinition, | |
| desiredCount: 5, | |
| taskDefinitionRevision: 'latest' | |
| }); | |
| ```suggestion | |
| By default, the service will use the revision of the passed task definition generated when the `TaskDefinition` | |
| is deployed by CloudFormation. | |
| To set a specific revision number or use the latest revision use the `taskDefinitionRevision` parameter: | |
| ```ts | |
| declare const cluster: ecs.Cluster; | |
| declare const taskDefinition: ecs.TaskDefinition; | |
| new ecs.ExternalService(this, 'Service', { | |
| cluster, | |
| taskDefinition, | |
| desiredCount: 5, | |
| taskDefinitionRevision: 1 | |
| }); | |
| new ecs.ExternalService(this, 'Service', { | |
| cluster, | |
| taskDefinition, | |
| desiredCount: 5, | |
| taskDefinitionRevision: 'latest' | |
| }); |
More concise.
There was a problem hiding this comment.
To make sure I understand correctly, you think the description of the latest special case and why it would be used is unnecessary/too much information?
There was a problem hiding this comment.
No, but I think that documentation needs to be clear and direct.
Feel free to add examples or use cases to my suggestion.
| } | ||
|
|
||
| if (props.taskDefinitionRevision) { | ||
| this.resource.taskDefinition = taskDefinition.family; | ||
| if (props.taskDefinitionRevision !== 'latest') { | ||
| this.resource.taskDefinition += `:${props.taskDefinitionRevision}`; | ||
| } | ||
| this.node.addDependency(taskDefinition); | ||
| } | ||
|
|
There was a problem hiding this comment.
| } | |
| if (props.taskDefinitionRevision) { | |
| this.resource.taskDefinition = taskDefinition.family; | |
| if (props.taskDefinitionRevision !== 'latest') { | |
| this.resource.taskDefinition += `:${props.taskDefinitionRevision}`; | |
| } | |
| this.node.addDependency(taskDefinition); | |
| } | |
| } else if (props.taskDefinitionRevision) { | |
| if (props.taskDefinitionRevision !== 'latest' && props.taskDefinitionRevision < 1) { | |
| throw new Error(`taskDefinitionRevision must be 'latest' or a positive number, got ${props.taskDefinitionRevision}`); | |
| } | |
| this.resource.taskDefinition = taskDefinition.family + (props.taskDefinitionRevision !== 'latest' | |
| `:${props.taskDefinitionRevision}` : ''); | |
| this.node.addDependency(taskDefinition); | |
| } | |
Should the revision number be enforced only for non-CODE_DEPLOY types?
Also, I would add validation for the revision when numeric (and a relative unit test).
There was a problem hiding this comment.
Hmmm... initially I didn't see any reason why the revision can't be made more specific than latest for CODE_DEPLOY - it's only as it is so that it's not whatever the latest version is at the time of deployment. However, the entire point of CODE_DEPLOY is that it's being deployed from code deploy, so if a new deployment is triggered it would override this anyways. I could maybe see a use case for if you want to temporarily pin and just not run the pipeline, but that seems rare and likely to cause more confusion than benefit. So you're probably right.
Good point on additionally validating that it's a number if not latest
There was a problem hiding this comment.
the entire point of CODE_DEPLOY is that it's being deployed from code deploy, so if a new deployment is triggered it would override this anyways
If this is the case it's better to keep the else if then, otherwise this.resource.taskDefinition would get overridden if taskDefinitionRevision is numeric.
There was a problem hiding this comment.
In this case, I'll also throw an error if a non-latest revision is specified and the deployment type is CODE_DEPLOY, to prevent confusion around explicitly asking for something different and it silently not doing that
| * The revision of the service's task definition to use for tasks in this service | ||
| * or 'latest' to use the latest ACTIVE task revision |
There was a problem hiding this comment.
| * The revision of the service's task definition to use for tasks in this service | |
| * or 'latest' to use the latest ACTIVE task revision | |
| * Revision number for the task definition or `latest` to use the latest active task revision. |
|
Thanks for the response - some good points which I should be able to address soon. With respect to the comments not mentioned inline:
See aws-cdk/packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts Lines 638 to 644 in c551a64
Yes - and indeed it is already done in the code I linked above. I believe the reason why it is necessary is because in the concrete implementations of BaseService (FargateService/Ec2Service), the task arn is passed as part of additionalProps, and the arn includes the revision |
Ok, got it. |
… deployment controller
|
Sorry for the delay in revisiting this. Made changes based on your comments. I've adjusted the way taskDefinitionRevision is typed per the comment thread above, let me know if it's still not right. |
lpizzinidev
left a comment
There was a problem hiding this comment.
Thanks 👍
Some adjustments for the added data structure are needed.
|
|
||
| /** | ||
| * Represents revision of a task definition, either a specific numbered revision or | ||
| * the specia "latest" revision | ||
| */ | ||
| export class TaskDefinitionRevision { | ||
| /** | ||
| * The most recent revision of a task | ||
| */ | ||
| static latest() { | ||
| return new TaskDefinitionRevision('latest'); | ||
| } | ||
|
|
||
| /** | ||
| * A specfic numbered revision of a task | ||
| */ | ||
| static revision(revision: number) { | ||
| return new TaskDefinitionRevision(revision.toString()); | ||
| } | ||
|
|
||
| public readonly revisionId: string; | ||
|
|
||
| constructor(revision: string) { | ||
| this.revisionId = revision; | ||
| } | ||
| } |
There was a problem hiding this comment.
| /** | |
| * Represents revision of a task definition, either a specific numbered revision or | |
| * the specia "latest" revision | |
| */ | |
| export class TaskDefinitionRevision { | |
| /** | |
| * The most recent revision of a task | |
| */ | |
| static latest() { | |
| return new TaskDefinitionRevision('latest'); | |
| } | |
| /** | |
| * A specfic numbered revision of a task | |
| */ | |
| static revision(revision: number) { | |
| return new TaskDefinitionRevision(revision.toString()); | |
| } | |
| public readonly revisionId: string; | |
| constructor(revision: string) { | |
| this.revisionId = revision; | |
| } | |
| } | |
| /** | |
| * Represents a task definition revision. | |
| */ | |
| export class TaskDefinitionRevision { | |
| /** | |
| * The most recent revision of a task | |
| */ | |
| public static readonly LATEST = new TaskDefinitionRevision('latest'); | |
| /** | |
| * Specific revision of a task | |
| */ | |
| public static of(revision: number) { | |
| if (revision < 1) { | |
| throw new Error(`A task definition revision must be 'latest' or a positive number, got ${revision}`); | |
| } | |
| return new TaskDefinitionRevision(revision.toString()); | |
| } | |
| private constructor(public readonly revision: string) {} | |
| } |
Let's follow the enum-class pattern for this.
Thanks for the clarification on union types 👍
A unit test is needed on revision number validation.
| if (props.taskDefinitionRevision.revisionId !== 'latest') { | ||
| this.resource.taskDefinition += `:${props.taskDefinitionRevision.revisionId}`; |
There was a problem hiding this comment.
| if (props.taskDefinitionRevision.revisionId !== 'latest') { | |
| this.resource.taskDefinition += `:${props.taskDefinitionRevision.revisionId}`; | |
| if (props.taskDefinitionRevision !== TaskDefinitionRevision.LATEST) { | |
| this.resource.taskDefinition += `:${props.taskDefinitionRevision.revision}`; |
| if ( | ||
| props.deploymentController?.type === DeploymentControllerType.CODE_DEPLOY | ||
| && props.taskDefinitionRevision | ||
| && props.taskDefinitionRevision.revisionId !== 'latest' | ||
| ) { | ||
| throw new Error('CODE_DEPLOY deploymentController cannot be used with a non-latest task definition revision'); | ||
| } | ||
|
|
There was a problem hiding this comment.
| if ( | |
| props.deploymentController?.type === DeploymentControllerType.CODE_DEPLOY | |
| && props.taskDefinitionRevision | |
| && props.taskDefinitionRevision.revisionId !== 'latest' | |
| ) { | |
| throw new Error('CODE_DEPLOY deploymentController cannot be used with a non-latest task definition revision'); | |
| } |
I don't think it's necessary.
If props.deploymentController?.type === DeploymentControllerType.CODE_DEPLOY we will run the first if statement and taskDefinitionRevision will just get ignored.
There was a problem hiding this comment.
The reason I did this is because if the user specifically asked for revision 23, but they got latest instead, that seems like an unintended surprise/footgun
There was a problem hiding this comment.
Thanks for clarifying, however, I think it's overkill to fail a deployment that would work.
Let's use Annotations.addWarningV2 to provide some feedback and be more user-friendly.
There was a problem hiding this comment.
Warning sounds completely fair - will do!
There was a problem hiding this comment.
I disagree here, I feel like an error is reasonable even though the deployment would work because we are changing a setting that the user had set and not actually running the revision the user had specified.
There was a problem hiding this comment.
That was my gut feeling. I'll revert that change unless I'm told otherwise
lpizzinidev
left a comment
There was a problem hiding this comment.
Thanks for the changes!
Some minor cleanup and it will be good to go for me.
Note #27036 (comment) from the previous review.
Co-authored-by: Luca Pizzini <lpizzini7@gmail.com>
Co-authored-by: Luca Pizzini <lpizzini7@gmail.com>
|
Overall looks good to me, just left one comment regarding changing the error to a warning. Thanks for the contribution and thanks @lpizzinidev for reviewing! |
Nothing I did, is this a situation where I just need to pull in the latest changes from main? |
|
I'm seeing the following linting error in the build logs, but it's strange because a docstring is included. |
|
I think it's the public member declared in the constructor? I'm assuming that needs to be pulled out in order to add a docstring |
paulhcsun
left a comment
There was a problem hiding this comment.
A couple minor wording changes.
That could be it, could you give that a try please? |
Co-authored-by: paulhcsun <47882901+paulhcsun@users.noreply.github.com>
AWS CodeBuild CI Report
Powered by github-codebuild-logs, available on the AWS Serverless Application Repository |
paulhcsun
left a comment
There was a problem hiding this comment.
Great work @luxaritas, thanks for the contribution!
|
Thank you for contributing! Your pull request will be updated from main and then merged automatically (do not update manually, and be sure to allow changes to be pushed to your fork). |
If using CodePipeline EcsDeployAction without using the CODE_DEPLOY deployment controller, future deployments of an ECS service will revert the task definition to the task definition deployed by CloudFormation, even though the latest active revision created by the deploy action is the one that is intended to be used. This provides a way to specify the specific revision of a task definition that should be used, including the special value
latestwhich uses the latest ACTIVE revision.Closes #26983.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license