Skip to content
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

Merged
merged 20 commits into from
Dec 14, 2023

Conversation

luxaritas
Copy link
Contributor

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

@aws-cdk-automation aws-cdk-automation requested a review from a team September 6, 2023 23:25
@github-actions github-actions bot added beginning-contributor [Pilot] contributed between 0-2 PRs to the CDK effort/medium Medium work item – several days of effort feature-request A feature should be added or improved. p2 labels Sep 6, 2023
@aws-cdk-automation aws-cdk-automation added the pr/needs-community-review This PR needs a review from a Trusted Community Member or Core Team Member. label Sep 6, 2023
Copy link
Contributor

@lpizzinidev lpizzinidev left a 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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
readonly taskDefinitionRevision?: string;
readonly taskDefinitionRevision?: number | 'latest';

Stricter type definition helps with validation and enables users to specify the correct value.

Copy link
Contributor Author

@luxaritas luxaritas Nov 11, 2023

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?

#### 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]_.

Copy link
Contributor Author

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

Comment on lines 613 to 641
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'
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Comment on lines 652 to 661
}

if (props.taskDefinitionRevision) {
this.resource.taskDefinition = taskDefinition.family;
if (props.taskDefinitionRevision !== 'latest') {
this.resource.taskDefinition += `:${props.taskDefinitionRevision}`;
}
this.node.addDependency(taskDefinition);
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
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).

Copy link
Contributor Author

@luxaritas luxaritas Oct 25, 2023

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

Copy link
Contributor

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.

Copy link
Contributor Author

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

Comment on lines 350 to 351
* The revision of the service's task definition to use for tasks in this service
* or 'latest' to use the latest ACTIVE task revision
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* 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.

@luxaritas
Copy link
Contributor Author

luxaritas commented Oct 25, 2023

Thanks for the response - some good points which I should be able to address soon. With respect to the comments not mentioned inline:

Not sure about the behavior for DeploymentControllerType.CODE_DEPLOY so feel free to clarify that.

See

if (props.deploymentController?.type === DeploymentControllerType.CODE_DEPLOY) {
// Strip the revision ID from the service's task definition property to
// prevent new task def revisions in the stack from triggering updates
// to the stack's ECS service resource
this.resource.taskDefinition = taskDefinition.family;
this.node.addDependency(taskDefinition);
}

Also, are you sure that it is necessary to explicitly set latest?
Looks like the ECS service defaults to the latest active value already.

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

@aws-cdk-automation aws-cdk-automation removed the pr/needs-community-review This PR needs a review from a Trusted Community Member or Core Team Member. label Oct 26, 2023
@lpizzinidev
Copy link
Contributor

@luxaritas

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.
Thanks for the follow-up and clarifications 👍

@luxaritas
Copy link
Contributor Author

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.

Copy link
Contributor

@lpizzinidev lpizzinidev left a 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.

Comment on lines 1237 to 1262

/**
* 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;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/**
* 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.

Comment on lines 666 to 667
if (props.taskDefinitionRevision.revisionId !== 'latest') {
this.resource.taskDefinition += `:${props.taskDefinitionRevision.revisionId}`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (props.taskDefinitionRevision.revisionId !== 'latest') {
this.resource.taskDefinition += `:${props.taskDefinitionRevision.revisionId}`;
if (props.taskDefinitionRevision !== TaskDefinitionRevision.LATEST) {
this.resource.taskDefinition += `:${props.taskDefinitionRevision.revision}`;

Comment on lines 650 to 657
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');
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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.

Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor Author

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!

Copy link
Contributor

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.

Copy link
Contributor Author

@luxaritas luxaritas Dec 13, 2023

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

Copy link
Contributor

@lpizzinidev lpizzinidev left a 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.

packages/aws-cdk-lib/aws-ecs/test/task-definition.test.ts Outdated Show resolved Hide resolved
packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts Outdated Show resolved Hide resolved
packages/aws-cdk-lib/aws-ecs/lib/base/base-service.ts Outdated Show resolved Hide resolved
packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts Outdated Show resolved Hide resolved
packages/aws-cdk-lib/aws-ecs/lib/base/task-definition.ts Outdated Show resolved Hide resolved
@aws-cdk-automation aws-cdk-automation added the pr/needs-maintainer-review This PR needs a review from a Core Team Member label Nov 16, 2023
@paulhcsun
Copy link
Contributor

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!

@luxaritas
Copy link
Contributor Author

[Container] 2023/12/13 22:28:16.388276 Running command npm config set unsafe-perm true
npm ERR! unsafe-perm is not a valid npm option

Nothing I did, is this a situation where I just need to pull in the latest changes from main?

@paulhcsun
Copy link
Contributor

I'm seeing the following linting error in the build logs, but it's strange because a docstring is included.

aws-cdk-lib: error: [awslint:docs-public-apis:aws-cdk-lib.aws_ecs.TaskDefinitionRevision.revision] Public API element must have a docstring 
aws-cdk-lib: Error: /codebuild/output/src2582654880/src/github.com/aws/aws-cdk/tools/@aws-cdk/cdk-build-tools/bin/cdk-awslint exited with error code 1
aws-cdk-lib: Build failed.!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

@luxaritas
Copy link
Contributor Author

luxaritas commented Dec 14, 2023

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

Copy link
Contributor

@paulhcsun paulhcsun left a 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.

@paulhcsun
Copy link
Contributor

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

That could be it, could you give that a try please?

@aws-cdk-automation
Copy link
Collaborator

AWS CodeBuild CI Report

  • CodeBuild project: AutoBuildv2Project1C6BFA3F-wQm2hXv2jqQv
  • Commit ID: ef6f775
  • Result: SUCCEEDED
  • Build Logs (available for 30 days)

Powered by github-codebuild-logs, available on the AWS Serverless Application Repository

Copy link
Contributor

@paulhcsun paulhcsun left a 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!

Copy link
Contributor

mergify bot commented Dec 14, 2023

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).

@mergify mergify bot merged commit de0d77b into aws:main Dec 14, 2023
10 checks passed
@aws-cdk-automation aws-cdk-automation removed the pr/needs-maintainer-review This PR needs a review from a Core Team Member label Dec 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
beginning-contributor [Pilot] contributed between 0-2 PRs to the CDK effort/medium Medium work item – several days of effort feature-request A feature should be added or improved. p2
Projects
None yet
Development

Successfully merging this pull request may close these issues.

(aws-ecs): Support specifying latest revision of task family in service instead of specific revision
4 participants