Skip to content

Commit

Permalink
Add L2 constructs to support EKS Hybrid Nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
abhay-krishna committed Dec 9, 2024
1 parent 06cdaac commit b250b8c
Show file tree
Hide file tree
Showing 4 changed files with 333 additions and 1 deletion.
23 changes: 23 additions & 0 deletions packages/aws-cdk-lib/aws-eks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion packages/aws-cdk-lib/aws-eks/lib/cluster-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/**
Expand Down Expand Up @@ -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,

Expand All @@ -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,
},
});

Expand Down
145 changes: 145 additions & 0 deletions packages/aws-cdk-lib/aws-eks/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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[];
}

/**
Expand Down Expand Up @@ -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', {
Expand All @@ -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,
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -2675,3 +2789,34 @@ function cpuArchForInstanceType(instanceType: ec2.InstanceType) {
function flatten<A>(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;
}
Loading

0 comments on commit b250b8c

Please sign in to comment.