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
design(ecs): multiple target group support design doc #3922
design(ecs): multiple target group support design doc #3922
Conversation
Codebuild (Continuous Integration) build failed for current commits. Please check log and resolve before PR is merged. |
|
||
// Open up security groups. For dynamic port mapping, we won't know the port range | ||
// in advance so we need to open up all ports. | ||
const port = this.taskDefinition.defaultContainer!.ingressPort; |
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.
This will need to be updated to reflect the desired host port for the container target.
|
||
... | ||
|
||
protected registerTargetBase(target: ContainerTarget) { |
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.
Depending on the order of operations, use case #2 would either effectively make this.targetContainers
a scratchpad that updates on each target registration (if depth first) or not work properly (if both registrations happen before both attachments). Do you know which is the case?
If the ordering is register/attach/register/attach I would suggest an annotation that the property is unstable/transient.
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 think the first one will be the case. However, I do agree that if we are going to implement this method then it should be annotated.
BTW, do you have any question or concern about the new API?
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.
Added thoughts below :)
I was requested to give this a review from #3891 since I have a real-world service that needs the second use case and proposed an alternative patch. This API design does give me what I need. I am a little concerned that it skips the abstraction layer of |
Thank you @karlpatr for your review! Really appreciate your feedback. And to address your concern about the port mapping, I would say it is a good point and will take into consideration when updating and finalizing the design doc. |
After a night of thinking this over, I do have a few more pieces of feedback; I do want to reiterate that this proposal already meets my need as it stands--these are just friction points that might benefit from additional thought. The proposed improvement to the current API has a comment that the array The call to
In that case both listeners would actually be using the settings for target2 due to the overwrite of the scratchpad before any targets have been attached. The simple answer is "don't do that", but I thought it's worth pointing out that the API allows people to shoot themselves in the foot. You could throw an exception if you see two attachments in a row after a register to prevent that edge case. |
Additional feedback for the new API: While I think that From a broader perspective, I'm also unsure if being able to set up the listener-service relationship from either service or listener methods will actually reduce the clarity of the API; as a CDK user I would want to know what the benefits and drawbacks of each approach were and be concerned about feature parity between the two methods as well as whether the generated templates would be compatible if I decided to change my approach. |
Thanks for more feedback! I do agree that the enhanced API suffers from that kind of issues and me and my teammates might finalize the design doc with only the new API, since the template that generated by the new API will be the same as the current one which set up the listener-service relationship from listeners. In terms of clarity, there are many nested interfaces in the new API, which might be simplify by introducing default values. |
Continuous integration build failed |
Without having read through everything thoroughly (bad me!), I have some observations upon skimming:
That is, registering a service into multiple targetgroups should be as easy as: targetGroup1.addTarget(ecsService);
targetGroup2.addTarget(ecsService); And this would probably work today already, wouldn't it? The only feature left to implement seems to be more control over the target container/target port.
You can leave the default implementation (using the first port on the first essential container) of In that sense, I love @karlpatr's proposal the most, which is essentially this, but the difference is how you get the integration class:
I think I even prefer the 2nd one, though it's slightly breaking with some conventions that we have in other services (where we use the 1st approach). We don't have this in other services though is because the 2nd approach (factory method) requires a dependency on the ELB package, whereas in other integration scenarios (say: Lambda and SNS) we avoid having dependencies between packages at all, and instead put the integration class into its own package. Since that ship has sailed here already (ECS already depends on ELB package), we're free to use the 2nd approach.
I would propose a method name without a verb to avoid the impression of a side effect happening, like
The |
Hi @rix0rrr, thanks for the review. Have you taken a look at my previous commit? This has the same as
FMHO isn't this also adding a new property "targetPort" only for ECS in ELBv2's target group class, instead of creating a new integration class? Besides, I don't think introducing a new integration class will help avoid the ordering problem. It will instead be an encapsulated alternative for "targetGroup1.addTarget(service.registerTarget(...))", which still suffer from the "register - add target -register another" kind of ordering problem if users put "service.registerTarget(...)" outside. From ECS's customers' perspective, the current proposal may be the best one, which is easy and robust for them to use. Hence, I do admit that will lead to a new "ECS-only" property in the ELBv2 package. What do you think? @SoManyHs @pkandasamy91 |
Thanks so much for taking the time to contribute to the AWS CDK ❤️ We will shortly assign someone to review this pull request and help get it
|
Agree that we shouldn't have a method that unexpectedly changes state or requires order-dependent method calls. Also agree that we shouldn't add an ECS-specific target in the ELB library -- feels like that breaks the philosophy of how we are using interfaces. Is it possible to invert the dependency (ie. go with the approach of attaching a service's container to a target group) without modifying the ELB lib? |
If that is truly what happens, I would classify that as an implementation bug, but I don't think this is an inherent consequence of this kind of API. The whole idea of a "scratch pad" is an implementation detail that doesn't need to happen. As I stated before in my response,
Not sure atomicity is the right word (or I might misunderstand what you're saying here) but in general I don't like ordering constraints, that is true, because it's just one more way for users to mess things up. In a very simple example:
Should do the same thing (Which is actually trickier than it looks, because to make it all Just Work for you, we need to modify the security group of the LB which we only have access to once everything is hooked up). What I find worst about this, is that IF there was an ordering dependency (must hook up this first, and then the other thing) and you did it wrong, there's no way for the user to tell that they did it wrong. However, in this case, the pattern is:
And the second line could throw a descriptive error if you didn't do things in the right order, which I find much less troublesome.
Let me be more blunt: I will not have it, especially as I'm not convinced that an integration class couldn't do the same job as effectively.
Not really. The underlying mechanism still relies on SOMETHING implementing
In fact, this is 80% of the implementation of interface EcsLoadBalancerTarget extends IApplicationLoadBalancerTarget, INetworkLoadBalancerTarget {
}
class BaseService {
public loadBalancerTarget(options: LoadBalancerTargetOptions): EcsLoadBalancerTarget {
const self = this;
return {
attachToApplicationTargetGroup(targetGroup: ApplicationTargetGroup): LoadBalancerTargetProps {
return self.attachToELBv2(targetGroup, options);
},
attachToApplicationTargetGroup(targetGroup: ApplicationTargetGroup): LoadBalancerTargetProps {
return self.attachToELBv2(targetGroup, options);
},
};
}
private attachToELBv2(targetGroup: elbv2.ITargetGroup, options: LoadBalancerTargetOptions) {
this.loadBalancers.push({
containerName: options.containerName,
containerPort: options.containerPort,
targetGroupArn: targetGroup.targetGroupArn
});
return { targetType: elbv2.TargetType.INSTANCE, portRange: /* whatever we used to return here */ };
}
} I'm using closures and object literals here to give the returned object access to a private member interface EcsLoadBalancerTarget extends IApplicationLoadBalancerTarget, INetworkLoadBalancerTarget {
}
class BaseService {
public EcsLoadBalancerTarget loadBalancerTarget(options: LoadBalancerTargetOptions) {
return new EcsLoadBalancerTarget();
}
private LoadBalancerTargetProps attachToELBv2(targetGroup: elbv2.ITargetGroup, options: LoadBalancerTargetOptions) {
loadBalancers.append({
containerName: options.containerName,
containerPort: options.containerPort,
targetGropuArn: targetGroup.targetGroupArn
});
return /* whatever we used to return here */;
}
private class EcsLoadBalancerTargetImpl implements EcsLoadBalancerTarget {
public LoadBalancerTargetProps attachToApplicationTargetGroup(targetGroup: ApplicationTargetGroup) {
return attachToELBv2(targetGroup, options);
}
public LoadBalancerTargetProps attachToApplicationTargetGroup(targetGroup: ApplicationTargetGroup) {
return attachToELBv2(targetGroup, options);
}
}
} |
@rix0rrr Sounds good to me! :eyesbleeding: |
69f2a58
to
2fc8ede
Compare
AWS CodeBuild CI Report
Powered by github-codebuild-logs, available on the AWS Serverless Application Repository |
I actually really like this approach; I think if you forward the existing 1 arg public attach functions on I think the remaining issue here would be validation, which could be performed during the overloaded attachment using an approach similar to the one I proposed in my PR. |
By the way, don't forget to declare the return value of your method to be the interface (instead of having the return type autoderived by TypeScript). If the thing you're returning is an literal or a private class (which it should be) jsii won't let it typecheck because the hidden type won't exist in client languages. |
I think this can be closed out now, since the PR for it has been merged. |
Multiple target group support: new registering target API and current API improve for ECS.
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license