Skip to content

Commit

Permalink
feat(ec2): Vpc supports allocating CIDR from AWS IPAM (#22458)
Browse files Browse the repository at this point in the history
Allows Vpc to Use [Aws IPAM](https://docs.aws.amazon.com/vpc/latest/ipam/what-it-is-ipam.html) for Ip address assignment:

```ts
import { IpAddresses } from '@aws-cdk/aws-ec2';

declare const pool: ec2.CfnIPAMPool;

new ec2.Vpc(stack, 'TheVPC', {
  ipAddresses: ec2.IpAddresses.awsIpamAllocation({
    ipv4IpamPoolId: pool.ref,
    ipv4NetmaskLength: 18,
    defaultSubnetIpv4NetmaskLength: 24
  })
});
```

This is useful for enterprise users that wish to adopt the benefits of centralised IP address management.

It introduces `ipAddresses` property to allow the new configuration.

----

Thanks to @rix0rrr for support on this.

---

closes #21333

----

#22443 - Issue adds a fix to allow the clean up of the AWS Ipam resource used in ingeg-test testing. Would be better to implement something like this later. for now disclaimer added to integ-test clean up needed on Ipam.

----

### All Submissions:

* [X] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md)

### New Features

* [X] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)?
	* [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)?

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
nbaillie authored Oct 27, 2022
1 parent 89a7365 commit 7ed9cd1
Show file tree
Hide file tree
Showing 24 changed files with 2,893 additions and 62 deletions.
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-apprunner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ To associate an App Runner service with a custom VPC, define `vpcConnector` for
import * as ec2 from '@aws-cdk/aws-ec2';

const vpc = new ec2.Vpc(this, 'Vpc', {
cidr: '10.0.0.0/16',
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16')
});

const vpcConnector = new apprunner.VpcConnector(this, 'VpcConnector', {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const stack = new cdk.Stack(app, 'integ-apprunner');

// Scenario 6: Create the service from ECR public with a VPC Connector
const vpc = new ec2.Vpc(stack, 'Vpc', {
cidr: '10.0.0.0/16',
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
});

const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc });
Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-apprunner/test/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,7 @@ test('specifying a vpcConnector should assign the service to it and set the egre
const stack = new cdk.Stack(app, 'demo-stack');

const vpc = new ec2.Vpc(stack, 'Vpc', {
cidr: '10.0.0.0/16',
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
});

const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc });
Expand Down
8 changes: 4 additions & 4 deletions packages/@aws-cdk/aws-apprunner/test/vpc-connector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ test('create a vpcConnector with all properties', () => {
const stack = new cdk.Stack(app, 'demo-stack');

const vpc = new ec2.Vpc(stack, 'Vpc', {
cidr: '10.0.0.0/16',
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
});

const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc });
Expand Down Expand Up @@ -48,7 +48,7 @@ test('create a vpcConnector without a name', () => {
const stack = new cdk.Stack(app, 'demo-stack');

const vpc = new ec2.Vpc(stack, 'Vpc', {
cidr: '10.0.0.0/16',
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
});

const securityGroup = new ec2.SecurityGroup(stack, 'SecurityGroup', { vpc });
Expand Down Expand Up @@ -85,7 +85,7 @@ test('create a vpcConnector without a security group should create one', () => {
const stack = new cdk.Stack(app, 'demo-stack');

const vpc = new ec2.Vpc(stack, 'Vpc', {
cidr: '10.0.0.0/16',
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
});

// WHEN
Expand Down Expand Up @@ -120,7 +120,7 @@ test('create a vpcConnector with an empty security group array should create one
const stack = new cdk.Stack(app, 'demo-stack');

const vpc = new ec2.Vpc(stack, 'Vpc', {
cidr: '10.0.0.0/16',
ipAddresses: ec2.IpAddresses.cidr('10.0.0.0/16'),
});

// WHEN
Expand Down
72 changes: 64 additions & 8 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,62 @@ new ec2.Vpc(this, 'TheVPC', {
provider.connections.allowFrom(ec2.Peer.ipv4('1.2.3.4/8'), ec2.Port.tcp(80));
```

### Ip Address Management

The VPC spans a supernet IP range, which contains the non-overlapping IPs of its contained subnets. Possible sources for this IP range are:

* You specify an IP range directly by specifying a CIDR
* You allocate an IP range of a given size automatically from AWS IPAM

By default the Vpc will allocate the `10.0.0.0/16` address range which will be exhaustively spread across all subnets in the subnet configuration. This behavior can be changed by passing an object that implements `IIpAddresses` to the `ipAddress` property of a Vpc. See the subsequent sections for the options.

Be aware that if you don't explicitly reserve subnet groups in `subnetConfiguration`, the address space will be fully allocated! If you predict you may need to add more subnet groups later, add them early on and set `reserved: true` (see the "Advanced Subnet Configuration" section for more information).

#### Specifying a CIDR directly

Use `IpAddresses.cidr` to define a Cidr range for your Vpc directly in code:

```ts
import { IpAddresses } from '@aws-cdk/aws-ec2';

new ec2.Vpc(stack, 'TheVPC', {
ipAddresses: ec2.IpAddresses.cidr('10.0.1.0/20')
});
```

Space will be allocated to subnets in the following order:

* First, spaces is allocated for all subnets groups that explicitly have a `cidrMask` set as part of their configuration (including reserved subnets).
* Afterwards, any remaining space is divided evenly between the rest of the subnets (if any).

The argument to `IpAddresses.cidr` may not be a token, and concrete Cidr values are generated in the synthesized CloudFormation template.

#### Allocating an IP range from AWS IPAM

Amazon VPC IP Address Manager (IPAM) manages a large IP space, from which chunks can be allocated for use in the Vpc. For information on Amazon VPC IP Address Manager please see the [official documentation](https://docs.aws.amazon.com/vpc/latest/ipam/what-it-is-ipam.html). An example of allocating from AWS IPAM looks like this:

```ts
import { IpAddresses } from '@aws-cdk/aws-ec2';

declare const pool: ec2.CfnIPAMPool;

new ec2.Vpc(stack, 'TheVPC', {
ipAddresses: ec2.IpAddresses.awsIpamAllocation({
ipv4IpamPoolId: pool.ref,
ipv4NetmaskLength: 18,
defaultSubnetIpv4NetmaskLength: 24
})
});
```

`IpAddresses.awsIpamAllocation` requires the following:

* `ipv4IpamPoolId`, the id of an IPAM Pool from which the VPC range should be allocated.
* `ipv4NetmaskLength`, the size of the IP range that will be requested from the Pool at deploy time.
* `defaultSubnetIpv4NetmaskLength`, the size of subnets in groups that don't have `cidrMask` set.

With this method of IP address management, no attempt is made to guess at subnet group sizes or to exhaustively allocate the IP range. All subnet groups must have an explicit `cidrMask` set as part of their subnet configuration, or `defaultSubnetIpv4NetmaskLength` must be set for a default size. If not, synthesis will fail and you must provide one or the other.

### Advanced Subnet Configuration

If the default VPC configuration (public and private subnets spanning the
Expand All @@ -227,9 +283,9 @@ subnet configuration could look like this:

```ts
const vpc = new ec2.Vpc(this, 'TheVPC', {
// 'cidr' configures the IP range and size of the entire VPC.
// The IP space will be divided over the configured subnets.
cidr: '10.0.0.0/21',
// 'IpAddresses' configures the IP range and size of the entire VPC.
// The IP space will be divided based on configuration for the subnets.
ipAddresses: IpAddresses.cidr('10.0.0.0/21'),

// 'maxAzs' configures the maximum number of availability zones to use.
// If you want to specify the exact availability zones you want the VPC
Expand Down Expand Up @@ -948,11 +1004,11 @@ new ec2.Instance(this, 'Instance2', {
}),
});

// AWS Linux 2 with kernel 5.x
// AWS Linux 2 with kernel 5.x
new ec2.Instance(this, 'Instance3', {
vpc,
instanceType,
machineImage: new ec2.AmazonLinuxImage({
machineImage: new ec2.AmazonLinuxImage({
generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
kernel: ec2.AmazonLinuxKernel.KERNEL5_X,
}),
Expand All @@ -962,7 +1018,7 @@ new ec2.Instance(this, 'Instance3', {
new ec2.Instance(this, 'Instance4', {
vpc,
instanceType,
machineImage: new ec2.AmazonLinuxImage({
machineImage: new ec2.AmazonLinuxImage({
generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2022,
}),
});
Expand Down Expand Up @@ -1407,9 +1463,9 @@ asset.grantRead(instance.role);
### Persisting user data

By default, EC2 UserData is run once on only the first time that an instance is started. It is possible to make the
user data script run on every start of the instance.
user data script run on every start of the instance.

When creating a Windows UserData you can use the `persist` option to set whether or not to add
When creating a Windows UserData you can use the `persist` option to set whether or not to add
`<persist>true</persist>` [to the user data script](https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ec2-windows-user-data.html#user-data-scripts). it can be used as follows:

```ts
Expand Down
77 changes: 77 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/cidr-splits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Return the splits necessary to allocate the given sequence of cidrs in the given order
*
* The entire block is of size 'rootNetmask', and subsequent blocks will be allocated
* from it sized according to the sizes in the 'netmasks' array.
*
* The return value is a list of `CidrSplit` objects, which represent
* invocations of a pair of `Fn.select(Fn.cidr(...))` operations.
*
* Strategy: walk through the IP block space, clipping to the next possible
* start of a block of the given size, then allocate it. Here is an unrealistic
* example (with a weird ordering of the netmasks to show how clipping and hence
* space wasting plays out in practice):
*
* root space /16
* ┌──────────────────────────────────────────────────────────────────────────────────────────────┐
* │ │
* A /21 B /19
* ┌───┬───┬───┬───┬───────────────┬───────────────┬───┬───────────┬───────────────┬──────────────┐
* │ A │ A │ A │###│ B │ B │ A │###########│ B │ .... │
* └───┴───┴───┴───┴───────────────┴───────────────┴───┴───────────┴───────────────┴──────────────┘
* ^^^______ wasted space _________________^^^^^^
*/
export function calculateCidrSplits(rootNetmask: number, netmasks: number[]): CidrSplit[] {
const ret = new Array<CidrSplit>();

let offset = 0;
for (const netmask of netmasks) {
const size = Math.pow(2, 32 - netmask);

// Clip offset to the next block of the given size
offset = nextMultiple(offset, size);

const count = Math.pow(2, netmask - rootNetmask);
ret.push({
count,
netmask,
index: offset / size,
});

// Consume
offset += size;
}

if (offset > Math.pow(2, 32 - rootNetmask)) {
throw new Error(`IP space of size /${rootNetmask} not big enough to allocate subnets of sizes ${netmasks.map(x => `/${x}`)}`);
}

return ret;
}

function nextMultiple(current: number, multiple: number) {
return Math.ceil(current / multiple) * multiple;
}

/**
* A representation of a pair of `Fn.select(Fn.cidr())` invocations
*/
export interface CidrSplit {
/**
* The netmask of this block size
*
* This is the inverse number of what you need to pass to Fn.cidr (pass `32 -
* netmask` to Fn.cidr)`.
*/
readonly netmask: number;

/**
* How many parts the mask needs to be split into
*/
readonly count: number;

/**
* What subnet index to select from the split
*/
readonly index: number;
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ec2/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export * from './client-vpn-endpoint-types';
export * from './client-vpn-endpoint';
export * from './client-vpn-authorization-rule';
export * from './client-vpn-route';
export * from './ip-addresses';

// AWS::EC2 CloudFormation Resources:
export * from './ec2.generated';
Expand Down
Loading

0 comments on commit 7ed9cd1

Please sign in to comment.