-
Notifications
You must be signed in to change notification settings - Fork 3.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(ecs): Support specifying revision of task definition #27036
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't this not allowed by CDK guidelines?
aws-cdk/docs/DESIGN_GUIDELINES.md
Lines 660 to 664 in 94ccec6
#### Unions | |
Do not use TypeScript union types in construct APIs (`string | number`) since | |
many of the target languages supported by the CDK cannot strongly-model such | |
types _[awslint:props-no-unions]_. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
} | |
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
* 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/** | |
* 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Warning sounds completely fair - will do!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That was my gut feeling. I'll revert that change unless I'm told otherwise
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 <[email protected]>
Co-authored-by: Luca Pizzini <[email protected]>
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple minor wording changes.
That could be it, could you give that a try please? |
Co-authored-by: paulhcsun <[email protected]>
AWS CodeBuild CI Report
Powered by github-codebuild-logs, available on the AWS Serverless Application Repository |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
latest
which 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