From 731b521380019297390c3260715d988646543626 Mon Sep 17 00:00:00 2001
From: Sumit Bhowmick <33891296+BhowmickSumit@users.noreply.github.com>
Date: Thu, 6 Feb 2025 21:22:35 +0530
Subject: [PATCH] Added EBS IO2 volumeType support to Managed NodeGroup
(#7989)
---
.../eksctl.io/v1alpha5/assets/schema.json | 15 ++++++-----
pkg/apis/eksctl.io/v1alpha5/defaults.go | 7 +++++
.../v1alpha5/outposts_validation_test.go | 1 +
pkg/apis/eksctl.io/v1alpha5/types.go | 5 ++++
pkg/apis/eksctl.io/v1alpha5/validation.go | 12 +++++++--
.../eksctl.io/v1alpha5/validation_test.go | 26 ++++++++++++++++++-
pkg/cfn/builder/block_device_mapping.go | 2 +-
pkg/cfn/builder/nodegroup_test.go | 12 +++++++++
pkg/ctl/cmdutils/configfile.go | 4 +++
9 files changed, 74 insertions(+), 10 deletions(-)
diff --git a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json
index bdc0add36b..0de36b3a56 100755
--- a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json
+++ b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json
@@ -1576,13 +1576,14 @@
},
"volumeType": {
"type": "string",
- "description": "Valid variants are: `\"gp2\"` is General Purpose SSD, `\"gp3\"` is General Purpose SSD which can be optimised for high throughput (default), `\"io1\"` is Provisioned IOPS SSD, `\"sc1\"` is Cold HDD, `\"st1\"` is Throughput Optimized HDD.",
- "x-intellij-html-description": "Valid variants are: "gp2"
is General Purpose SSD, "gp3"
is General Purpose SSD which can be optimised for high throughput (default), "io1"
is Provisioned IOPS SSD, "sc1"
is Cold HDD, "st1"
is Throughput Optimized HDD.",
+ "description": "Valid variants are: `\"gp2\"` is General Purpose SSD, `\"gp3\"` is General Purpose SSD which can be optimised for high throughput (default), `\"io1\"` is Provisioned IOPS SSD, `\"io2\"` is Provisioned IOPS SSD, `\"sc1\"` is Cold HDD, `\"st1\"` is Throughput Optimized HDD.",
+ "x-intellij-html-description": "Valid variants are: "gp2"
is General Purpose SSD, "gp3"
is General Purpose SSD which can be optimised for high throughput (default), "io1"
is Provisioned IOPS SSD, "io2"
is Provisioned IOPS SSD, "sc1"
is Cold HDD, "st1"
is Throughput Optimized HDD.",
"default": "gp3",
"enum": [
"gp2",
"gp3",
"io1",
+ "io2",
"sc1",
"st1"
]
@@ -1945,13 +1946,14 @@
},
"volumeType": {
"type": "string",
- "description": "Valid variants are: `\"gp2\"` is General Purpose SSD, `\"gp3\"` is General Purpose SSD which can be optimised for high throughput (default), `\"io1\"` is Provisioned IOPS SSD, `\"sc1\"` is Cold HDD, `\"st1\"` is Throughput Optimized HDD.",
- "x-intellij-html-description": "Valid variants are: "gp2"
is General Purpose SSD, "gp3"
is General Purpose SSD which can be optimised for high throughput (default), "io1"
is Provisioned IOPS SSD, "sc1"
is Cold HDD, "st1"
is Throughput Optimized HDD.",
+ "description": "Valid variants are: `\"gp2\"` is General Purpose SSD, `\"gp3\"` is General Purpose SSD which can be optimised for high throughput (default), `\"io1\"` is Provisioned IOPS SSD, `\"io2\"` is Provisioned IOPS SSD, `\"sc1\"` is Cold HDD, `\"st1\"` is Throughput Optimized HDD.",
+ "x-intellij-html-description": "Valid variants are: "gp2"
is General Purpose SSD, "gp3"
is General Purpose SSD which can be optimised for high throughput (default), "io1"
is Provisioned IOPS SSD, "io2"
is Provisioned IOPS SSD, "sc1"
is Cold HDD, "st1"
is Throughput Optimized HDD.",
"default": "gp3",
"enum": [
"gp2",
"gp3",
"io1",
+ "io2",
"sc1",
"st1"
]
@@ -2654,13 +2656,14 @@
},
"volumeType": {
"type": "string",
- "description": "Valid variants are: `\"gp2\"` is General Purpose SSD, `\"gp3\"` is General Purpose SSD which can be optimised for high throughput (default), `\"io1\"` is Provisioned IOPS SSD, `\"sc1\"` is Cold HDD, `\"st1\"` is Throughput Optimized HDD.",
- "x-intellij-html-description": "Valid variants are: "gp2"
is General Purpose SSD, "gp3"
is General Purpose SSD which can be optimised for high throughput (default), "io1"
is Provisioned IOPS SSD, "sc1"
is Cold HDD, "st1"
is Throughput Optimized HDD.",
+ "description": "Valid variants are: `\"gp2\"` is General Purpose SSD, `\"gp3\"` is General Purpose SSD which can be optimised for high throughput (default), `\"io1\"` is Provisioned IOPS SSD, `\"io2\"` is Provisioned IOPS SSD, `\"sc1\"` is Cold HDD, `\"st1\"` is Throughput Optimized HDD.",
+ "x-intellij-html-description": "Valid variants are: "gp2"
is General Purpose SSD, "gp3"
is General Purpose SSD which can be optimised for high throughput (default), "io1"
is Provisioned IOPS SSD, "io2"
is Provisioned IOPS SSD, "sc1"
is Cold HDD, "st1"
is Throughput Optimized HDD.",
"default": "gp3",
"enum": [
"gp2",
"gp3",
"io1",
+ "io2",
"sc1",
"st1"
]
diff --git a/pkg/apis/eksctl.io/v1alpha5/defaults.go b/pkg/apis/eksctl.io/v1alpha5/defaults.go
index 08fb81ba86..5406f5da5b 100644
--- a/pkg/apis/eksctl.io/v1alpha5/defaults.go
+++ b/pkg/apis/eksctl.io/v1alpha5/defaults.go
@@ -227,6 +227,10 @@ func setVolumeDefaults(ng *NodeGroupBase, controlPlaneOnOutposts bool, template
if ng.VolumeIOPS == nil {
ng.VolumeIOPS = aws.Int(DefaultNodeVolumeIO1IOPS)
}
+ case NodeVolumeTypeIO2:
+ if ng.VolumeIOPS == nil {
+ ng.VolumeIOPS = aws.Int(DefaultNodeVolumeIO2IOPS)
+ }
}
if ng.AMIFamily == NodeImageFamilyBottlerocket && !IsSetAndNonEmptyString(ng.VolumeName) {
@@ -254,6 +258,9 @@ func setDefaultsForAdditionalVolumes(ng *NodeGroupBase, controlPlaneOnOutposts b
if *av.VolumeType == NodeVolumeTypeIO1 && av.VolumeIOPS == nil {
ng.AdditionalVolumes[i].VolumeIOPS = aws.Int(DefaultNodeVolumeIO1IOPS)
}
+ if *av.VolumeType == NodeVolumeTypeIO2 && av.VolumeIOPS == nil {
+ ng.AdditionalVolumes[i].VolumeIOPS = aws.Int(DefaultNodeVolumeIO2IOPS)
+ }
}
}
diff --git a/pkg/apis/eksctl.io/v1alpha5/outposts_validation_test.go b/pkg/apis/eksctl.io/v1alpha5/outposts_validation_test.go
index 5f276e2ea1..ea8fcf57b5 100644
--- a/pkg/apis/eksctl.io/v1alpha5/outposts_validation_test.go
+++ b/pkg/apis/eksctl.io/v1alpha5/outposts_validation_test.go
@@ -314,6 +314,7 @@ var _ = Describe("Outposts validation", func() {
},
Entry(api.NodeVolumeTypeGP3, api.NodeVolumeTypeGP3, true),
Entry(api.NodeVolumeTypeIO1, api.NodeVolumeTypeIO1, true),
+ Entry(api.NodeVolumeTypeIO2, api.NodeVolumeTypeIO2, true),
Entry(api.NodeVolumeTypeSC1, api.NodeVolumeTypeSC1, true),
Entry(api.NodeVolumeTypeST1, api.NodeVolumeTypeST1, true),
Entry(api.NodeVolumeTypeGP2, api.NodeVolumeTypeGP2, false),
diff --git a/pkg/apis/eksctl.io/v1alpha5/types.go b/pkg/apis/eksctl.io/v1alpha5/types.go
index f0b5cacc77..9803ec48ab 100644
--- a/pkg/apis/eksctl.io/v1alpha5/types.go
+++ b/pkg/apis/eksctl.io/v1alpha5/types.go
@@ -393,6 +393,8 @@ const (
NodeVolumeTypeGP3 = "gp3"
// NodeVolumeTypeIO1 is Provisioned IOPS SSD
NodeVolumeTypeIO1 = "io1"
+ // NodeVolumeTypeIO2 is Provisioned IOPS SSD
+ NodeVolumeTypeIO2 = "io2"
// NodeVolumeTypeSC1 is Cold HDD
NodeVolumeTypeSC1 = "sc1"
// NodeVolumeTypeST1 is Throughput Optimized HDD
@@ -413,6 +415,8 @@ const (
DefaultNodeVolumeThroughput = 125
// DefaultNodeVolumeIO1IOPS defines the default throughput for io1 volumes, set to the min value
DefaultNodeVolumeIO1IOPS = 100
+ // DefaultNodeVolumeIO2IOPS defines the default throughput for io2 volumes, set to the min value
+ DefaultNodeVolumeIO2IOPS = 100
// DefaultNodeVolumeGP3IOPS defines the default throughput for gp3, set to the min value
DefaultNodeVolumeGP3IOPS = 3000
)
@@ -536,6 +540,7 @@ func SupportedNodeVolumeTypes() []string {
NodeVolumeTypeGP2,
NodeVolumeTypeGP3,
NodeVolumeTypeIO1,
+ NodeVolumeTypeIO2,
NodeVolumeTypeSC1,
NodeVolumeTypeST1,
}
diff --git a/pkg/apis/eksctl.io/v1alpha5/validation.go b/pkg/apis/eksctl.io/v1alpha5/validation.go
index d434a6597d..4b1bd2a7b1 100644
--- a/pkg/apis/eksctl.io/v1alpha5/validation.go
+++ b/pkg/apis/eksctl.io/v1alpha5/validation.go
@@ -31,6 +31,8 @@ const (
MaxThroughput = 1000
MinIO1Iops = DefaultNodeVolumeIO1IOPS
MaxIO1Iops = 64000
+ MinIO2Iops = DefaultNodeVolumeIO2IOPS
+ MaxIO2Iops = 256000
MinGP3Iops = DefaultNodeVolumeGP3IOPS
MaxGP3Iops = 16000
OneDay = 86400
@@ -774,8 +776,8 @@ func validateNodeGroupBase(np NodePool, path string, controlPlaneOnOutposts bool
func validateVolumeOpts(ng *NodeGroupBase, path string, controlPlaneOnOutposts bool) error {
if ng.VolumeType != nil {
volumeType := *ng.VolumeType
- if ng.VolumeIOPS != nil && !(volumeType == NodeVolumeTypeIO1 || volumeType == NodeVolumeTypeGP3) {
- return fmt.Errorf("%s.volumeIOPS is only supported for %s and %s volume types", path, NodeVolumeTypeIO1, NodeVolumeTypeGP3)
+ if ng.VolumeIOPS != nil && !(volumeType == NodeVolumeTypeIO1 || volumeType == NodeVolumeTypeIO2 || volumeType == NodeVolumeTypeGP3) {
+ return fmt.Errorf("%s.volumeIOPS is only supported for %s, %s and %s volume types", path, NodeVolumeTypeIO1, NodeVolumeTypeIO2, NodeVolumeTypeGP3)
}
if volumeType == NodeVolumeTypeIO1 {
@@ -784,6 +786,12 @@ func validateVolumeOpts(ng *NodeGroupBase, path string, controlPlaneOnOutposts b
}
}
+ if volumeType == NodeVolumeTypeIO2 {
+ if ng.VolumeIOPS != nil && !(*ng.VolumeIOPS >= MinIO2Iops && *ng.VolumeIOPS <= MaxIO2Iops) {
+ return fmt.Errorf("value for %s.volumeIOPS must be within range %d-%d", path, MinIO2Iops, MaxIO2Iops)
+ }
+ }
+
if ng.VolumeThroughput != nil && volumeType != NodeVolumeTypeGP3 {
return fmt.Errorf("%s.volumeThroughput is only supported for %s volume type", path, NodeVolumeTypeGP3)
}
diff --git a/pkg/apis/eksctl.io/v1alpha5/validation_test.go b/pkg/apis/eksctl.io/v1alpha5/validation_test.go
index e5d7b94242..a5f47fa73c 100644
--- a/pkg/apis/eksctl.io/v1alpha5/validation_test.go
+++ b/pkg/apis/eksctl.io/v1alpha5/validation_test.go
@@ -307,10 +307,34 @@ var _ = Describe("ClusterConfig validation", func() {
})
})
+ When("VolumeType is io2", func() {
+ BeforeEach(func() {
+ *ng0.VolumeType = api.NodeVolumeTypeIO2
+ })
+
+ It("does not fail", func() {
+ Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(Succeed())
+ })
+
+ When(fmt.Sprintf("the value of volumeIOPS is < %d", api.MinIO2Iops), func() {
+ It("returns an error", func() {
+ ng0.VolumeIOPS = aws.Int(api.MinIO2Iops - 1)
+ Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("value for nodeGroups[0].volumeIOPS must be within range 100-256000"))
+ })
+ })
+
+ When(fmt.Sprintf("the value of volumeIOPS is > %d", api.MaxIO2Iops), func() {
+ It("returns an error", func() {
+ ng0.VolumeIOPS = aws.Int(api.MaxIO2Iops + 1)
+ Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("value for nodeGroups[0].volumeIOPS must be within range 100-256000"))
+ })
+ })
+ })
+
When("VolumeType is one for which IOPS is not supported", func() {
It("returns an error", func() {
*ng0.VolumeType = api.NodeVolumeTypeGP2
- Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("nodeGroups[0].volumeIOPS is only supported for io1 and gp3 volume types"))
+ Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("nodeGroups[0].volumeIOPS is only supported for io1, io2 and gp3 volume types"))
})
})
})
diff --git a/pkg/cfn/builder/block_device_mapping.go b/pkg/cfn/builder/block_device_mapping.go
index 5358213cb3..c8d4919f9e 100644
--- a/pkg/cfn/builder/block_device_mapping.go
+++ b/pkg/cfn/builder/block_device_mapping.go
@@ -84,7 +84,7 @@ func makeBlockDeviceMapping(vm *api.VolumeMapping) *gfnec2.LaunchTemplate_BlockD
mapping.Ebs.KmsKeyId = gfnt.NewString(*vm.VolumeKmsKeyID)
}
- if (*vm.VolumeType == api.NodeVolumeTypeIO1 || *vm.VolumeType == api.NodeVolumeTypeGP3) && vm.VolumeIOPS != nil {
+ if (*vm.VolumeType == api.NodeVolumeTypeIO1 || *vm.VolumeType == api.NodeVolumeTypeIO2 || *vm.VolumeType == api.NodeVolumeTypeGP3) && vm.VolumeIOPS != nil {
mapping.Ebs.Iops = gfnt.NewInteger(*vm.VolumeIOPS)
}
diff --git a/pkg/cfn/builder/nodegroup_test.go b/pkg/cfn/builder/nodegroup_test.go
index c5f19c599d..a64b0d8af1 100644
--- a/pkg/cfn/builder/nodegroup_test.go
+++ b/pkg/cfn/builder/nodegroup_test.go
@@ -1204,6 +1204,18 @@ var _ = Describe("Unmanaged NodeGroup Template Builder", func() {
})
})
+ Context("ng.VolumeType is IO2", func() {
+ BeforeEach(func() {
+ ng.VolumeType = aws.String(api.NodeVolumeTypeIO1)
+ ng.VolumeIOPS = aws.Int(500)
+ })
+
+ It("IOPS are set on the block device mapping", func() {
+ mapping := ngTemplate.Resources["NodeGroupLaunchTemplate"].Properties.LaunchTemplateData.BlockDeviceMappings[0]
+ Expect(mapping.Ebs["Iops"]).To(Equal(float64(500)))
+ })
+ })
+
Context("ng.VolumeType is GP3", func() {
BeforeEach(func() {
ng.VolumeType = aws.String(api.NodeVolumeTypeGP3)
diff --git a/pkg/ctl/cmdutils/configfile.go b/pkg/ctl/cmdutils/configfile.go
index 15620fb220..d847462ab2 100644
--- a/pkg/ctl/cmdutils/configfile.go
+++ b/pkg/ctl/cmdutils/configfile.go
@@ -661,6 +661,10 @@ func normalizeNodeGroup(ng *api.NodeGroup, l *commonClusterConfigLoader) error {
return fmt.Errorf("%s volume type is not supported via flag --node-volume-type, please use a config file", api.NodeVolumeTypeIO1)
}
+ if *ng.VolumeType == api.NodeVolumeTypeIO2 {
+ return fmt.Errorf("%s volume type is not supported via flag --node-volume-type, please use a config file", api.NodeVolumeTypeIO2)
+ }
+
normalizeBaseNodeGroup(ng, l.CobraCommand)
return nil
}