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(core): generalization of dependencies #1583

Merged
merged 14 commits into from
Feb 4, 2019
Merged

Conversation

rix0rrr
Copy link
Contributor

@rix0rrr rix0rrr commented Jan 20, 2019

CONSTRUCTS NOW TAKE DEPENDENCIES ON OTHER CONSTRUCTS.

Before, only Resources could take dependencies, and they would depend
on IDependable. Instead, we generalize the concept of dependencies
from construct trees to other construct trees. The result will be that
any resource in the depending tree will depend on all resources in the
depended tree.

Lazy dependency objects have been removed; they are are now calculated
in the prepare() phase.

DEPENDENCIES ARE CROSS-STACK AWARE

If you take a dependency on a construct in another stack, the dependency
does not get rendered in the template, but is instead added as a
dependency between stacks.

BREAKING CHANGE: resource.addDependency() has been moved onto
ConstructNode. You now write resource.node.addDependency().

Fixes #1568, fixes #95.


Pull Request Checklist

  • Testing
    • Unit test added
    • CLI change?: manually run integration tests and paste output as a PR comment
    • cdk-init template change?: coordinated update of integration tests with team
  • Docs
    • jsdocs: All public APIs documented
    • README: README and/or documentation topic updated
  • Title and Description
    • Change type: title prefixed with fix, feat will appear in changelog
    • Title: use lower-case and doesn't end with a period
    • Breaking?: last paragraph: "BREAKING CHANGE: <describe what changed + link for details>"
    • Issues: Indicate issues fixed via: "Fixes #xxx" or "Closes #xxx"
  • Sensitive Modules (requires 2 PR approvers)
    • IAM Policy Document (in @aws-cdk/aws-iam)
    • EC2 Security Groups and ACLs (in @aws-cdk/aws-ec2)
    • Grant APIs (only if not based on official documentation with a reference)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license.

CONSTRUCTS NOW TAKE DEPENDENCIES ON OTHER CONSTRUCTS.

Before, only `Resource`s could take dependencies, and they would depend
on `IDependable`. Instead, we generalize the concept of dependencies
from construct trees to other construct trees. The result will be that
any resource in the depending tree will depend on all resources in the
depended tree.

Lazy dependency objects have been removed; they are are now calculated
in the prepare() phase.

DEPENDENCIES ARE CROSS-STACK AWARE

If you take a dependency on a construct in another stack, the dependency
does not get rendered in the template, but is instead added as a
dependency between stacks.

BREAKING CHANGE: `resource.addDependency()` has been moved onto
`ConstructNode`. You now write `resource.node.addDependency()`.
Copy link
Contributor

@RomainMuller RomainMuller left a comment

Choose a reason for hiding this comment

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

Looks nice :) Discussed some concerns about possible impact on change set execution performance (due to inability to parallelize stuff that could have been, only because DependsOn was over-eager), but I feel it's an okay tradeoff (don't expect this mechanism to be used "often" between large trees). Would nee ncie if we could find a strategy to make this "customizable" by the depended-on construct (so it could scope down to omit the resources that are not interesting to depend on, or where the dependencies are already implied transitively).

*/
protected readonly defaultPort: string;
public readonly loadBalancerConstructs = new Array<cdk.IConstruct>();
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably should prefix that name with an _ since the user won't need to interact with this directly. Also, probably should be called something like loadBalancerAttachments or something.

@rix0rrr
Copy link
Contributor Author

rix0rrr commented Jan 22, 2019

Note: for APIGW, the set of dependencies has increased; we can make those explicit again.

Also, we should probably find a good way of naming sets of constructs that are only exposed in order to take dependencies on them. OR find a way to expose those via types. The alternative is we reintroduce IDependable and make IConstruct implement that.

const ret = new Set<IConstruct>();
let node: ConstructNode | undefined = this;
while (node !== undefined) {
addSet(ret, node.dependencies);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would have been easier to read if you had just in-lined the loop here.

}
},

'cross-stack construct dependencies are not rendered but turned into stack dependencies'(test: Test) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Smooth!

@sam-goodwin sam-goodwin added the @aws-cdk/core Related to core CDK functionality label Jan 22, 2019
@@ -182,5 +168,7 @@ class LatestDeploymentResource extends CfnDeployment {

this.lazyLogicalId = this.originalLogicalId + md5.digest("hex");
}

super.prepare();
Copy link
Contributor

Choose a reason for hiding this comment

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

See my note about maybe moving resource dependency calculation to the stack. If you decide to leave Rhiannon here, then add a wee comment on the super call to hint why it Ian needed

* so we add an ordering dependency on the Target Group being associated with
* a Load Balancer.
*/
public dependOnLoadBalancerAttachment(targetGroup: elbv2.ITargetGroup) {
Copy link
Contributor

Choose a reason for hiding this comment

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

“AddTargetGroup” maybe?

this.natGatewayByAZ[sub.availabilityZone] = sub.addNatGateway();
const gateway = sub.addNatGateway();
this.natGatewayByAZ[sub.availabilityZone] = gateway.natGatewayId;
this.natDependencies.push(gateway);
Copy link
Contributor

Choose a reason for hiding this comment

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

Wouldn’t it make more sense to just expose this as “natGateways” ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe. But I don't want to overpromise; they were there to be able to take a dependency on, nothing more.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds like a legitimate API to expose the NAT gateways and internet gateways within the VPC, no?

*/
public readonly dependencyElements: IDependable[] = [];
Copy link
Contributor

Choose a reason for hiding this comment

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

Just expose the internet gateway?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's actually not the gateway, it's the attachment of the gateway to the VPC.

@@ -33,14 +33,24 @@ export class Stack extends Construct {
* @returns The Stack object (throws if the node is not part of a Stack-rooted tree)
*/
public static find(scope: IConstruct): Stack {
const curr = Stack.tryFind(scope);
Copy link
Contributor

Choose a reason for hiding this comment

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

That’s an unsafe breaking change... maybe we can’t safely
Make it without checking all call sites...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why is this a breaking change? find() used to throw and still throws, tryFind() does not throw.

/**
* Return all dependencies registered on this node and its ancestors.
*/
public myDependencies(): IConstruct[] {
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder the api can be more explicit and/or rich to indicate that it returns both explicit and implicit dependencies (maybe “findDependencies(includeImpicit = true)”

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can, but there's no use for it (at least today). Future proofing?

Copy link
Contributor

Choose a reason for hiding this comment

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

I just don't like the fact that it returns implicit dependencies without that reflecting in the API. It could be findImplicitAndExplicitDependencies() but findDependencies({ implicit: true }) feels a bit less ugly.

...I am not sure about the "implicit" terminology. Maybe "inherited" or "derived" or something like that...

protected prepare() {
super.prepare();

// As an optimization, do the stack dependencies first on the parents
Copy link
Contributor

Choose a reason for hiding this comment

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

Would it make more sense perhaps to do this globally at the stack instead of distribute to each resource?

@rix0rrr
Copy link
Contributor Author

rix0rrr commented Jan 23, 2019

I'm having one more issue with this PR at the moment.

Representing a "bundle of dependencies"

Relevant in the following cases:

  • Any instance that needs an EIP is dependent on the
    AWS::VPC2::VPCGatewayAttachment object having been created. This
    is basically any instance in a Public Subnet, such as ASGs, ELBs,
    etc.
  • RDS instances have a requirement on network connectivity, which translates
    into either the IGW attachment (public subnets) or a Route to a NAT gateway
    having been created (private subnets). Note: right now we're modeling this
    as a dependency on the NAT gateway itself, but I'm fairly sure that's not
    correct; it probably just happens that the race condition window is so small
    that we haven't seen it cause problems yet.
  • Some resources that only get a TargetGroup as argument (in EBLv2) need a
    LoadBalancer associated with that particular TargetGroup. This includes ECS
    Services and TargetTrackingScalingPolicys that scale on ALB request count
    (both in EC2 AutoScaling and in Application AutoScaling). Again, in this
    case it's not the LoadBalancer itself that's interesting or needs to be
    depended upon, but the association object, which in this case is either
    a Listener or a ListenerRule object.

Right now, I'm representing this as an IConstruct[] (so you have to do
construct.node.addDependency(...targetGroup.loadBalancerDependencies)), but I actually
don't like that very much. It overpromises on the API; the only ability you
should get is being able to take a dependency on these things.

I'm thinking of reintroducing IDependable and implementing it like this
instead:

interface IDependable {
  readonly dependencyElements: IConstruct[];
}

interface Construct extends IDependable {
  public readonly dependencyElements = [this];
}

class DependableSet implements IDependable {
  public add(element: IConstruct) { ... }
}

This will allow naive use in most cases:
construct.node.addDependency(otherConstruct), but also allow explicit
control over dependency sets when it makes sense. For completeness

// OLD (IConstruct[])
construct.node.addDependency(otherConstruct);
construct.node.addDependency(...targetGroup.loadBalancerDependencies);

// NEW (IDependable)
construct.node.addDependency(otherConstruct);
construct.node.addDependency(targetGroup.loadBalancerAttached);

For the VPC case, instead of exposing both the IGW dependency objects and the NAT
gateway dependency objects on the VPC object itself, and having consumers
grub around those, I think it makes more sense to do the following:

interface IVpcSubnet {
  /**
   * Returns the right set of dependencies depending on the subnet type
   */
  readonly internetConnectivityDependency: IDependable;
}

@eladb
Copy link
Contributor

eladb commented Jan 23, 2019

@rix0rrr I like the IDependable approach. Looks clean and I love the fact that by default people don't have to think about it but it gives them the ability to customize!

@eladb
Copy link
Contributor

eladb commented Jan 23, 2019

Just one question: why do we need DependableSet?

@rix0rrr
Copy link
Contributor Author

rix0rrr commented Jan 23, 2019

It would be an IDependable for the cases where you don't want just all constructs rooted under this one, so you just customize a literal list.

@RomainMuller
Copy link
Contributor

This blocks #1633

@rix0rrr rix0rrr merged commit 53e6825 into master Feb 4, 2019
@rix0rrr rix0rrr deleted the huijbers/construct-deps branch February 4, 2019 13:46
moofish32 pushed a commit to moofish32/aws-cdk that referenced this pull request Feb 5, 2019
Constructs can now take a dependency on any other construct.

Before, only `Resource`s could take dependencies, and they would depend
on `IDependable` which had to be implemented explicitly. In this change
we generalize the concept of dependencies from construct trees to other
construct trees; all constructs now take dependencies and also implement
`IDependable`.  The semantics are that any resource in the depending
tree will depend on all resources in the depended tree.

Dependencies are cross-stack aware

If you take a dependency on a construct in another stack, the dependency
does not get rendered in the template, but is instead added as a
dependency between stacks.

Fixes aws#1568, fixes aws#95.

BREAKING CHANGE: `resource.addDependency()` has been moved onto
`ConstructNode`. You now write `resource.node.addDependency()`. VPC's
`internetDependency` has been moved to the subnets as
`internetConnectivityEstablished`. Target Group's
`loadBalancerAssociationDependencies` has been renamed to
`loadBalancerAttached`.
@NGL321 NGL321 added the contribution/core This is a PR that came from AWS. label Sep 27, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
@aws-cdk/core Related to core CDK functionality contribution/core This is a PR that came from AWS.
Projects
None yet
5 participants