Skip to content

Commit

Permalink
Add support for spot instances nodegroups
Browse files Browse the repository at this point in the history
  • Loading branch information
martina-if committed Jun 4, 2019
1 parent 29e74e2 commit e540267
Show file tree
Hide file tree
Showing 10 changed files with 399 additions and 37 deletions.
2 changes: 1 addition & 1 deletion examples/01-simple-cluster.yaml
@@ -1,5 +1,5 @@
# A simple example of ClusterConfig object:
---
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

Expand Down
20 changes: 20 additions & 0 deletions examples/08-spot-instances.yaml
@@ -0,0 +1,20 @@
# An example of ClusterConfig showing nodegroups with mixed instances (spot and on demand):
---
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig

metadata:
name: cluster-8
region: eu-central-1

nodeGroups:
- name: ng-1
minSize: 2
maxSize: 5
instancesDistribution:
maxPrice: 0.017
instanceTypes: ["t3.small", "t3.medium"] # At least two instance types should be specified
onDemandBaseCapacity: 0
onDemandPercentageAboveBaseCapacity: 100
spotInstancePools: 2

16 changes: 16 additions & 0 deletions pkg/apis/eksctl.io/v1alpha5/types.go
Expand Up @@ -380,6 +380,8 @@ type NodeGroup struct {
AMIFamily string `json:"amiFamily,omitempty"`
// +optional
InstanceType string `json:"instanceType,omitempty"`
//+optional
InstancesDistribution *NodeGroupInstancesDistribution `json:"instancesDistribution,omitempty"`
// +optional
AvailabilityZones []string `json:"availabilityZones,omitempty"`
// +optional
Expand Down Expand Up @@ -495,4 +497,18 @@ type (
// +optional
PublicKeyName *string `json:"publicKeyName,omitempty"`
}

// NodeGroupInstancesDistribution holds the configuration for spot instances
NodeGroupInstancesDistribution struct {
//+required
InstanceTypes []string `json:"instanceTypes,omitEmpty"`
// +optional
MaxPrice *float64 `json:"maxPrice,omitempty"`
//+optional
OnDemandBaseCapacity *int `json:"onDemandBaseCapacity,omitEmpty"`
//+optional
OnDemandPercentageAboveBaseCapacity *int `json:"onDemandPercentageAboveBaseCapacity,omitEmpty"`
//+optional
SpotInstancePools *int `json:"spotInstancePools,omitEmpty"`
}
)
42 changes: 42 additions & 0 deletions pkg/apis/eksctl.io/v1alpha5/validation.go
Expand Up @@ -152,6 +152,48 @@ func ValidateNodeGroup(i int, ng *NodeGroup) error {
}
}

if err := validateInstancesDistribution(ng); err != nil {
return err
}

return nil
}

func validateInstancesDistribution(ng *NodeGroup) error {
if ng.InstancesDistribution == nil {
return nil
}

if ng.InstanceType != "" && ng.InstanceType != "mixed" {
return fmt.Errorf("instanceType should be \"mixed\" or unset when using the mixed instances feature")
}

distribution := ng.InstancesDistribution
if distribution.InstanceTypes == nil || len(distribution.InstanceTypes) == 0 {
return fmt.Errorf("at least two instance types have to be specified for mixed nodegroups")
}

allInstanceTypes := make(map[string]bool)
for _, instanceType := range distribution.InstanceTypes {
allInstanceTypes[instanceType] = true
}

if len(allInstanceTypes) < 2 || len(allInstanceTypes) > 20 {
return fmt.Errorf("mixed nodegroups should have between 2 and 20 different instance types")
}

if distribution.OnDemandBaseCapacity != nil && *distribution.OnDemandBaseCapacity < 0 {
return fmt.Errorf("onDemandBaseCapacity should be 0 or more")
}

if distribution.OnDemandPercentageAboveBaseCapacity != nil && (*distribution.OnDemandPercentageAboveBaseCapacity < 0 || *distribution.OnDemandPercentageAboveBaseCapacity > 100) {
return fmt.Errorf("percentageAboveBase should be between 0 and 100")
}

if distribution.SpotInstancePools != nil && (*distribution.SpotInstancePools < 1 || *distribution.SpotInstancePools > 20) {
return fmt.Errorf("spotInstancePools should be between 1 and 20")
}

return nil
}

Expand Down
89 changes: 89 additions & 0 deletions pkg/apis/eksctl.io/v1alpha5/validation_test.go
Expand Up @@ -46,9 +46,98 @@ var _ = Describe("ConfigFile ssh flags validation", func() {
err := validateNodeGroupSSH(nil)
Expect(err).ToNot(HaveOccurred())
})

Context("Instances distribution", func() {

var ng *NodeGroup
BeforeEach(func() {
ng = &NodeGroup{
InstancesDistribution: &NodeGroupInstancesDistribution{
InstanceTypes: []string{"t3.medium", "t3.large"},
},
}
})

It("It doesn't panic when instance distribution is not enabled", func() {
ng.InstancesDistribution = nil
err := validateInstancesDistribution(ng)
Expect(err).ToNot(HaveOccurred())
})

It("It fails when instance distribution is enabled and instanceType is not empty or \"mixed\"", func() {
err := validateInstancesDistribution(ng)
Expect(err).ToNot(HaveOccurred())

ng.InstanceType = "t3.small"

err = validateInstancesDistribution(ng)
Expect(err).To(HaveOccurred())
})

It("It fails when the instance distribution doesn't have at least 2 different instance types", func() {
ng.InstanceType = "mixed"
ng.InstancesDistribution.InstanceTypes = []string{"t3.medium", "t3.medium"}

err := validateInstancesDistribution(ng)
Expect(err).To(HaveOccurred())

ng.InstanceType = "mixed"
ng.InstancesDistribution.InstanceTypes = []string{"t3.medium", "t3.small"}

err = validateInstancesDistribution(ng)
Expect(err).ToNot(HaveOccurred())
})

It("It fails when the onDemandBaseCapacity is not above 0", func() {
ng.InstancesDistribution.OnDemandBaseCapacity = newInt(-1)

err := validateInstancesDistribution(ng)
Expect(err).To(HaveOccurred())

ng.InstancesDistribution.OnDemandBaseCapacity = newInt(1)

err = validateInstancesDistribution(ng)
Expect(err).ToNot(HaveOccurred())
})

It("It fails when the spotInstancePools is not between 1 and 20", func() {
ng.InstancesDistribution.SpotInstancePools = newInt(0)

err := validateInstancesDistribution(ng)
Expect(err).To(HaveOccurred())

ng.InstancesDistribution.SpotInstancePools = newInt(21)
err = validateInstancesDistribution(ng)
Expect(err).To(HaveOccurred())

ng.InstancesDistribution.SpotInstancePools = newInt(2)
err = validateInstancesDistribution(ng)
Expect(err).ToNot(HaveOccurred())
})

It("It fails when the percentageAboveBase is not between 0 and 100", func() {
ng.InstancesDistribution.OnDemandPercentageAboveBaseCapacity = newInt(-1)

err := validateInstancesDistribution(ng)
Expect(err).To(HaveOccurred())

ng.InstancesDistribution.OnDemandPercentageAboveBaseCapacity = newInt(101)
err = validateInstancesDistribution(ng)
Expect(err).To(HaveOccurred())

ng.InstancesDistribution.OnDemandPercentageAboveBaseCapacity = newInt(50)
err = validateInstancesDistribution(ng)
Expect(err).ToNot(HaveOccurred())
})
})
})

func checkItDetectsError(SSHConfig *NodeGroupSSH) {
err := validateNodeGroupSSH(SSHConfig)
Expect(err).To(HaveOccurred())
}

func newInt(value int) *int {
v := value
return &v
}
2 changes: 1 addition & 1 deletion pkg/apis/eksctl.io/v1alpha5/vpc.go
Expand Up @@ -155,7 +155,7 @@ func (c *ClusterConfig) HasSufficientPublicSubnets() bool {
}

var errInsufficientSubnets = fmt.Errorf(
"inssuficient number of subnets, at least %dx public and/or %dx private subnets are required",
"insufficient number of subnets, at least %dx public and/or %dx private subnets are required",
MinRequiredSubnets, MinRequiredSubnets)

// HasSufficientSubnets validates if there is a sufficient number
Expand Down
61 changes: 61 additions & 0 deletions pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

74 changes: 73 additions & 1 deletion pkg/cfn/builder/api_test.go
Expand Up @@ -89,6 +89,23 @@ type Properties struct {
SecurityGroupIds []interface{}
SubnetIds []interface{}
}
MixedInstancesPolicy *struct {
LaunchTemplate struct {
LaunchTemplateSpecification struct {
LaunchTemplateName map[string]string
Version map[string]string
Overrides []struct {
InstanceType string
}
}
}
InstancesDistribution struct {
OnDemandBaseCapacity string
OnDemandPercentageAboveBaseCapacity string
SpotMaxPrice string
SpotInstancePools string
}
}
}

type LaunchTemplateData struct {
Expand All @@ -99,6 +116,13 @@ type LaunchTemplateData struct {
DeviceIndex int
AssociatePublicIpAddress bool
}
InstanceMarketOptions *struct {
MarketType string
SpotOptions struct {
SpotInstanceType string
MaxPrice string
}
}
}

type Template struct {
Expand Down Expand Up @@ -1132,7 +1156,7 @@ var _ = Describe("CloudFormation template builder API", func() {
})
})

Context("NodeGroup with cutom role and profile", func() {
Context("NodeGroup with custom role and profile", func() {
cfg, ng := newClusterConfigAndNodegroup(true)

ng.IAM.InstanceRoleARN = "arn:role"
Expand Down Expand Up @@ -2160,6 +2184,54 @@ var _ = Describe("CloudFormation template builder API", func() {
})

})

Context("Nodegroup with Mixed instances", func() {
cfg, ng := newClusterConfigAndNodegroup(true)

maxSpotPrice := 0.045
baseCap := 40
percentageOnDemand := 20
pools := 3
ng.InstancesDistribution = &api.NodeGroupInstancesDistribution{
MaxPrice: &maxSpotPrice,
InstanceTypes: []string{"m5.large", "m5a.xlarge"},
OnDemandBaseCapacity: &baseCap,
OnDemandPercentageAboveBaseCapacity: &percentageOnDemand,
SpotInstancePools: &pools,
}

zero := 0
ng.MinSize = &zero
ng.MaxSize = &zero

build(cfg, "eksctl-test-spot-cluster", ng)

roundtrip()

It("should have mixed instances with correct max price", func() {
Expect(ngTemplate.Resources).To(HaveKey("NodeGroupLaunchTemplate"))

launchTemplateData := getLaunchTemplateData(ngTemplate)
Expect(launchTemplateData.InstanceMarketOptions).To(BeNil())

nodeGroupProperties := getNodeGroupProperties(ngTemplate)
Expect(nodeGroupProperties.MinSize).To(Equal("0"))
Expect(nodeGroupProperties.MaxSize).To(Equal("0"))
Expect(nodeGroupProperties.DesiredCapacity).To(Equal(""))

Expect(nodeGroupProperties.MixedInstancesPolicy).To(Not(BeNil()))
Expect(nodeGroupProperties.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification.LaunchTemplateName["Fn::Sub"]).To(Equal("${AWS::StackName}"))
Expect(nodeGroupProperties.MixedInstancesPolicy.LaunchTemplate.LaunchTemplateSpecification.Version["Fn::GetAtt"]).To(Equal("NodeGroupLaunchTemplate.LatestVersionNumber"))
Expect(nodeGroupProperties.MixedInstancesPolicy.LaunchTemplate).To(Not(BeNil()))

Expect(nodeGroupProperties.MixedInstancesPolicy.InstancesDistribution).To(Not(BeNil()))
Expect(nodeGroupProperties.MixedInstancesPolicy.InstancesDistribution.OnDemandBaseCapacity).To(Equal("40"))
Expect(nodeGroupProperties.MixedInstancesPolicy.InstancesDistribution.OnDemandPercentageAboveBaseCapacity).To(Equal("20"))
Expect(nodeGroupProperties.MixedInstancesPolicy.InstancesDistribution.SpotInstancePools).To(Equal("3"))
Expect(nodeGroupProperties.MixedInstancesPolicy.InstancesDistribution.SpotMaxPrice).To(Equal("0.045000"))

})
})
})

func setSubnets(cfg *api.ClusterConfig) {
Expand Down

0 comments on commit e540267

Please sign in to comment.