Skip to content

Commit 91a3e8c

Browse files
authored
fix(ec2): IPAM allocated subnets cannot split more than 256 times (#28027)
Because of IPAM allocation, we can't know the parent CIDR at synth time, so we cannot calculate the CIDR split at synth time either. This forces us to rely on the `{ Fn::Cidr }` function provided by CloudFormation. For resource consumption reasons, this function is limited to splitting any range into at most 256 subranges, which means the IPAM allocated VPC cannot split into more subranges either. This PR adds a recursive split feature: if we need to split an CIDR range more than 256 times, we will do multiple splits: ```ts Fn.select(300, Fn.cidr(range, 4096, 4)) // <-- illegal // == Fn.select(44, Fn.cidr(Fn.select(1, Fn.cidr(range, 4, 12)), 256, 4)) ``` Fixes #25537. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 95538a1 commit 91a3e8c

File tree

2 files changed

+74
-4
lines changed

2 files changed

+74
-4
lines changed

packages/aws-cdk-lib/aws-ec2/lib/ip-addresses.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { calculateCidrSplits } from './cidr-splits';
1+
import { CidrSplit, calculateCidrSplits } from './cidr-splits';
22
import { NetworkBuilder } from './network-util';
33
import { SubnetConfiguration } from './vpc';
44
import { Fn, Token } from '../../core';
@@ -230,7 +230,7 @@ class AwsIpam implements IIpAddresses {
230230

231231
const allocatedSubnets: AllocatedSubnet[] = cidrSplit.map(subnet => {
232232
return {
233-
cidr: Fn.select(subnet.index, Fn.cidr(input.vpcCidr, subnet.count, `${32-subnet.netmask}`)),
233+
cidr: cidrSplitToCfnExpression(input.vpcCidr, subnet),
234234
};
235235
});
236236

@@ -241,6 +241,45 @@ class AwsIpam implements IIpAddresses {
241241
}
242242
}
243243

244+
/**
245+
* Convert a CIDR split command to a CFN expression that calculates the same CIDR
246+
*
247+
* Can recursively produce multiple `{ Fn::Cidr }` expressions.
248+
*
249+
* This is necessary because CFN's `{ Fn::Cidr }` reifies the split to an actual list of
250+
* strings, and to limit resource consumption `count` may never be higher than 256. So
251+
* if we need to split deeper, we need to do more than one split.
252+
*
253+
* (Function public for testing)
254+
*/
255+
export function cidrSplitToCfnExpression(parentCidr: string, split: CidrSplit) {
256+
const MAX_COUNT = 256;
257+
const MAX_COUNT_BITS = 8;
258+
259+
if (split.count === 1) {
260+
return parentCidr;
261+
}
262+
263+
if (split.count <= MAX_COUNT) {
264+
return Fn.select(split.index, Fn.cidr(parentCidr, split.count, `${32-split.netmask}`));
265+
}
266+
267+
if (split.netmask - MAX_COUNT_BITS < 1) {
268+
throw new Error(`Cannot split an IP range into ${split.count} /${split.netmask}s`);
269+
}
270+
271+
const parentSplit = {
272+
netmask: split.netmask - MAX_COUNT_BITS,
273+
count: Math.ceil(split.count / MAX_COUNT),
274+
index: Math.floor(split.index / MAX_COUNT),
275+
};
276+
return cidrSplitToCfnExpression(cidrSplitToCfnExpression(parentCidr, parentSplit), {
277+
netmask: split.netmask,
278+
count: MAX_COUNT,
279+
index: split.index - (parentSplit.index * MAX_COUNT),
280+
});
281+
}
282+
244283
/**
245284
* Implements static Ip assignment locally.
246285
*

packages/aws-cdk-lib/aws-ec2/test/ip-addresses.test.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11

22
import { Template } from '../../assertions';
33
import { Stack } from '../../core';
4-
import { IpAddresses, SubnetType, Vpc } from '../lib';
4+
import { IpAddresses, SubnetType, Vpc, cidrSplitToCfnExpression } from '../lib';
5+
import { CidrSplit } from '../lib/cidr-splits';
56

67
describe('Cidr vpc allocation', () => {
78

@@ -406,5 +407,35 @@ describe('AwsIpam Vpc Integration', () => {
406407
template.resourceCountIs('AWS::EC2::Subnet', 2);
407408

408409
});
409-
410410
});
411+
412+
type CfnSplit = {
413+
count: number;
414+
hostbits: number;
415+
select: number;
416+
}
417+
418+
test.each([
419+
// Index into first block
420+
[{ count: 4096, netmask: 28, index: 123 }, /* -> */ { count: 16, hostbits: 12, select: 0 }, { count: 256, hostbits: 4, select: 123 }],
421+
// Index into second block
422+
[{ count: 4096, netmask: 28, index: 300 }, /* -> */ { count: 16, hostbits: 12, select: 1 }, { count: 256, hostbits: 4, select: 44 }],
423+
// Index into third block
424+
[{ count: 4096, netmask: 28, index: 513 }, /* -> */ { count: 16, hostbits: 12, select: 2 }, { count: 256, hostbits: 4, select: 1 }],
425+
// Count too low for netmask (wasting space)
426+
[{ count: 4000, netmask: 28, index: 300 }, /* -> */ { count: 16, hostbits: 12, select: 1 }, { count: 256, hostbits: 4, select: 44 }],
427+
])('recursive splitting when CIDR needs to be split more than 256 times: %p', (split: CidrSplit, first: CfnSplit, second: CfnSplit) => {
428+
const stack = new Stack();
429+
expect(stack.resolve(cidrSplitToCfnExpression('10.0.0.0/16', split))).toEqual({
430+
'Fn::Select': [
431+
second.select,
432+
{
433+
'Fn::Cidr': [
434+
{ 'Fn::Select': [first.select, { 'Fn::Cidr': ['10.0.0.0/16', first.count, `${first.hostbits}`] }] },
435+
second.count,
436+
`${second.hostbits}`,
437+
],
438+
},
439+
],
440+
});
441+
});

0 commit comments

Comments
 (0)