-
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
(BREAKING) Enhancement - Custom VPC Subnets #250
Conversation
initial bare bones working vpc
@PaulMaddox can you please take a look at this? |
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 totally looks like the right direction. I am not an expert in VPC networking (hence requested @PaulMaddox to take a look), but in general I like the concept and the API seems clean.
* InvalidSubnetCidrError is thrown when attempting to split a CIDR range into more | ||
* subnets than it has IP space for. | ||
*/ | ||
export class InvalidSubnetCidrError extends Error { |
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.
We have an issue with our cross-language binding tech (jsii) that doesn't allow you to subclass Error
. I would just use the message content for test assertions:
assert.throws(() => doSomething(), /matchError/);
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.
@eladb should I still create custom Error classes and then only test for the matching text? Or are you saying don't even create subclasses of Error?
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.
Just use Error
. I know, it's not pretty.
// https://stackoverflow.com/questions/31626231/custom-error-class-in-typescript | ||
Object.setPrototypeOf(this, InvalidSubnetCountError.prototype); | ||
} | ||
} |
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.
Fix indent to 4 spaces
public static validIp(ipAddress: string): boolean { | ||
return ipAddress.split('.').map( | ||
(octet: string) => (parseInt(octet, 10) >= 0 && parseInt(octet, 10) <= 255)). | ||
reduce((valid: boolean, value: boolean) => (valid && 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.
Nit: I think "find" might be more readable than map/reduce, and you should also verify the address includes exactly 4 octets:
return ipAddress.split('.').map(parseInt).find(o >= 0 && o <= 255).length === 4
Alternatively, find a well-tested regex...
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'll move to your find solution. The regex googling had mixed reviews.
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 moved to filter
because find seemed to return only the first match. Still better than reduce since it's not really standard sum type of behavior?
} | ||
|
||
} | ||
|
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.
Eventually make sure to add jsdocs to all exported entities
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
/** | ||
* Define subnets that will be built by default per AZ (maxAZs). If left | ||
* empty the default VPC configuration will be used | ||
* |
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.
Use "@default" to describe the default behavior
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.
Also, what is the default VPC configuration?
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.
currently I left the default as is. Likely I'll keep this end state the same, but replace reduce the split cidr logic.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
* Define subnets that will be built by default per AZ (maxAZs). If left | ||
* empty the default VPC configuration will be used | ||
* | ||
* The subnets are contructed in the context of the VPC so you only need |
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.
contructed => constructed
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
* across 3 AZs then maxAZs = 3 and provide the following: | ||
* subnets: [ | ||
* { | ||
* cidrMask: 24; |
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.
"," instead of ";" between hash elements 😄
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
const networkBuilder = new NetworkBuilder(cidr); | ||
this.createCustomSubnets(networkBuilder, subnets, zones); | ||
} else { | ||
// Split the CIDR range into each availablity zone |
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.
Why can't we use NetworkBuilder for this use case as well? If I understand correctly, should be possible... Less branching
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 was just going for minimal change, but I can easily do this.
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 would like to see that if subnets
is not supplied by the user, it is generated so that it would lead the same effective behavior here without the if
. We then make the subnet configuration required. Let's minimize the amount of code paths that are taken.
When you do so, please run the integ tests to verify that the generated template is still the same.
return subnets.map((subnet) => (subnet.cidr)); | ||
} | ||
|
||
public getSubnets(): 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.
Use getter: public get subnets(): string[]
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
/** | ||
* Specify configuration parameters for a VPC to be built | ||
*/ | ||
export interface VpcSubnetBuilderProps { |
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.
Use jsdocs (/** */
) for all properties and explicitly call out @default
s
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
const networkBuilder = new NetworkBuilder(cidr); | ||
this.createCustomSubnets(networkBuilder, subnets, zones); | ||
} else { | ||
// Split the CIDR range into each availablity zone |
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 would like to see that if subnets
is not supplied by the user, it is generated so that it would lead the same effective behavior here without the if
. We then make the subnet configuration required. Let's minimize the amount of code paths that are taken.
When you do so, please run the integ tests to verify that the generated template is still the same.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
@@ -58,6 +59,34 @@ export interface VpcNetworkProps { | |||
* @default All AZs in the region | |||
*/ | |||
maxAZs?: number; | |||
|
|||
/** | |||
* Define subnets that will be built per AZ (maxAZs), if numAZs is |
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.
Simplify the wording a bit here. Something like:
Configure the subnets to build for each AZ
Should be sufficient in the first line. More elaboration can be done in the body of the docstring.
I think the maxAZs
setting doesn't need to be invoked, that seems logical enough.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
* @default the VPC CIDR will be evenly divided between 1 public and 1 | ||
* private subnet per AZ | ||
*/ | ||
subnets?: VpcSubnetBuilderProps[]; |
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.
Does this name make sense? subnets
to me feels like the constructed resources, which this isn't about. This is specifying construction/configuration parameters per AZs.
How about subnetConfiguration
? Please change the name of the type to match.
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.
subnetConfiguration
is much better.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
*/ | ||
public readonly internalSubnets: VpcSubnetRef[] = []; | ||
|
||
public readonly natGatewayByAZ: Obj<Token> = {}; |
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.
Should this be public? We don't like exposing Token
, so either define an appropriate subclass for Token
or make this private
.
In fact, it seems to be used inside a single method only. Why not make it a local variable there?
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.
Agree will move to local
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
export enum SubnetType { | ||
|
||
/** | ||
* Outbound traffic is not routed. This can be good for subnets with RDS or |
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.
Split doc text into caption line/body.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
Public = 2, | ||
|
||
/** | ||
* Outbound traffic will be routed via a NAT Gateways preference being in |
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.
Split doc text into caption line/body.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
// defaults to false if true will map public IPs on launch | ||
mapPublicIpOnLaunch?: boolean; | ||
// number of AZs to build this subnet in | ||
numAZs?: number; |
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'm very curious about the interaction between maxAZs
and numAZs
, and what the use case is here.
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.
see comment below regarding constrained IP space environments. I might be missing some checks to ensure numAZs <= maxAZs
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 now make this check and error if applicable
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
*/ | ||
export interface VpcSubnetBuilderProps { | ||
// the cidr mask value from 16-28 | ||
cidrMask: number; |
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.
Could we make this optional? For example, by splitting unclaimed address space evenly over the subnets?
I'm thinking of the following calculation:
space_per_az = total_space / total_azs
unclaimed_space = space_per_az - sum(subnet.cidrMask for all nets with cidrMask configured)
defaultCidrMask = (space_per_az - unclaimed_space) / count(nets without cidrMask)
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.
Let me give this a try. I can't think of a reason it will not work, but hard to play out all the situations.
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 is now included
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
name: string; | ||
// if true will place a NAT Gateway in this subnet, subnetType must be Public | ||
natGateway?: boolean; | ||
// defaults to false if true will map public IPs on launch |
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.
Is that true? I believe it depends on the subnet type, certainly for public subnets it defaults to true.
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.
Bad docs! In public I defaulted to true
in private I defaulted to false
. https://github.com/awslabs/aws-cdk/pull/250/files#diff-ee81a78878f10abd0d632c84312bcd8dR324
I'll update the docs to match. If you think I should change that behavior let me know.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
|
||
/** | ||
* Outbound traffic will be routed via a NAT Gateways preference being in | ||
* the same AZ, but if not available will use another AZ. This is common for |
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 remark about "cost conscious", is that about private subnets in general or just the case where there might be fewer NAT gateways than subnets?
Is that also the purpose of numAZs
, to have fewer public subnets than private subnets?
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.
Yes. We create 90 day use accounts for experimentation. In these accounts we commonly use only one NAT GW. I need to test this feature, but you are inferring the right intent. Should I change wording to be more clear?
The purpose of numAZs is the rare case when you don't want the subnet in all AZs. This is where I get a little skeptical but the use case is commonly data replication over a VPN where the IP space is constrained. I don't see this very often, but I thought we could easily support. If we think this is going to add too much complexity you can probably talk me out of this.
I will have some time today/tomorrow to look through this |
Also, thanks for contributing! 🎉 |
|
||
} | ||
|
||
export class NetworkBuilder { |
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 should have a documentation comment describing the functionality, and a usage example.
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.
yes anything exported will get full docs. Hopefully I get a complete draft today/tonight
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.
Awesome. Thanks for contributing too!
…; removed public NatGatewayByAz to local variable
outboundTraffic: OutboundTrafficMode.FromPublicAndPrivateSubnets, | ||
subnets: [ | ||
{ | ||
cidrMask: 24, |
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.
Is specifying the cidrMask mandatory here? I think it is looking at the code. Do you think it would be worth making it an optional, and automatically calculate it if not provided (we'd have to document this behaviour).
I could see a scenario where I wanted to provide a custom subnet configuration, but not care too much about the subnet sizes. Having it default to automatically calculating would save customers having to do the CIDR math themselves.
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 now works with even splits for subnets.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
/** | ||
* The type of Subnet | ||
*/ | ||
export enum SubnetType { |
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 like the concept of defining the network access (and whether NAT gateways etc are required) at a subnet level rather than a VPC level. I do feel that as it is, it's slightly confusing to be providing this at a VpcNetwork level (with OutboundTrafficMode
) and at a subnet level (with SubnetType
).
I realise you've done it this way to minimise breaking changes on the VpcNetwork construct, however it would be really nice if we:
- If no subnet configuration is provided, use a default one that specifies public and private in each AZ, with outbound communication from private via NAT Gateways
- Allow users to override by providing a subnetConfiguration
- Auto calculate the outbound network mode and requirements (NAT Gateway etc) based on all of the subnets
- Remove the concept of
OutboundTrafficMode
I think this would be a more coherent API for customers. @rix0rrr what do you think about this (would be a breaking change). From what I can see no other part of the CDK uses this OutboundTrafficMode
API today.
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 this would simplify the construct implementation too - You could just define a const DefaultSubnetConfiguration = ...
, and use that if none is provided. Especially if we make cidrMask
an optional that automatically calculates if not provided (as it wouldn't be in the DefaultSubnetConfiguration).
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.
Breaking changes are fine at this stage of the project. We want the best APIs for 1.0.0. Just make sure to document in the commit/PR message under "BREAKING CHANGE" (see CONTRIBUTING guide).
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 like the proposal for the breaking change.
I honestly struggled with this as well. I also flip flopped on the implementation between which one should win; right now it's actually not well enforced and I agree confusing. I'll wait for more consensus before making a change.
Didn't see @eladb comment. I'll work up the change.
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.
Something else I would like to bring up:
Does the terminology still make sense?
We now have PUBLIC (clear enough), and both PRIVATE and INTERNAL.
I'm fine if these terms are commonly used and well-understood, but the distinction between private and internal isn't obvious to me from the term.
I guess private is standardized and well-understood, so that means we should find a more descriptive term for internal.
- very_private?
- unroutable
- air_gapped (not technically correct but evocative)
- something else?
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.
isolated
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.
YES! private
and isolated
sound good
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.
First off you're my hero.
This change makes a ton of sense - and I'm thrilled about it.
Most of my comments are very minor - so don't feel like they're required - most are just small things :)
🎉
} | ||
|
||
public addSubnets(mask: number, count?: number): string[] { | ||
if (mask < 16 || mask > 28) { |
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 am being insanely pedantic - but the /16
is in fact a soft limit 😬
I don't think it's something we need to change right now - but just something we need to think about how we want to handle in the future (more services have limits like this, where like 95% of people will hit the default limits, but not everyone)
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.
agree - I was hoping that was out of scope for this first round since /16 was already in here before me.
This brings up a question I was wondering about: https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html#VPC_Sizing. The recommended is use Private space and play by the rules, but technically I could deploy things that are not very useful. Where should we draw the line between good practice and rule here? Not sure this PR is the best place for a general discussion, but I'm definitely not sure how to answer this question tonight.
return subnets.map((subnet) => (subnet.cidr)); | ||
} | ||
|
||
public get subnets(): 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.
I wonder if subnets
is the best name for this - given that we map sunbetCidrs
to get only the cidr
property?
(I'm not a stickler for names - so I'll defer to how you feel about it)
|
||
export class CidrBlock { | ||
|
||
public static calculateNetmask(mask: number): 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.
Nit: Is public
the right visibility? Seems like we're using this only in the constructor?
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.
good question. I made this public because of the export
. I only export
to test, perhaps that is a lack of Typescript testing. However, once somebody else could see this class then I would expect this static method to exist like it does in many other languages (e.g. Golang https://golang.org/pkg/net/#CIDRMask). I'm happy to make a change if there is a strong opinion in the other direction, but that's how I got here.
|
||
public maxIp(): string { | ||
const minIpNum = NetworkUtils.ipToNum(this.minIp()); | ||
return NetworkUtils.numToIp(minIpNum + 2 ** (32 - this.mask) - 1); |
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 just a comment explaining this line will help future CDKers 😃
const ipOct = this.cidr.split('/')[0].split('.'); | ||
// tslint:disable:no-bitwise | ||
return netmaskOct.map( | ||
(maskOct, index) => parseInt(maskOct, 10) & parseInt(ipOct[index], 10)).join('.'); |
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.
Likewise.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
switch (subnet.subnetType) { | ||
case SubnetType.Public: | ||
const publicSubnet = new VpcPublicSubnet(this, name, { | ||
mapPublicIpOnLaunch: subnet.mapPublicIpOnLaunch || true, |
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'm really bad at type script - but does || true
mean always true or does it mean "true unless the other value is set"?
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.
Unrelated : I'm (mildy) curious why VpcPublicSubnet
would take mapPublicIpOnLaunch
- when is that not the case?
(That's probably my fault, I'm just thinking aloud)
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.
My only legit use case is a bastion host. The public IPs that get put onto these images cannot be found in any CloudTrail. In some sensitive environments the non-traceable IP that is mapped causes audit trail complexities. Beyond that use case it is very odd to not want a public IP on a host in public subnet, but I suspect there are some obscure use cases beyond mine.
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'm really bad at type script - but does || true mean always true or does it mean "true unless the other value is set"?
You're very right, this is a bug! It indeed means always true!
The correct way to write it would have been:
subnet.mapPublicIpOnLaunch !== undefined ? subnet.mapPublicIpOnLaunch : true
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
break; | ||
case SubnetType.Private: | ||
const privateSubnet = new VpcPrivateSubnet(this, name, { | ||
mapPublicIpOnLaunch: subnet.mapPublicIpOnLaunch || false, |
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.
Interesting - what's the case for a subnet having this set to true? Is there ever a case where we'd do this?
(I could totally be wrong - it's been a day and my brain is 💀 )
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 one I have no use case for and I've never seen it done. However, I'm not a networking expert.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
this.privateSubnets.push(privateSubnet); | ||
break; | ||
case SubnetType.Internal: | ||
const internalSubnet = new VpcPrivateSubnet(this, name, { |
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.
new VpcActuallyPrivateSubnet
🤣
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
break; | ||
case SubnetType.Internal: | ||
const internalSubnet = new VpcPrivateSubnet(this, name, { | ||
mapPublicIpOnLaunch: subnet.mapPublicIpOnLaunch || false, |
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 reading this - I wonder do we need the mapPublicIpOnLaunch
field - if based on the subnet type we're making these defaults.
Why would we do a InternalSubnet
over a PrivateSubnet
with that set to true.
Just seems like we could drop the field maybe?
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'm guessing behind the scenes there may be some code that was just easier if the data structures were the same. However, I'll leave this up to the team to decide (not sure if that means @eladb or a polly vote in slack -- I'm pretty new here).
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.
Definitely it's easier if the internal data structures are the same, but we could and should still catch nonsensical configuration at the edges.
So the only use case we've seen so far is setting this value to false
in a PUBLIC subnet, no? Which might make sense if people both:
- Want a bastion host; and
- Are tight on address space (because otherwise you would launch the private hosts in an INTERNAL or PRIVATE subnet).
But if you're tight on address space you could also just make the subnets smaller... so doesn't it make more sense to always leave this to the behavior that's natural for the subnet type?
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
let ngwId = natGatewayByAZ[privateSubnet.availabilityZone]; | ||
if (ngwId === undefined) { | ||
const ngwArray = Array.from(Object.values(natGatewayByAZ)); | ||
ngwId = ngwArray[i % ngwArray.length]; |
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 helpful comment line would be AMAZING 😺
Let me know if you need help with any of the requested changes! 💯 |
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.
@rix0rrr -- I am now failing the integ tests as you thought. Since this has moved to full on breaking change, I assume I can modify those tests or is there something special in there? I didn't quite get to looking at them yet.
@eladb - I have a conflict in here and you will be happy to know I can resolve this! Do we have any standards about using merge vs rebase for resolution? I typically rebase but because this PR is long and full of comments I don't want to rebase until the end. If we merge then I'm really hoping I get to squash at the end and clean up my commits -- or I can just create a new branch and PR with a link. End goal is I don't want all my garbage fix commits showing up and I want to keep whatever git practice you guys are using.
} | ||
|
||
public addSubnets(mask: number, count?: number): string[] { | ||
if (mask < 16 || mask > 28) { |
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.
agree - I was hoping that was out of scope for this first round since /16 was already in here before me.
This brings up a question I was wondering about: https://docs.aws.amazon.com/AmazonVPC/latest/UserGuide/VPC_Subnets.html#VPC_Sizing. The recommended is use Private space and play by the rules, but technically I could deploy things that are not very useful. Where should we draw the line between good practice and rule here? Not sure this PR is the best place for a general discussion, but I'm definitely not sure how to answer this question tonight.
|
||
export class CidrBlock { | ||
|
||
public static calculateNetmask(mask: number): 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.
good question. I made this public because of the export
. I only export
to test, perhaps that is a lack of Typescript testing. However, once somebody else could see this class then I would expect this static method to exist like it does in many other languages (e.g. Golang https://golang.org/pkg/net/#CIDRMask). I'm happy to make a change if there is a strong opinion in the other direction, but that's how I got here.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
switch (subnet.subnetType) { | ||
case SubnetType.Public: | ||
const publicSubnet = new VpcPublicSubnet(this, name, { | ||
mapPublicIpOnLaunch: subnet.mapPublicIpOnLaunch || true, |
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.
My only legit use case is a bastion host. The public IPs that get put onto these images cannot be found in any CloudTrail. In some sensitive environments the non-traceable IP that is mapped causes audit trail complexities. Beyond that use case it is very odd to not want a public IP on a host in public subnet, but I suspect there are some obscure use cases beyond mine.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
break; | ||
case SubnetType.Private: | ||
const privateSubnet = new VpcPrivateSubnet(this, name, { | ||
mapPublicIpOnLaunch: subnet.mapPublicIpOnLaunch || false, |
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 one I have no use case for and I've never seen it done. However, I'm not a networking expert.
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
break; | ||
case SubnetType.Internal: | ||
const internalSubnet = new VpcPrivateSubnet(this, name, { | ||
mapPublicIpOnLaunch: subnet.mapPublicIpOnLaunch || false, |
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'm guessing behind the scenes there may be some code that was just easier if the data structures were the same. However, I'll leave this up to the team to decide (not sure if that means @eladb or a polly vote in slack -- I'm pretty new here).
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
*/ | ||
export interface VpcSubnetBuilderProps { | ||
// the cidr mask value from 16-28 | ||
cidrMask: number; |
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 is now included
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
// defaults to false if true will map public IPs on launch | ||
mapPublicIpOnLaunch?: boolean; | ||
// number of AZs to build this subnet in | ||
numAZs?: number; |
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 now make this check and error if applicable
outboundTraffic: OutboundTrafficMode.FromPublicAndPrivateSubnets, | ||
subnets: [ | ||
{ | ||
cidrMask: 24, |
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 now works with even splits for subnets.
Re: failing integ tests. Depends on what the break is. If it's source API, totally fine, but I would hope that the same Constructs get generated based on the same conceptual configuration as before. Because if we change those, people downstream consuming the change will have their VPC torn down and recreated, which is probably not going to go down well. |
Mike (@moofish32), I would like to pull your changes to look at them in my source tree. Could you give me read access to your clone? |
@rix0rrr, you can checkout the PR without having access to @moofish32's private repo by adding the following to your ~/.git/config:
then do:
|
@moofish32, after fixing some trivialities (API changes and linter errors), there are two problems:
|
packages/@aws-cdk/ec2/lib/vpc.ts
Outdated
/** | ||
* The CIDR Mask or the number of leading 1 bits in the routing mask | ||
* | ||
* Valid values are 16 - 28 |
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.
@default Available space is automatically divided over maskless subnets
Wait, I get confused. In the first sentence, I thought we were talking about But in any case, if they did NOT want a public IP in a public subnet, wouldn't the solution then be to launch the instances in a private subnet? I still have a hard time seeing the use case. |
Oh I see. Yes, that sounds fair. It seems like a feature request that we can add later on. Or here's another idea: instead of saying "MAX number of nat gateways", rename to Then we have a forwards compatible API that makes sense now (lower the amount of gateways) and still makes sense if we want to raise the amount of gateways. We can punt on the actual implementation and do that at a later time. |
I would love that. In fact, does that not get rid of the need to have the parameter at all? If you have a private subnet, you MUST want NAT routing. Otherwise, you would have used an internal subnet, no? |
Speaking of How do you feel about that? |
I'm ready to approve this, modulo some small issues:
|
…licIp, and internal -> isolated
… nextAvailableIp, adding in nextBlock() method
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.
@rix0rrr -- I think I have addressed all your comments and the new ones that came after I started my review here. There were a couple points of confusion but hopefully we can clear those up.
@@ -86,4 +86,237 @@ export class NetworkUtils { | |||
|
|||
} | |||
|
|||
public static validIp(ipAddress: string): boolean { | |||
return ipAddress.split('.').map((octet: string) => parseInt(octet, 10)). |
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.
done - added tests to verify
filter((octet: number) => octet >= 0 && octet <= 255).length === 4; | ||
} | ||
/** | ||
* |
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.
fixed
} | ||
|
||
/** | ||
* |
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.
fixed
export class NetworkBuilder { | ||
|
||
/** | ||
* the CIDR range used when creating the network |
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.
fixed
/** | ||
* the list of CIDR blocks for subnets within this network | ||
*/ | ||
protected subnetCidrs: CidrBlock[]; |
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.
fixed used private readonly subnetCidrs: CidrBlock[] = [];
NetworkUtils.ipToNum(this.maxIpConsumed); | ||
const ipsPerSubnet: number = Math.floor(remaining / subnetCount); | ||
return 32 - Math.floor(Math.log2(ipsPerSubnet)); | ||
// return this.addSubnets(mask, subnetCount); |
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.
yep, gone.
/** | ||
* return the CIDR notation strings for all subnets in the network | ||
*/ | ||
public get subnets(): 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.
done
} | ||
|
||
/** | ||
* Creates a CIDR Block |
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.
done. I did not implement split()
I'm not positive what you were suggesting here.
const subnets: CidrBlock[] = []; | ||
count = count || 1; | ||
for (let i = 0; i < count; i ++) { | ||
const subnet: CidrBlock = CidrBlock.fromOffsetIp(this.maxIpConsumed, mask); |
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.
done, went with nextBlock()
* CidrBlock.fromOffsetIp('10.0.0.15', 24) returns '10.0.1.0/24' | ||
*/ | ||
public static fromOffsetIp(ipAddress: string, mask: number): CidrBlock { | ||
const initialCidr = new CidrBlock(`${ipAddress}/${mask}`); |
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.
ok -- I think this is done. I'm not sure Typescript and I agree on constructor overloading design, but perhaps there is a simpler way and I didn't know it?
I've removed this. There is still a corner case where a team may want to prohibit public IPs on instances that are not EIPs. However, it is likely obscure and could be addressed if CloudTrail simply logged these IPs. Let's see if we get immediate feature requests instead of complicating the code, as you are you suggesting.
This is implemented
This is implemented
I've implemented
I took a shot at cleaning this up. Somethings did get much simpler, the constructor was not one. |
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 looks great! @rix0rrr any other comments?
Nope--the API is in great shape. I think we're ready to merge this. I'll approve it. Don't know if Mike will have permissions to merge (if so, squash merge and a critical look at the commit message please, be sure to note that this is a breaking change). If not, either Elad can merge tomorrow or I will do it. |
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 Mike, thanks for the effort!
I don't see the ability to squash and merge. @eladb if you want I can take a pass at the commit message since I doubt anybody else is going to make sense of the ones in there? |
@eladb @rix0rrr -- my quick summary/commit message proposal Adding the ability to customize VpcNetwork configurations:
|
@eladb -- I'll merge in master and resolve these conflicts. |
Thanks, looking forward. I will pull then. |
@moofish32 ready for merge? |
Yes. Unless anybody sees another comment/issue 👍 |
I have created an initial approach/concept to enable a more customizable
subnet design for VPCs. I have tried to make minimal changes (leaving in
tact the current default system).
By submitting this pull request, I confirm that my contribution is made under
the terms of the beta license.