diff --git a/packages/aws-cdk-lib/aws-eks/README.md b/packages/aws-cdk-lib/aws-eks/README.md index a32f94c651bb4..893d31ebf119d 100644 --- a/packages/aws-cdk-lib/aws-eks/README.md +++ b/packages/aws-cdk-lib/aws-eks/README.md @@ -33,6 +33,7 @@ In addition, the library also supports defining Kubernetes resource manifests wi - [ARM64 Support](#arm64-support) - [Masters Role](#masters-role) - [Encryption](#encryption) + - [Hybrid nodes](#hybrid-nodes) - [Permissions and Security](#permissions-and-security) - [AWS IAM Mapping](#aws-iam-mapping) - [Access Config](#access-config) @@ -1010,6 +1011,28 @@ declare const cluster: eks.Cluster; const clusterEncryptionConfigKeyArn = cluster.clusterEncryptionConfigKeyArn; ``` +### Hybrid Nodes + +When you create an Amazon EKS cluster, you can configure it to leverage the [EKS Hybrid Nodes](https://aws.amazon.com/eks/hybrid-nodes/) feature, allowing you to use your on-premises and edge infrastructure as nodes in your EKS cluster. Refer to the Hyrid Nodes [networking documentation](https://docs.aws.amazon.com/eks/latest/userguide/hybrid-nodes-networking.html) to configure your on-premises network, node and pod CIDRs, access control, etc before creating your EKS Cluster. + +Once you have identified the on-premises node and pod (optional) CIDRs you will use for your hybrid nodes and the workloads running on them, you can specify them during cluster creation using the `remoteNodeNetworks` and `remotePodNetworks` (optional) properties: + +```ts +new eks.Cluster(this, 'Cluster', { + version: eks.KubernetesVersion.V1_31, + remoteNodeNetworks: [ + { + cidrs: ['10.0.0.0/16'], + }, + ], + remotePodNetworks: [ + { + cidrs: ['192.168.0.0/16'], + }, + ], +}); +``` + ## Permissions and Security Amazon EKS provides several mechanism of securing the cluster and granting permissions to specific IAM users and roles. diff --git a/packages/aws-cdk-lib/aws-eks/lib/cluster-resource.ts b/packages/aws-cdk-lib/aws-eks/lib/cluster-resource.ts index 5e054a44ec35c..10dcd3f94844a 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/cluster-resource.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/cluster-resource.ts @@ -27,6 +27,7 @@ export interface ClusterResourceProps { readonly tags?: { [key: string]: string }; readonly logging?: { [key: string]: [ { [key: string]: any } ] }; readonly accessconfig?: CfnCluster.AccessConfigProperty; + readonly remoteNetworkConfig?: CfnCluster.RemoteNetworkConfigProperty; } /** @@ -90,6 +91,7 @@ export class ClusterResource extends Construct { tags: props.tags, logging: props.logging, accessConfig: props.accessconfig, + remoteNetworkConfig: props.remoteNetworkConfig, }, AssumeRoleArn: this.adminRole.roleArn, @@ -98,7 +100,7 @@ export class ClusterResource extends Construct { // doesn't contain XXX key in object" (see #8276) by incrementing this // number, you will effectively cause a "no-op update" to the cluster // which will return the new set of attribute. - AttributesRevision: 3, + AttributesRevision: 4, }, }); diff --git a/packages/aws-cdk-lib/aws-eks/lib/cluster.ts b/packages/aws-cdk-lib/aws-eks/lib/cluster.ts index 2c624a477c9b8..3ef3f3b6c97c4 100644 --- a/packages/aws-cdk-lib/aws-eks/lib/cluster.ts +++ b/packages/aws-cdk-lib/aws-eks/lib/cluster.ts @@ -22,6 +22,7 @@ import { ServiceAccount, ServiceAccountOptions } from './service-account'; import { LifecycleLabel, renderAmazonLinuxUserData, renderBottlerocketUserData } from './user-data'; import * as autoscaling from '../../aws-autoscaling'; import * as ec2 from '../../aws-ec2'; +import { CidrBlock } from '../../aws-ec2/lib/network-util'; import * as iam from '../../aws-iam'; import * as kms from '../../aws-kms'; import * as lambda from '../../aws-lambda'; @@ -703,6 +704,19 @@ export interface ClusterOptions extends CommonClusterOptions { * @default AuthenticationMode.CONFIG_MAP */ readonly authenticationMode?: AuthenticationMode; + + /** + * IPv4 CIDR blocks defining the expected address range of hybrid nodes + * that will join the cluster. + * @default - none + */ + readonly remoteNodeNetworks?: RemoteNodeNetwork[]; + + /** + * IPv4 CIDR blocks for Pods running Kubernetes webhooks on hybrid nodes. + * @default - none + */ + readonly remotePodNetworks?: RemotePodNetwork[]; } /** @@ -1668,6 +1682,74 @@ export class Cluster extends ClusterBase { throw new Error('Cannot specify serviceIpv4Cidr with ipFamily equal to IpFamily.IP_V6'); } + if (props.remoteNodeNetworks) { + // validate that no two CIDRs overlap within the same remote node network + for (let i = 0; i < props.remoteNodeNetworks.length; i++) { + if (props.remoteNodeNetworks[i].cidrs.length > 1) { + for (let j = 0; j < props.remoteNodeNetworks[i].cidrs.length; j++) { + for (let k = j + 1; k < props.remoteNodeNetworks[i].cidrs.length; k++) { + const overlap = validateCidrPairOverlap(props.remoteNodeNetworks[i].cidrs[j], props.remoteNodeNetworks[i].cidrs[k]); + if (overlap) { + throw new Error(`CIDR ${props.remoteNodeNetworks[i].cidrs[j]} should not overlap with CIDR ${props.remoteNodeNetworks[i].cidrs[k]} in remote node network #${i+1}`); + } + } + } + } + } + + // validate that no two CIDRs overlap across different remote node networks + for (let i = 0; i < props.remoteNodeNetworks.length; i++) { + for (let j = i + 1; j < props.remoteNodeNetworks.length; j++) { + const [overlap, remoteNodeCidr1, remoteNodeCidr2] = validateCidrBlocksOverlap( + props.remoteNodeNetworks[i].cidrs, + props.remoteNodeNetworks[j].cidrs, + ); + if (overlap) { + throw new Error(`CIDR block ${remoteNodeCidr1} in remote node network #${i+1} should not overlap with CIDR block ${remoteNodeCidr2} in remote node network #${j+1}`); + } + } + } + + if (props.remotePodNetworks) { + // validate that no two CIDRs overlap within the same remote pod network + for (let i = 0; i < props.remotePodNetworks.length; i++) { + if (props.remotePodNetworks[i].cidrs.length > 1) { + for (let j = 0; j < props.remotePodNetworks[i].cidrs.length; j++) { + for (let k = j + 1; k < props.remotePodNetworks[i].cidrs.length; k++) { + const overlap = validateCidrPairOverlap(props.remotePodNetworks[i].cidrs[j], props.remotePodNetworks[i].cidrs[k]); + if (overlap) { + throw new Error(`CIDR ${props.remotePodNetworks[i].cidrs[j]} should not overlap with CIDR ${props.remotePodNetworks[i].cidrs[k]} in remote pod network #${i+1}`); + } + } + } + } + } + + // validate that no two CIDRs overlap across different remote pod networks + for (let i = 0; i < props.remotePodNetworks.length; i++) { + for (let j = i + 1; j < props.remotePodNetworks.length; j++) { + const [overlap, remotePodCidr1, remotePodCidr2] = validateCidrBlocksOverlap( + props.remotePodNetworks[i].cidrs, + props.remotePodNetworks[j].cidrs, + ); + if (overlap) { + throw new Error(`CIDR block ${remotePodCidr1} in remote pod network #${i} should not overlap with CIDR block ${remotePodCidr2} in remote pod network #${j}`); + } + } + } + + // validate that no two CIDRs overlap between a given remote node network and remote pod network + for (const nodeNetwork of props.remoteNodeNetworks) { + for (const podNetwork of props.remotePodNetworks) { + const [overlap, remoteNodeCidr, remotePodCidr] = validateCidrBlocksOverlap(nodeNetwork.cidrs, podNetwork.cidrs); + if (overlap) { + throw new Error(`Remote node network CIDR block ${remoteNodeCidr} should not overlap with remote pod network CIDR block ${remotePodCidr}`); + } + } + } + } + } + this.authenticationMode = props.authenticationMode; const resource = this._clusterResource = new ClusterResource(this, 'Resource', { @@ -1679,6 +1761,14 @@ export class Cluster extends ClusterBase { authenticationMode: props.authenticationMode, bootstrapClusterCreatorAdminPermissions: props.bootstrapClusterCreatorAdminPermissions, }, + ...(props.remoteNodeNetworks ? { + remoteNetworkConfig: { + remoteNodeNetworks: props.remoteNodeNetworks, + ...(props.remotePodNetworks ? { + remotePodNetworks: props.remotePodNetworks, + }: {}), + }, + } : {}), resourcesVpcConfig: { securityGroupIds: [securityGroup.securityGroupId], subnetIds, @@ -2392,6 +2482,30 @@ export interface AutoScalingGroupOptions { readonly spotInterruptHandler?: boolean; } +/** + * Network configuration of nodes run on-premises with EKS Hybrid Nodes. + */ +export interface RemoteNodeNetwork { + /** + * Specifies the list of remote node CIDRs. + * + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-remotenodenetwork.html#cfn-eks-cluster-remotenodenetwork-cidrs + */ + readonly cidrs: string[]; +} + +/** + * Network configuration of pods run on-premises with EKS Hybrid Nodes. + */ +export interface RemotePodNetwork { + /** + * Specifies the list of remote pod CIDRs. + * + * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-remotepodnetwork.html#cfn-eks-cluster-remotepodnetwork-cidrs + */ + readonly cidrs: string[]; +} + /** * Import a cluster to use in another stack */ @@ -2675,3 +2789,34 @@ function cpuArchForInstanceType(instanceType: ec2.InstanceType) { function flatten(xss: A[][]): A[] { return Array.prototype.concat.call([], ...xss); } + +function validateCidrBlocksOverlap(cidrBlocks1: string[], cidrBlocks2: string[]): [boolean, string, string] { + for (const cidr1 of cidrBlocks1) { + for (const cidr2 of cidrBlocks2) { + const overlap = validateCidrPairOverlap(cidr1, cidr2); + if (overlap) { + return [true, cidr1, cidr2]; + } + } + } + + return [false, '', '']; +} + +function validateCidrPairOverlap(cidr1: string, cidr2: string): boolean { + const cidr1Range = new CidrBlock(cidr1); + const cidr1IpRange: [string, string] = [cidr1Range.minIp(), cidr1Range.maxIp()]; + + const cidr2Range = new CidrBlock(cidr2); + const cidr2IpRange: [string, string] = [cidr2Range.minIp(), cidr2Range.maxIp()]; + + return rangesOverlap(cidr1IpRange, cidr2IpRange); +} + +function rangesOverlap(range1: [string, string], range2: [string, string]): boolean { + const [start1, end1] = range1; + const [start2, end2] = range2; + + // Check if ranges overlap + return start1 <= end2 && start2 <= end1; +} diff --git a/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts b/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts index 254987e61065e..8e29aff660494 100644 --- a/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts +++ b/packages/aws-cdk-lib/aws-eks/test/cluster.test.ts @@ -3380,4 +3380,166 @@ describe('cluster', () => { }); + describe('RemoteNetworkConfig', () => { + test('create a cluster using remote network config with only remote node networks', () => { + // GIVEN + const { stack } = testFixture(); + const remoteNodeNetworkCidrs = ['172.16.0.0/12']; + + // WHEN + new eks.Cluster(stack, 'Cluster', { + version: CLUSTER_VERSION, + remoteNodeNetworks: [ + { + cidrs: remoteNodeNetworkCidrs, + }, + ], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { + Config: { + remoteNetworkConfig: { + remoteNodeNetworks: [ + { + cidrs: remoteNodeNetworkCidrs, + }, + ], + }, + }, + }); + }); + + test('create a cluster using remote network config with both remote node and pod networks', () => { + // GIVEN + const { stack } = testFixture(); + const remoteNodeNetworkCidrs = ['172.16.0.0/12']; + const remotePodNetworkCidrs = ['10.16.0.0/12']; + + // WHEN + new eks.Cluster(stack, 'Cluster', { + version: CLUSTER_VERSION, + remoteNodeNetworks: [ + { + cidrs: remoteNodeNetworkCidrs, + }, + ], + remotePodNetworks: [ + { + cidrs: remotePodNetworkCidrs, + }, + ], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { + Config: { + remoteNetworkConfig: { + remoteNodeNetworks: [ + { + cidrs: remoteNodeNetworkCidrs, + }, + ], + remotePodNetworks: [ + { + cidrs: remotePodNetworkCidrs, + }, + ], + }, + }, + }); + }); + + test('create a cluster using remote network config with overlapping remote node and pod networks', () => { + // GIVEN + const { stack } = testFixture(); + const overlappingCidr = '172.16.0.0/12'; + const remoteNodeNetworkCidrs = ['192.168.0.0/12', overlappingCidr]; + const remotePodNetworkCidrs = [overlappingCidr]; + + // WHEN + expect(() => { + new eks.Cluster(stack, 'Cluster', { + version: CLUSTER_VERSION, + remoteNodeNetworks: [ + { + cidrs: remoteNodeNetworkCidrs, + }, + ], + remotePodNetworks: [ + { + cidrs: remotePodNetworkCidrs, + }, + ], + }); + }).toThrow(`Remote node network CIDR block ${overlappingCidr} should not overlap with remote pod network CIDR block ${overlappingCidr}`); + }); + + test('create a cluster using remote network config with overlapping CIDRs across two different remote node networks', () => { + // GIVEN + const { stack } = testFixture(); + const overlappingCidr = '172.16.0.0/12'; + const remoteNodeNetworkCidrs1 = ['192.168.0.0/12', overlappingCidr]; + const remoteNodeNetworkCidrs2 = [overlappingCidr, '10.0.0.0/16']; + + // WHEN + expect(() => { + new eks.Cluster(stack, 'Cluster', { + version: CLUSTER_VERSION, + remoteNodeNetworks: [ + { + cidrs: remoteNodeNetworkCidrs1, + }, + { + cidrs: remoteNodeNetworkCidrs2, + }, + ], + }); + }).toThrow(`CIDR block ${overlappingCidr} in remote node network #1 should not overlap with CIDR block ${overlappingCidr} in remote node network #2`); + }); + + test('create a cluster using remote network config with overlapping CIDRs within the same remote node network', () => { + // GIVEN + const { stack } = testFixture(); + const overlappingCidr = '172.16.0.0/12'; + const remoteNodeNetworkCidrs = [overlappingCidr, overlappingCidr]; + + // WHEN + expect(() => { + new eks.Cluster(stack, 'Cluster', { + version: CLUSTER_VERSION, + remoteNodeNetworks: [ + { + cidrs: remoteNodeNetworkCidrs, + }, + ], + }); + }).toThrow(`CIDR ${overlappingCidr} should not overlap with CIDR ${overlappingCidr} in remote node network #1`); + }); + + test('create a cluster using remote network config with overlapping CIDRs within the same remote pod network', () => { + // GIVEN + const { stack } = testFixture(); + const overlappingCidr = '172.16.0.0/12'; + const remoteNodeNetworkCidrs = ['192.168.0.0/12']; + const remotePodNetworkCidrs = [overlappingCidr, overlappingCidr]; + + // WHEN + expect(() => { + new eks.Cluster(stack, 'Cluster', { + version: CLUSTER_VERSION, + remoteNodeNetworks: [ + { + cidrs: remoteNodeNetworkCidrs, + }, + ], + remotePodNetworks: [ + { + cidrs: remotePodNetworkCidrs, + }, + ], + }); + }).toThrow(`CIDR ${overlappingCidr} should not overlap with CIDR ${overlappingCidr} in remote pod network #1`); + }); + }); });