Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eks): spot support for managed nodegroups #11962

Merged
merged 29 commits into from
Dec 22, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
78717a7
Fixes aws/aws-cdk#11827
pahud Dec 9, 2020
554d0a4
chore(doc): update README
pahud Dec 9, 2020
93d300a
chore(doc): minor
pahud Dec 9, 2020
05a2c93
Merge branch 'master' into pahud/aws-eks-managed-nodegroup-11827
pahud Dec 9, 2020
7abc1cb
Merge branch 'master' into pahud/aws-eks-managed-nodegroup-11827
pahud Dec 11, 2020
0dd4b3c
Merge branch 'master' into pahud/aws-eks-managed-nodegroup-11827
pahud Dec 15, 2020
af765cd
use instanceTypes instead
pahud Dec 16, 2020
cf53f7f
Merge branch 'master' into pahud/aws-eks-managed-nodegroup-11827
pahud Dec 16, 2020
1d12a05
add integ tests
pahud Dec 16, 2020
ef1dff4
minor
pahud Dec 16, 2020
53c7c35
minor
pahud Dec 16, 2020
0e78865
minor
pahud Dec 16, 2020
d2d9ca5
add annotations
pahud Dec 16, 2020
1a02392
minor
pahud Dec 17, 2020
481e6f1
Update packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts
pahud Dec 18, 2020
f05ddbb
Update packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts
pahud Dec 18, 2020
0ab46c4
Merge branch 'master' into pahud/aws-eks-managed-nodegroup-11827
pahud Dec 18, 2020
fb13656
update the helper function
pahud Dec 18, 2020
9888f2f
Update packages/@aws-cdk/aws-eks/test/test.nodegroup.ts
pahud Dec 18, 2020
cdd304f
fix tests
pahud Dec 18, 2020
50c2ac6
minor
pahud Dec 18, 2020
7ec29a5
Merge branch 'master' into pahud/aws-eks-managed-nodegroup-11827
pahud Dec 22, 2020
437256c
Merge branch 'master' into pahud/aws-eks-managed-nodegroup-11827
pahud Dec 22, 2020
47725ad
fix tests
pahud Dec 22, 2020
a20441f
fix tests
pahud Dec 22, 2020
ef0ccd9
Some rephrasing
iliapolo Dec 22, 2020
265ffae
Some rephrasing
iliapolo Dec 22, 2020
6104476
Some rephrasing
iliapolo Dec 22, 2020
394cefe
Merge branch 'master' into pahud/aws-eks-managed-nodegroup-11827
iliapolo Dec 22, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions packages/@aws-cdk/aws-eks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,14 +201,35 @@ const cluster = new eks.Cluster(this, 'HelloEKS', {
});

cluster.addNodegroupCapacity('custom-node-group', {
instanceType: new ec2.InstanceType('m5.large'),
instanceTypes: [new ec2.InstanceType('m5.large')],
minSize: 4,
diskSize: 100,
amiType: eks.NodegroupAmiType.AL2_X86_64_GPU,
...
});
```

#### Spot Instances Support

Use `capacityType` to create the Spot managed node groups. To maximize the availability of your applications while using
iliapolo marked this conversation as resolved.
Show resolved Hide resolved
Spot Instances, we recommend that you configure a Spot managed node group to use multiple instance types with `instanceTypes`.
iliapolo marked this conversation as resolved.
Show resolved Hide resolved

> For more details visit [Managed node group capacity types](https://docs.aws.amazon.com/eks/latest/userguide/managed-node-groups.html#managed-node-group-capacity-types).


```ts
cluster.addNodegroupCapacity('extra-ng-spot', {
instanceTypes: [
new ec2.InstanceType('c5.large'),
new ec2.InstanceType('c5a.large'),
new ec2.InstanceType('c5d.large'),
],
minSize: 3,
capacityType: eks.CapacityType.SPOT,
});

```

#### Launch Template Support

You can specify a launch template that the node group will use. Note that when using a custom AMI, Amazon EKS doesn't merge any user data.
Expand Down Expand Up @@ -236,7 +257,9 @@ cluster.addNodegroupCapacity('extra-ng', {
});
```

> For more details visit [Launch Template Support](https://docs.aws.amazon.com/en_ca/eks/latest/userguide/launch-templates.html).
You may specify one or multiple instance types in either `instanceTypes` property of `NodeGroup` or in the launch template but not both.
iliapolo marked this conversation as resolved.
Show resolved Hide resolved

> For more details visit [Launch Template Support](https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html).

Graviton 2 instance types are supported including `c6g`, `m6g`, `r6g` and `t4g`.

Expand Down Expand Up @@ -540,7 +563,7 @@ Amazon Linux 2 AMI for ARM64 will be automatically selected.
```ts
// add a managed ARM64 nodegroup
cluster.addNodegroupCapacity('extra-ng-arm', {
instanceType: new ec2.InstanceType('m6g.medium'),
instanceTypes: [new ec2.InstanceType('m6g.medium')],
minSize: 2,
});

Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-eks/lib/cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1131,7 +1131,7 @@ export class Cluster extends ClusterBase {
this.addAutoScalingGroupCapacity('DefaultCapacity', { instanceType, minCapacity }) : undefined;

this.defaultNodegroup = props.defaultCapacityType !== DefaultCapacityType.EC2 ?
this.addNodegroupCapacity('DefaultCapacity', { instanceType, minSize: minCapacity }) : undefined;
this.addNodegroupCapacity('DefaultCapacity', { instanceTypes: [instanceType], minSize: minCapacity }) : undefined;
}

const outputConfigCommand = props.outputConfigCommand === undefined ? true : props.outputConfigCommand;
Expand Down
74 changes: 63 additions & 11 deletions packages/@aws-cdk/aws-eks/lib/managed-nodegroup.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { InstanceType, ISecurityGroup, SubnetSelection } from '@aws-cdk/aws-ec2';
import { IRole, ManagedPolicy, Role, ServicePrincipal } from '@aws-cdk/aws-iam';
import { IResource, Resource } from '@aws-cdk/core';
import { IResource, Resource, Annotations } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { Cluster, ICluster } from './cluster';
import { CfnNodegroup } from './eks.generated';
Expand Down Expand Up @@ -37,6 +37,20 @@ export enum NodegroupAmiType {
AL2_ARM_64 = 'AL2_ARM_64'
}

/**
* Capacity type of the managed node group
*/
export enum CapacityType {
/**
* spot instances
*/
SPOT = 'SPOT',
/**
* on-demand instances
*/
ON_DEMAND = 'ON_DEMAND'
}

/**
* The remote access (SSH) configuration to use with your node group.
*
Expand Down Expand Up @@ -95,7 +109,7 @@ export interface NodegroupOptions {
/**
* The AMI type for your node group.
*
* @default - auto-determined from the instanceType property.
* @default - auto-determined from the instanceTypes property.
*/
readonly amiType?: NodegroupAmiType;
/**
Expand Down Expand Up @@ -135,11 +149,18 @@ export interface NodegroupOptions {
/**
* The instance type to use for your node group. Currently, you can specify a single instance type for a node group.
* The default value for this parameter is `t3.medium`. If you choose a GPU instance type, be sure to specify the
* `AL2_x86_64_GPU` with the amiType parameter.
* `AL2_x86_64_GPU` with the amiType parameter. This property will be ignored if `instanceTypes` is defined.
pahud marked this conversation as resolved.
Show resolved Hide resolved
*
* @default t3.medium
* @deprecated Use `instanceTypes` instead.
*/
readonly instanceType?: InstanceType;
/**
* The instance types to use for your node group.
* @default t3.medium will be used according to the cloudformation document.
* @see - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-nodegroup.html#cfn-eks-nodegroup-instancetypes
*/
readonly instanceTypes?: InstanceType[];
/**
* The Kubernetes labels to be applied to the nodes in the node group when they are created.
*
Expand Down Expand Up @@ -183,6 +204,12 @@ export interface NodegroupOptions {
* @default - no launch template
*/
readonly launchTemplateSpec?: LaunchTemplateSpec;
/**
* The capacity type of the nodegroup.
*
* @default - ON_DEMAND
*/
readonly capacityType?: CapacityType;
}

/**
Expand Down Expand Up @@ -246,13 +273,24 @@ export class Nodegroup extends Resource implements INodegroup {
this.maxSize = props.maxSize ?? this.desiredSize;
this.minSize = props.minSize ?? 1;

const DEFAULT_INSTANCE_TYPE = new InstanceType('t3.medium');
pahud marked this conversation as resolved.
Show resolved Hide resolved

if (this.desiredSize > this.maxSize) {
throw new Error(`Desired capacity ${this.desiredSize} can't be greater than max size ${this.maxSize}`);
}
if (this.desiredSize < this.minSize) {
throw new Error(`Minimum capacity ${this.minSize} can't be greater than desired size ${this.desiredSize}`);
}

if (props.instanceType) {
Annotations.of(this).addWarning('instanceType will be deprecated, remember to use instanceTypes instead.');
pahud marked this conversation as resolved.
Show resolved Hide resolved
}
const instanceTypes = props.instanceTypes ?? (props.instanceType ? [props.instanceType] : [DEFAULT_INSTANCE_TYPE]);
const determinedAmiType = determineAmiTypes(instanceTypes);
if (props.amiType && props.amiType !== determinedAmiType) {
throw new Error(`amiType is not correct - should be ${determinedAmiType}`);
pahud marked this conversation as resolved.
Show resolved Hide resolved
}

if (!props.nodeRole) {
const ngRole = new Role(this, 'NodeGroupRole', {
assumedBy: new ServicePrincipal('ec2.amazonaws.com'),
Expand All @@ -271,11 +309,12 @@ export class Nodegroup extends Resource implements INodegroup {
nodegroupName: props.nodegroupName,
nodeRole: this.role.roleArn,
subnets: this.cluster.vpc.selectSubnets(props.subnets).subnetIds,
amiType: props.amiType ?? (props.instanceType ? getAmiTypeForInstanceType(props.instanceType).toString() :
undefined),
// AmyType is not allowed by CFN when specifying an image id in your launch template.
amiType: props.launchTemplateSpec == undefined ? determinedAmiType : undefined,
pahud marked this conversation as resolved.
Show resolved Hide resolved
diskSize: props.diskSize,
forceUpdateEnabled: props.forceUpdate ?? true,
instanceTypes: props.instanceType ? [props.instanceType.toString()] : undefined,
instanceTypes: props.instanceTypes ? props.instanceTypes.map(t => t.toString()) :
props.instanceType ? [props.instanceType.toString()] : undefined,
labels: props.labels,
releaseVersion: props.releaseVersion,
remoteAccess: props.remoteAccess ? {
Expand All @@ -291,17 +330,21 @@ export class Nodegroup extends Resource implements INodegroup {
tags: props.tags,
});

if (props.capacityType) {
resource.addPropertyOverride('CapacityType', props.capacityType.valueOf());
pahud marked this conversation as resolved.
Show resolved Hide resolved
}

if (props.launchTemplateSpec) {
if (props.diskSize) {
// see - https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html
// and https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-nodegroup.html#cfn-eks-nodegroup-disksize
throw new Error('diskSize must be specified within the launch template');
}
if (props.instanceType) {
pahud marked this conversation as resolved.
Show resolved Hide resolved
// see - https://docs.aws.amazon.com/eks/latest/userguide/launch-templates.html
// and https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-nodegroup.html#cfn-eks-nodegroup-disksize
throw new Error('Instance types must be specified within the launch template');
}
/**
* Instance types can be specified either in `instanceType` or launch template but not both. AS we can not check the content of
* the provided launch template and the `instanceType` property is preferrable. We allow users to define `instanceType` property here.
* see - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-eks-nodegroup.html#cfn-eks-nodegroup-instancetypes
*/
// TODO: update this when the L1 resource spec is updated.
resource.addPropertyOverride('LaunchTemplate', {
Id: props.launchTemplateSpec.id,
Expand Down Expand Up @@ -340,3 +383,12 @@ function getAmiTypeForInstanceType(instanceType: InstanceType) {
NodegroupAmiType.AL2_X86_64;
}

function determineAmiTypes(instanceType: InstanceType[]) {
const amiTypes = instanceType.map(i =>getAmiTypeForInstanceType(i));
const uniq = [...new Set(amiTypes)];
if (uniq.length > 1) {
throw new Error('instanceTypes of different CPU architectures not allowed');
pahud marked this conversation as resolved.
Show resolved Hide resolved
} else {
return uniq[0];
}
}
pahud marked this conversation as resolved.
Show resolved Hide resolved
110 changes: 110 additions & 0 deletions packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json
Original file line number Diff line number Diff line change
Expand Up @@ -1251,6 +1251,13 @@
]
},
"\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]},{\\\"rolearn\\\":\\\"",
{
"Fn::GetAtt": [
"ClusterNodegroupextrangspotNodeGroupRoleB53B4857",
"Arn"
]
},
"\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]},{\\\"rolearn\\\":\\\"",
{
"Fn::GetAtt": [
"ClusterNodegroupextrangarmNodeGroupRoleADF5749F",
Expand Down Expand Up @@ -3251,6 +3258,109 @@
}
}
},
"ClusterNodegroupextrangspotNodeGroupRoleB53B4857": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Statement": [
{
"Action": "sts:AssumeRole",
"Effect": "Allow",
"Principal": {
"Service": {
"Fn::Join": [
"",
[
"ec2.",
{
"Ref": "AWS::URLSuffix"
}
]
]
}
}
}
],
"Version": "2012-10-17"
},
"ManagedPolicyArns": [
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/AmazonEKSWorkerNodePolicy"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/AmazonEKS_CNI_Policy"
]
]
},
{
"Fn::Join": [
"",
[
"arn:",
{
"Ref": "AWS::Partition"
},
":iam::aws:policy/AmazonEC2ContainerRegistryReadOnly"
]
]
}
]
}
},
"ClusterNodegroupextrangspotB327AE6B": {
"Type": "AWS::EKS::Nodegroup",
"Properties": {
"ClusterName": {
"Ref": "Cluster9EE0221C"
},
"NodeRole": {
"Fn::GetAtt": [
"ClusterNodegroupextrangspotNodeGroupRoleB53B4857",
"Arn"
]
},
"Subnets": [
{
"Ref": "VpcPrivateSubnet1Subnet536B997A"
},
{
"Ref": "VpcPrivateSubnet2Subnet3788AAA1"
},
{
"Ref": "VpcPrivateSubnet3SubnetF258B56E"
}
],
"AmiType": "AL2_x86_64",
"ForceUpdateEnabled": true,
"InstanceTypes": [
"c5.large",
"c5a.large",
"c5d.large"
],
"ScalingConfig": {
"DesiredSize": 3,
"MaxSize": 3,
"MinSize": 3
},
"CapacityType": "SPOT"
}
},
"ClusterNodegroupextrangarmNodeGroupRoleADF5749F": {
"Type": "AWS::IAM::Role",
"Properties": {
Expand Down
16 changes: 16 additions & 0 deletions packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ class EksClusterStack extends TestStack {

this.assertNodeGroupX86();

this.assertNodeGroupSpot();

this.assertNodeGroupArm();

this.assertNodeGroupCustomAmi();
Expand Down Expand Up @@ -146,6 +148,20 @@ class EksClusterStack extends TestStack {
nodeRole: this.cluster.defaultCapacity ? this.cluster.defaultCapacity.role : undefined,
});
}
private assertNodeGroupSpot() {
// add a extra nodegroup
this.cluster.addNodegroupCapacity('extra-ng-spot', {
instanceTypes: [
new ec2.InstanceType('c5.large'),
new ec2.InstanceType('c5a.large'),
new ec2.InstanceType('c5d.large'),
],
minSize: 3,
// reusing the default capacity nodegroup instance role when available
nodeRole: this.cluster.defaultCapacity ? this.cluster.defaultCapacity.role : undefined,
capacityType: eks.CapacityType.SPOT,
});
}
private assertNodeGroupCustomAmi() {
// add a extra nodegroup
const userData = ec2.UserData.forLinux();
Expand Down
Loading