diff --git a/packages/aws-cdk-lib/aws-ec2/lib/ip-addresses.ts b/packages/aws-cdk-lib/aws-ec2/lib/ip-addresses.ts index d6b4e7cb0b93d..6d0085fe312f1 100644 --- a/packages/aws-cdk-lib/aws-ec2/lib/ip-addresses.ts +++ b/packages/aws-cdk-lib/aws-ec2/lib/ip-addresses.ts @@ -1,4 +1,4 @@ -import { calculateCidrSplits } from './cidr-splits'; +import { CidrSplit, calculateCidrSplits } from './cidr-splits'; import { NetworkBuilder } from './network-util'; import { SubnetConfiguration } from './vpc'; import { Fn, Token } from '../../core'; @@ -230,7 +230,7 @@ class AwsIpam implements IIpAddresses { const allocatedSubnets: AllocatedSubnet[] = cidrSplit.map(subnet => { return { - cidr: Fn.select(subnet.index, Fn.cidr(input.vpcCidr, subnet.count, `${32-subnet.netmask}`)), + cidr: cidrSplitToCfnExpression(input.vpcCidr, subnet), }; }); @@ -241,6 +241,45 @@ class AwsIpam implements IIpAddresses { } } +/** + * Convert a CIDR split command to a CFN expression that calculates the same CIDR + * + * Can recursively produce multiple `{ Fn::Cidr }` expressions. + * + * This is necessary because CFN's `{ Fn::Cidr }` reifies the split to an actual list of + * strings, and to limit resource consumption `count` may never be higher than 256. So + * if we need to split deeper, we need to do more than one split. + * + * (Function public for testing) + */ +export function cidrSplitToCfnExpression(parentCidr: string, split: CidrSplit) { + const MAX_COUNT = 256; + const MAX_COUNT_BITS = 8; + + if (split.count === 1) { + return parentCidr; + } + + if (split.count <= MAX_COUNT) { + return Fn.select(split.index, Fn.cidr(parentCidr, split.count, `${32-split.netmask}`)); + } + + if (split.netmask - MAX_COUNT_BITS < 1) { + throw new Error(`Cannot split an IP range into ${split.count} /${split.netmask}s`); + } + + const parentSplit = { + netmask: split.netmask - MAX_COUNT_BITS, + count: Math.ceil(split.count / MAX_COUNT), + index: Math.floor(split.index / MAX_COUNT), + }; + return cidrSplitToCfnExpression(cidrSplitToCfnExpression(parentCidr, parentSplit), { + netmask: split.netmask, + count: MAX_COUNT, + index: split.index - (parentSplit.index * MAX_COUNT), + }); +} + /** * Implements static Ip assignment locally. * diff --git a/packages/aws-cdk-lib/aws-ec2/test/ip-addresses.test.ts b/packages/aws-cdk-lib/aws-ec2/test/ip-addresses.test.ts index e443f5fddd670..e766c475e5dcb 100644 --- a/packages/aws-cdk-lib/aws-ec2/test/ip-addresses.test.ts +++ b/packages/aws-cdk-lib/aws-ec2/test/ip-addresses.test.ts @@ -1,7 +1,8 @@ import { Template } from '../../assertions'; import { Stack } from '../../core'; -import { IpAddresses, SubnetType, Vpc } from '../lib'; +import { IpAddresses, SubnetType, Vpc, cidrSplitToCfnExpression } from '../lib'; +import { CidrSplit } from '../lib/cidr-splits'; describe('Cidr vpc allocation', () => { @@ -406,5 +407,35 @@ describe('AwsIpam Vpc Integration', () => { template.resourceCountIs('AWS::EC2::Subnet', 2); }); - }); + +type CfnSplit = { + count: number; + hostbits: number; + select: number; +} + +test.each([ + // Index into first block + [{ count: 4096, netmask: 28, index: 123 }, /* -> */ { count: 16, hostbits: 12, select: 0 }, { count: 256, hostbits: 4, select: 123 }], + // Index into second block + [{ count: 4096, netmask: 28, index: 300 }, /* -> */ { count: 16, hostbits: 12, select: 1 }, { count: 256, hostbits: 4, select: 44 }], + // Index into third block + [{ count: 4096, netmask: 28, index: 513 }, /* -> */ { count: 16, hostbits: 12, select: 2 }, { count: 256, hostbits: 4, select: 1 }], + // Count too low for netmask (wasting space) + [{ count: 4000, netmask: 28, index: 300 }, /* -> */ { count: 16, hostbits: 12, select: 1 }, { count: 256, hostbits: 4, select: 44 }], +])('recursive splitting when CIDR needs to be split more than 256 times: %p', (split: CidrSplit, first: CfnSplit, second: CfnSplit) => { + const stack = new Stack(); + expect(stack.resolve(cidrSplitToCfnExpression('10.0.0.0/16', split))).toEqual({ + 'Fn::Select': [ + second.select, + { + 'Fn::Cidr': [ + { 'Fn::Select': [first.select, { 'Fn::Cidr': ['10.0.0.0/16', first.count, `${first.hostbits}`] }] }, + second.count, + `${second.hostbits}`, + ], + }, + ], + }); +}); \ No newline at end of file