diff --git a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json
index a962912d76..dabc3eee1d 100755
--- a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json
+++ b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json
@@ -749,6 +749,11 @@
"description": "enables [EBS optimization](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-optimized.html)",
"x-intellij-html-description": "enables EBS optimization"
},
+ "cpuCredits": {
+ "type": "string",
+ "description": "configures [T3 Unlimited](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances-unlimited-mode.html), valid only for T-type instances. Possible values: 'unlimited' or 'standard'.",
+ "x-intellij-html-description": "configures T3 Unlimited, valid only for T-type instances. Possible values: 'unlimited' or 'standard'."
+ },
"iam": {
"$ref": "#/definitions/NodeGroupIAM"
},
@@ -896,6 +901,7 @@
"securityGroups",
"asgMetricsCollection",
"ebsOptimized",
+ "cpuCredits",
"volumeType",
"volumeName",
"volumeEncrypted",
diff --git a/pkg/apis/eksctl.io/v1alpha5/types.go b/pkg/apis/eksctl.io/v1alpha5/types.go
index f864683e83..a2e7178740 100644
--- a/pkg/apis/eksctl.io/v1alpha5/types.go
+++ b/pkg/apis/eksctl.io/v1alpha5/types.go
@@ -687,6 +687,10 @@ type NodeGroup struct {
// +optional
EBSOptimized *bool `json:"ebsOptimized,omitempty"`
+ // CPUCredits configures [T3 Unlimited](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/burstable-performance-instances-unlimited-mode.html), valid only for T-type instances
+ // +optional
+ CPUCredits *string `json:"cpuCredits,omitempty"`
+
// Valid variants are `VolumeType` constants
// +optional
VolumeType *string `json:"volumeType,omitempty"`
diff --git a/pkg/apis/eksctl.io/v1alpha5/validation.go b/pkg/apis/eksctl.io/v1alpha5/validation.go
index eb0374ba4a..ce600a7765 100644
--- a/pkg/apis/eksctl.io/v1alpha5/validation.go
+++ b/pkg/apis/eksctl.io/v1alpha5/validation.go
@@ -260,6 +260,10 @@ func ValidateNodeGroup(i int, ng *NodeGroup) error {
return err
}
+ if err := validateCPUCredits(ng); err != nil {
+ return err
+ }
+
return nil
}
@@ -466,6 +470,35 @@ func validateInstancesDistribution(ng *NodeGroup) error {
return nil
}
+func validateCPUCredits(ng *NodeGroup) error {
+ isTInstance := false
+ instanceTypes := []string{ng.InstanceType}
+
+ if ng.CPUCredits == nil {
+ return nil
+ }
+
+ if ng.InstanceType == "mixed" {
+ instanceTypes = ng.InstancesDistribution.InstanceTypes
+ }
+
+ for _, instanceType := range instanceTypes {
+ if strings.HasPrefix(instanceType, "t") {
+ isTInstance = true
+ }
+ }
+
+ if !isTInstance {
+ return fmt.Errorf("cpuCredits option set for nodegroup, but it has no t2/t3 instance types")
+ }
+
+ if strings.ToLower(*ng.CPUCredits) != "unlimited" && strings.ToLower(*ng.CPUCredits) != "standard" {
+ return fmt.Errorf("cpuCredits option accepts only one of 'standard' or 'unlimited'")
+ }
+
+ return nil
+}
+
func validateNodeGroupSSH(SSH *NodeGroupSSH) error {
numSSHFlagsEnabled := countEnabledFields(
SSH.PublicKeyPath,
diff --git a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go
index ad5ce4472f..a544896c9b 100644
--- a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go
+++ b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go
@@ -701,6 +701,11 @@ func (in *NodeGroup) DeepCopyInto(out *NodeGroup) {
*out = new(bool)
**out = **in
}
+ if in.CPUCredits != nil {
+ in, out := &in.CPUCredits, &out.CPUCredits
+ *out = new(string)
+ **out = **in
+ }
if in.VolumeType != nil {
in, out := &in.VolumeType, &out.VolumeType
*out = new(string)
diff --git a/pkg/cfn/builder/api_test.go b/pkg/cfn/builder/api_test.go
index 935003972a..ff2af375f9 100644
--- a/pkg/cfn/builder/api_test.go
+++ b/pkg/cfn/builder/api_test.go
@@ -132,6 +132,9 @@ type LaunchTemplateData struct {
MaxPrice string
}
}
+ CreditSpecification *struct {
+ CPUCredits string
+ }
}
type Template struct {
@@ -2882,14 +2885,18 @@ var _ = Describe("CloudFormation template builder API", func() {
})
+ maxSpotPrice := 0.045
+ baseCap := 40
+ percentageOnDemand := 20
+ pools := 3
+ spotAllocationStrategy := "lowest-price"
+ zero := 0
+ cpuCreditsUnlimited := "unlimited"
+ cpuCreditsStandard := "standard"
+
Context("Nodegroup with Mixed instances", func() {
cfg, ng := newClusterConfigAndNodegroup(true)
- maxSpotPrice := 0.045
- baseCap := 40
- percentageOnDemand := 20
- pools := 3
- spotAllocationStrategy := "lowest-price"
ng.InstanceType = "mixed"
ng.InstancesDistribution = &api.NodeGroupInstancesDistribution{
MaxPrice: &maxSpotPrice,
@@ -2900,7 +2907,6 @@ var _ = Describe("CloudFormation template builder API", func() {
SpotAllocationStrategy: &spotAllocationStrategy,
}
- zero := 0
ng.MinSize = &zero
ng.MaxSize = &zero
@@ -2934,6 +2940,69 @@ var _ = Describe("CloudFormation template builder API", func() {
})
})
+
+ Context("NodeGroup{CPUCredits=nil}", func() {
+ cfg, ng := newClusterConfigAndNodegroup(true)
+
+ build(cfg, "eksctl-test-t3-unlimited", ng)
+
+ roundtrip()
+
+ It("should have correct resources and attributes", func() {
+ Expect(getLaunchTemplateData(ngTemplate).CreditSpecification).To(BeNil())
+ })
+ })
+
+ Context("NodeGroup{CPUCredits=standard InstancesDistribution.InstanceTypes=t3.medium,t3a.medium}", func() {
+ cfg, ng := newClusterConfigAndNodegroup(true)
+
+ ng.InstanceType = "mixed"
+ ng.CPUCredits = &cpuCreditsStandard
+ ng.InstancesDistribution = &api.NodeGroupInstancesDistribution{
+ MaxPrice: &maxSpotPrice,
+ InstanceTypes: []string{"t3.medium", "t3a.medium"},
+ OnDemandBaseCapacity: &baseCap,
+ OnDemandPercentageAboveBaseCapacity: &percentageOnDemand,
+ SpotInstancePools: &pools,
+ SpotAllocationStrategy: &spotAllocationStrategy,
+ }
+
+ build(cfg, "eksctl-test-t3-unlimited", ng)
+
+ roundtrip()
+
+ It("should have correct resources and attributes", func() {
+ Expect(getLaunchTemplateData(ngTemplate).CreditSpecification).ToNot(BeNil())
+ Expect(getLaunchTemplateData(ngTemplate).CreditSpecification.CPUCredits).ToNot(BeNil())
+ Expect(getLaunchTemplateData(ngTemplate).CreditSpecification.CPUCredits).To(Equal("standard"))
+ })
+ })
+
+ Context("NodeGroup{CPUCredits=unlimited InstancesDistribution.InstanceTypes=t3.medium,t3a.medium}", func() {
+ cfg, ng := newClusterConfigAndNodegroup(true)
+
+ ng.InstanceType = "mixed"
+ ng.CPUCredits = &cpuCreditsUnlimited
+ ng.InstancesDistribution = &api.NodeGroupInstancesDistribution{
+ MaxPrice: &maxSpotPrice,
+ InstanceTypes: []string{"t3.medium", "t3a.medium"},
+ OnDemandBaseCapacity: &baseCap,
+ OnDemandPercentageAboveBaseCapacity: &percentageOnDemand,
+ SpotInstancePools: &pools,
+ SpotAllocationStrategy: &spotAllocationStrategy,
+ }
+
+ build(cfg, "eksctl-test-t3-unlimited", ng)
+
+ roundtrip()
+
+ It("should have correct resources and attributes", func() {
+ Expect(getLaunchTemplateData(ngTemplate).CreditSpecification).ToNot(BeNil())
+ Expect(getLaunchTemplateData(ngTemplate).CreditSpecification.CPUCredits).ToNot(BeNil())
+ Expect(getLaunchTemplateData(ngTemplate).CreditSpecification.CPUCredits).To(Equal("unlimited"))
+ })
+ })
+
})
func setSubnets(cfg *api.ClusterConfig) {
diff --git a/pkg/cfn/builder/nodegroup.go b/pkg/cfn/builder/nodegroup.go
index 5736fae8cd..ba59ff369d 100644
--- a/pkg/cfn/builder/nodegroup.go
+++ b/pkg/cfn/builder/nodegroup.go
@@ -267,6 +267,7 @@ func newLaunchTemplateData(n *NodeGroupResourceSet) *gfn.AWSEC2LaunchTemplate_La
HttpPutResponseHopLimit: gfn.NewInteger(2),
},
}
+
if !api.HasMixedInstances(n.spec) {
launchTemplateData.InstanceType = gfn.NewString(n.spec.InstanceType)
} else {
@@ -276,6 +277,12 @@ func newLaunchTemplateData(n *NodeGroupResourceSet) *gfn.AWSEC2LaunchTemplate_La
launchTemplateData.EbsOptimized = gfn.NewBoolean(*n.spec.EBSOptimized)
}
+ if n.spec.CPUCredits != nil {
+ launchTemplateData.CreditSpecification = &gfn.AWSEC2LaunchTemplate_CreditSpecification{
+ CpuCredits: gfn.NewString(strings.ToLower(*n.spec.CPUCredits)),
+ }
+ }
+
return launchTemplateData
}