diff --git a/builder/amazon/common/block_device.go b/builder/amazon/common/block_device.go index 55654de14..3f1a31cdf 100644 --- a/builder/amazon/common/block_device.go +++ b/builder/amazon/common/block_device.go @@ -13,6 +13,11 @@ import ( "github.com/hashicorp/packer/packer-plugin-sdk/template/interpolate" ) +const ( + minIops = 100 + maxIops = 64000 +) + // These will be attached when launching your instance. Your // options here may vary depending on the type of VM you use. // @@ -154,6 +159,11 @@ func (blockDevice BlockDevice) BuildEC2BlockDeviceMapping() *ec2.BlockDeviceMapp return mapping } +var iopsRatios = map[string]int64{ + "io1": 50, + "io2": 500, +} + func (b *BlockDevice) Prepare(ctx *interpolate.Context) error { if b.DeviceName == "" { return fmt.Errorf("The `device_name` must be specified " + @@ -166,6 +176,18 @@ func (b *BlockDevice) Prepare(ctx *interpolate.Context) error { "true` when setting a kms_key_id.", b.DeviceName) } + if ratio, ok := iopsRatios[b.VolumeType]; b.VolumeSize != 0 && ok { + if b.IOPS/b.VolumeSize > ratio { + return fmt.Errorf("%s: the maximum ratio of provisioned IOPS to requested volume size "+ + "(in GiB) is %v:1 for %s volumes", b.DeviceName, ratio, b.VolumeType) + } + + if b.IOPS < minIops || b.IOPS > maxIops { + return fmt.Errorf("IOPS must be between %d and %d for device %s", + minIops, maxIops, b.DeviceName) + } + } + _, err := interpolate.RenderInterface(&b, ctx) return err } diff --git a/builder/amazon/common/block_device_test.go b/builder/amazon/common/block_device_test.go index 3d7e55fd7..c19d76b8e 100644 --- a/builder/amazon/common/block_device_test.go +++ b/builder/amazon/common/block_device_test.go @@ -7,6 +7,7 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/google/go-cmp/cmp" "github.com/hashicorp/packer/packer-plugin-sdk/template/config" + "github.com/hashicorp/packer/packer-plugin-sdk/template/interpolate" ) func TestBlockDevice(t *testing.T) { @@ -182,3 +183,107 @@ func TestBlockDevice(t *testing.T) { } } } + +func TestIOPSValidation(t *testing.T) { + + cases := []struct { + device BlockDevice + ok bool + msg string + }{ + // volume size unknown + { + device: BlockDevice{ + DeviceName: "/dev/sdb", + VolumeType: "io1", + IOPS: 1000, + }, + ok: true, + }, + { + device: BlockDevice{ + DeviceName: "/dev/sdb", + VolumeType: "io2", + IOPS: 1000, + }, + ok: true, + }, + // ratio requirement satisfied + { + device: BlockDevice{ + DeviceName: "/dev/sdb", + VolumeType: "io1", + VolumeSize: 50, + IOPS: 1000, + }, + ok: true, + }, + { + device: BlockDevice{ + DeviceName: "/dev/sdb", + VolumeType: "io2", + VolumeSize: 100, + IOPS: 1000, + }, + ok: true, + }, + // ratio requirement not satisfied + { + device: BlockDevice{ + DeviceName: "/dev/sdb", + VolumeType: "io1", + VolumeSize: 10, + IOPS: 2000, + }, + ok: false, + msg: "/dev/sdb: the maximum ratio of provisioned IOPS to requested volume size (in GiB) is 50:1 for io1 volumes", + }, + { + device: BlockDevice{ + DeviceName: "/dev/sdb", + VolumeType: "io2", + VolumeSize: 50, + IOPS: 30000, + }, + ok: false, + msg: "/dev/sdb: the maximum ratio of provisioned IOPS to requested volume size (in GiB) is 500:1 for io2 volumes", + }, + // exceed max iops + { + device: BlockDevice{ + DeviceName: "/dev/sdb", + VolumeType: "io2", + VolumeSize: 500, + IOPS: 99999, + }, + ok: false, + msg: "IOPS must be between 100 and 64000 for device /dev/sdb", + }, + // lower than min iops + { + device: BlockDevice{ + DeviceName: "/dev/sdb", + VolumeType: "io2", + VolumeSize: 50, + IOPS: 10, + }, + ok: false, + msg: "IOPS must be between 100 and 64000 for device /dev/sdb", + }, + } + + ctx := interpolate.Context{} + for _, testCase := range cases { + err := testCase.device.Prepare(&ctx) + if testCase.ok && err != nil { + t.Fatalf("should not error, but: %v", err) + } + if !testCase.ok { + if err == nil { + t.Fatalf("should error") + } else if err.Error() != testCase.msg { + t.Fatalf("wrong error: expected %s, found: %v", testCase.msg, err) + } + } + } +}