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

fix(ec2-alpha): readme updates, new unit tests, logic update #33086

Merged
merged 4 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions packages/@aws-cdk/aws-ec2-alpha/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ new VpcV2(this, 'Vpc', {

`SubnetV2` is a re-write of the [`ec2.Subnet`](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ec2.Subnet.html) construct.
This new construct can be used to add subnets to a `VpcV2` instance:
Note: When defining a subnet with `SubnetV2`, CDK automatically creates a new route table, unless a route table is explicitly provided as an input to the construct.

```ts

Expand All @@ -66,6 +67,14 @@ By default `VpcV2` uses `10.0.0.0/16` as the primary CIDR if none is defined.
Additional CIDRs can be adding to the VPC via the `secondaryAddressBlocks` prop.
The following example illustrates the different options of defining the address blocks:

Note: There’s currently an issue with IPAM pool deletion that may affect the `cdk --destroy` command. This is because IPAM takes time to detect when the IP address pool has been deallocated after the VPC is deleted. The current workaround is to wait until the IP address is fully deallocated from the pool before retrying the deletion. Below command can be used to check allocations for a pool using CLI

```shell
aws ec2 get-ipam-pool-allocations --ipam-pool-id <ipam-pool-id>
```

Ref: https://docs.aws.amazon.com/cli/latest/reference/ec2/get-ipam-pool-allocations.html

```ts

const stack = new Stack();
Expand Down Expand Up @@ -527,6 +536,7 @@ For more information, see [Enable VPC internet access using internet gateways](h

You can add an internet gateway to a VPC using `addInternetGateway` method. By default, this method creates a route in all Public Subnets with outbound destination set to `0.0.0.0` for IPv4 and `::0` for IPv6 enabled VPC.
Instead of using the default settings, you can configure a custom destination range by providing an optional input `destination` to the method.
In addition to the custom IP range, you can also choose to filter subnets where default routes should be created.

The code example below shows how to add an internet gateway with a custom outbound destination IP range:

Expand All @@ -546,6 +556,34 @@ myVpc.addInternetGateway({
});
```

The following code examples demonstrates how to add an internet gateway with a custom outbound destination IP range for specific subnets:

```ts
const stack = new Stack();
const myVpc = new VpcV2(this, 'Vpc');

const mySubnet = new SubnetV2(this, 'Subnet', {
vpc: myVpc,
availabilityZone: 'eu-west-2a',
ipv4CidrBlock: new IpCidr('10.0.0.0/24'),
subnetType: SubnetType.PUBLIC });

myVpc.addInternetGateway({
ipv4Destination: '192.168.0.0/16',
subnets: [mySubnet],
});
```

```ts
const stack = new Stack();
const myVpc = new VpcV2(this, 'Vpc');

myVpc.addInternetGateway({
ipv4Destination: '192.168.0.0/16',
subnets: [{subnetType: SubnetType.PRIVATE_WITH_EGRESS}],
});
```

## Importing an existing VPC

You can import an existing VPC and its subnets using the `VpcV2.fromVpcV2Attributes()` method or an individual subnet using `SubnetV2.fromSubnetV2Attributes()` method.
Expand Down
6 changes: 0 additions & 6 deletions packages/@aws-cdk/aws-ec2-alpha/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config');
module.exports = {
...baseConfig,
coverageThreshold: {
global: {
statements: 75,
branches: 63,
},
},
};;
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-ec2-alpha/lib/ipam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ export class Ipam extends Resource {
if (props?.ipamName) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Is it possible for ipamName to be a token?

Tags.of(this).add(NAME_TAG, props.ipamName);
}
if (!props?.operatingRegion && !Stack.of(this).region) {
if (props?.operatingRegion && (props.operatingRegion.length === 0)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: The operatingRegion is a list so it should be named operatingRegions.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

addressed in latest rev but this would be breaking change which should be okay for alpha, added in PR description

BREAKING CHANGE: operatingRegion property under IPAM class is now renamed to operatingRegions.

throw new Error('Please provide at least one operating region');
}

Expand Down
12 changes: 6 additions & 6 deletions packages/@aws-cdk/aws-ec2-alpha/lib/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,12 +605,8 @@ export class RouteTargetType {
readonly endpoint?: IVpcEndpoint;

constructor(props: RouteTargetProps) {
if ((props.gateway && props.endpoint) || (!props.gateway && !props.endpoint)) {
throw new Error('Exactly one of `gateway` or `endpoint` must be specified.');
Copy link
Contributor

Choose a reason for hiding this comment

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

I see this is moved into the Route constructor. Curious about the reasoning

Copy link
Contributor Author

Choose a reason for hiding this comment

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

While adding unit tests, stack was not throwing expected error while using addRoute method with both endpoint and target as an input, so moved it from RouteTargetType class to Route
addRoute

} else {
this.gateway = props.gateway;
this.endpoint = props.endpoint;
}
this.gateway = props.gateway;
this.endpoint = props.endpoint;
}
}

Expand Down Expand Up @@ -729,6 +725,10 @@ export class Route extends Resource implements IRouteV2 {
if (this.target.gateway?.routerType === RouterType.EGRESS_ONLY_INTERNET_GATEWAY && isDestinationIpv4) {
throw new Error('Egress only internet gateway does not support IPv4 routing');
}

if ((props.target.gateway && props.target.endpoint) || (!props.target.gateway && !props.target.endpoint)) {
throw new Error('Exactly one of `gateway` or `endpoint` must be specified.');
}
this.targetRouterType = this.target.gateway ? this.target.gateway.routerType : RouterType.VPC_ENDPOINT;
// Gateway generates route automatically via its RouteTable, thus we don't need to generate the resource for it
if (!(this.target.endpoint instanceof GatewayVpcEndpoint)) {
Expand Down
38 changes: 30 additions & 8 deletions packages/@aws-cdk/aws-ec2-alpha/lib/vpc-v2-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ export interface InternetGatewayOptions{
* @default - provisioned without a resource name
*/
readonly internetGatewayName?: string;

/**
* List of subnets where route to IGW will be added
*
* @default - route created for all subnets with Type `SubnetType.Public`
*/
readonly subnets?: SubnetSelection[];
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: Naming this subnetSelections seems better so users are not confused that it may be of the type SubnetV2[].

Copy link
Contributor Author

Choose a reason for hiding this comment

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

actually, tried to keep it consistent per the functions in main lib with the current options in main lib for adding interfaces and endpoints, https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk-lib/aws-ec2/lib/vpc-endpoint.ts#L110 , where mySubnet is a new subnet defined using SubnetV2.

myVpc.addInternetGateway({
ipv4Destination: '192.168.0.0/16',
subnets: [mySubnet],
});

}

/**
Expand Down Expand Up @@ -437,9 +444,14 @@ export abstract class VpcV2Base extends Resource implements IVpcV2 {
};

if (options?.subnets) {
// Use Set to ensure unique subnets
const processedSubnets = new Set<string>();
const subnets = flatten(options.subnets.map(s => this.selectSubnets(s).subnets));
subnets.forEach((subnet) => {
this.createEgressRoute(subnet, egw, options.destination);
if (!processedSubnets.has(subnet.node.id)) {
this.createEgressRoute(subnet, egw, options.destination);
processedSubnets.add(subnet.node.id);
}
});
}
}
Expand Down Expand Up @@ -476,9 +488,23 @@ export abstract class VpcV2Base extends Resource implements IVpcV2 {
this._internetConnectivityEstablished.add(igw);
this._internetGatewayId = igw.routerTargetId;

// If there are no public subnets defined, no default route will be added
if (this.publicSubnets) {
this.publicSubnets.forEach( (s) => this.addDefaultInternetRoute(s, igw, options));
// Add routes for subnets defined as an input
if (options?.subnets) {
// Use Set to ensure unique subnets
const processedSubnets = new Set<string>();
const subnets = flatten(options.subnets.map(s => this.selectSubnets(s).subnets));
subnets.forEach((subnet) => {
if (!processedSubnets.has(subnet.node.id)) {
if (!this.publicSubnets.includes(subnet)) {
Annotations.of(this).addWarningV2('InternetGatewayWarning',
`Subnet ${subnet.node.id} is not a public subnet. Internet Gateway should be added only to public subnets.`);
}
this.addDefaultInternetRoute(subnet, igw, options);
processedSubnets.add(subnet.node.id);
};
}); // If there are no input subnets defined, default route will be added to all public subnets
Copy link
Contributor

Choose a reason for hiding this comment

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

My understanding is that if a subnet has a route to the internet, then it becomes a public subnet.

So should we allow adding internet gateway to private subnets? If we do, should we warn the users so that they do not accidentally make their subnets, intended to be private, public?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, this was my understanding as well but after talking to service team there can be cases with VPNGW type of attachment where customer might be leveraging their on-premises network to route the traffic to internet.
I was thinking of adding warning as well for private subnets, but there can be different categories under private subnets too. But I still think notifying is good idea, so added a warning for all kinds of subnets except PUBLIC.

Copy link
Contributor

Choose a reason for hiding this comment

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

there can be cases with VPNGW type of attachment where customer might be leveraging their on-premises network to route the traffic to internet.

And in such case, it won't be a private subnet? Is it correct to say that "Private Subnet" is a concept to describe a subnet that is configured to not able to access the internet? I am thinking if we should keep the public vs private subnet concept in CDK since, say, a private subnet may become public due to drifting.

Anyways, this is not a blocking comment. We can discuss further offline. Thank you for explaining.

} else if (!options?.subnets && this.publicSubnets) {
this.publicSubnets.forEach((publicSubnets) => this.addDefaultInternetRoute(publicSubnets, igw, options));
}
}

Expand All @@ -488,10 +514,6 @@ export abstract class VpcV2Base extends Resource implements IVpcV2 {
*/
private addDefaultInternetRoute(subnet: ISubnetV2, igw: InternetGateway, options?: InternetGatewayOptions): void {

if (subnet.subnetType !== SubnetType.PUBLIC) {
throw new Error('No public subnets defined to add route for internet gateway');
}

// Add default route to IGW for IPv6
if (subnet.ipv6CidrBlock) {
new Route(this, `${subnet.node.id}-DefaultIPv6Route`, {
Expand Down
105 changes: 105 additions & 0 deletions packages/@aws-cdk/aws-ec2-alpha/test/ipam.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,109 @@ describe('IPAM Test', () => {
);
});

test('IPAM throws error if awsService is not provided for IPv6 address', () => {
// Create IPAM resources
const ipamRegion = new Ipam(stack, 'TestIpam', {
operatingRegion: ['us-west-2'],
});
const poolOptions: vpc.PoolOptions = {
addressFamily: AddressFamily.IP_V6,
publicIpSource: IpamPoolPublicIpSource.AMAZON,
locale: 'us-west-2',
};
expect(() => ipamRegion.publicScope.addPool('TestPool', poolOptions)).toThrow('awsService is required when addressFamily is set to ipv6');
});

test('IPAM throws error if operating region is provided as an empty array', () => {
const app = new cdk.App();
const stack_new = new cdk.Stack(app, 'TestStack');
expect(() => new Ipam(stack_new, 'TestIpam', {
operatingRegion: [],
})).toThrow('Please provide at least one operating region');
});

test('IPAM infers region from provided operating region correctly', () => {
const app = new cdk.App();
const stack_new = new cdk.Stack(app, 'TestStack');
new Ipam(stack_new, 'TestIpam', {
operatingRegion: ['us-west-2'],
});
Template.fromStack(stack_new).hasResourceProperties(
'AWS::EC2::IPAM', {
OperatingRegions: [
{
RegionName: 'us-west-2',
},
],
},
);
});

test('IPAM infers region from stack if not provided under IPAM class object', () => {
const app = new cdk.App();
const stack_new = new cdk.Stack(app, 'TestStack', {
env: {
region: 'us-west-2',
},
});
new Ipam(stack_new, 'TestIpam', {});
Template.fromStack(stack_new).hasResourceProperties(
'AWS::EC2::IPAM', {
OperatingRegions: [
{
RegionName: 'us-west-2',
},
],
},
);
});

test('IPAM refers to stack region token', () => {
const app = new cdk.App();
const stack_new = new cdk.Stack(app, 'TestStack');
new Ipam(stack_new, 'TestIpam', {});
Template.fromStack(stack_new).hasResourceProperties(
'AWS::EC2::IPAM', {
OperatingRegions: [
{
RegionName: {
Ref: 'AWS::Region',
},
},
],
},
);
});

test('IPAM throws error if locale(region) of pool is not one of the operating regions', () => {
const ipamRegion = new Ipam(stack, 'TestIpam', {
operatingRegion: ['us-west-2'],
});
const poolOptions: vpc.PoolOptions = {
addressFamily: AddressFamily.IP_V6,
awsService: vpc.AwsServiceName.EC2,
publicIpSource: IpamPoolPublicIpSource.AMAZON,
locale: 'us-west-1', // Incorrect Region
};
expect(() => ipamRegion.publicScope.addPool('TestPool', poolOptions)).toThrow("The provided locale 'us-west-1' is not in the operating regions.");
});

test('IPAM handles operating regions correctly', () => {
const new_app = new cdk.App();
const testStack = new cdk.Stack(new_app, 'TestStack', {
env: {
region: 'us-west-1',
},
});
new Ipam(testStack, 'TestIpamNew', {});
Template.fromStack(testStack).hasResourceProperties(
'AWS::EC2::IPAM', {
OperatingRegions: [
{
RegionName: 'us-west-1',
},
],
},
);
});
});// End Test
Loading
Loading