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

aws_instance: Support for gp3 volumes #16620

Merged
merged 12 commits into from
Dec 17, 2020
65 changes: 58 additions & 7 deletions aws/resource_aws_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,14 @@ func resourceAwsInstance() *schema.Resource {
ForceNew: true,
},

"throughput": {
Type: schema.TypeInt,
Optional: true,
Computed: true,
ForceNew: true,
DiffSuppressFunc: throughputDiffSuppressFunc,
},

"volume_size": {
Type: schema.TypeInt,
Optional: true,
Expand Down Expand Up @@ -496,6 +504,13 @@ func resourceAwsInstance() *schema.Resource {
DiffSuppressFunc: iopsDiffSuppressFunc,
},

"throughput": {
Type: schema.TypeInt,
Optional: true,
Computed: true,
DiffSuppressFunc: throughputDiffSuppressFunc,
},

"volume_size": {
Type: schema.TypeInt,
Optional: true,
Expand Down Expand Up @@ -585,11 +600,19 @@ func resourceAwsInstance() *schema.Resource {
}

func iopsDiffSuppressFunc(k, old, new string, d *schema.ResourceData) bool {
// Suppress diff if volume_type is not io1 or io2 and iops is unset or configured as 0
// Suppress diff if volume_type is not io1, io2, or gp3 and iops is unset or configured as 0
i := strings.LastIndexByte(k, '.')
vt := k[:i+1] + "volume_type"
v := d.Get(vt).(string)
return (strings.ToLower(v) != ec2.VolumeTypeIo1 || strings.ToLower(v) != ec2.VolumeTypeIo2) && new == "0"
return (strings.ToLower(v) != ec2.VolumeTypeIo1 || strings.ToLower(v) != ec2.VolumeTypeIo2 || strings.ToLower(v) != ec2.VolumeTypeGp3) && new == "0"
rajivshah3 marked this conversation as resolved.
Show resolved Hide resolved
}

func throughputDiffSuppressFunc(k, old, new string, d *schema.ResourceData) bool {
// Suppress diff if volume_type is not gp3 and throughput is unset or configured as 0
i := strings.LastIndexByte(k, '.')
vt := k[:i+1] + "volume_type"
v := d.Get(vt).(string)
return strings.ToLower(v) != ec2.VolumeTypeGp3 && new == "0"
}

func resourceAwsInstanceCreate(d *schema.ResourceData, meta interface{}) error {
Expand Down Expand Up @@ -1399,7 +1422,7 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
if v, ok := d.Get("root_block_device.0.iops").(int); ok && v != 0 {
// Enforce IOPs usage with a valid volume type
// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/12667
if t, ok := d.Get("root_block_device.0.volume_type").(string); ok && t != ec2.VolumeTypeIo1 && t != ec2.VolumeTypeIo2 {
if t, ok := d.Get("root_block_device.0.volume_type").(string); ok && t != ec2.VolumeTypeIo1 && t != ec2.VolumeTypeIo2 && t != ec2.VolumeTypeGp3 {
if t == "" {
// Volume defaults to gp2
t = ec2.VolumeTypeGp2
Expand All @@ -1410,6 +1433,16 @@ func resourceAwsInstanceUpdate(d *schema.ResourceData, meta interface{}) error {
input.Iops = aws.Int64(int64(v))
}
}
if d.HasChange("root_block_device.0.throughput") {
if v, ok := d.Get("root_block_device.0.throughput").(int); ok && v != 0 {
// Enforce throughput usage with a valid volume type
if t, ok := d.Get("root_block_device.0.volume_type").(string); ok && t != ec2.VolumeTypeGp3 {
return fmt.Errorf("error updating instance: throughput attribute not supported for type %s", t)
}
modifyVolume = true
input.Throughput = aws.Int64(int64(v))
}
}
if modifyVolume {
_, err := conn.ModifyVolume(&input)
if err != nil {
Expand Down Expand Up @@ -1743,6 +1776,9 @@ func readBlockDevicesFromInstance(instance *ec2.Instance, conn *ec2.EC2) (map[st
if vol.KmsKeyId != nil {
bd["kms_key_id"] = aws.StringValue(vol.KmsKeyId)
}
if vol.Throughput != nil {
bd["throughput"] = aws.Int64Value(vol.Throughput)
}
if instanceBd.DeviceName != nil {
bd["device_name"] = aws.StringValue(instanceBd.DeviceName)
}
Expand Down Expand Up @@ -1931,9 +1967,9 @@ func readBlockDeviceMappingsFromConfig(d *schema.ResourceData, conn *ec2.EC2) ([
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
if iops, ok := bd["iops"].(int); ok && iops > 0 {
if ec2.VolumeTypeIo1 == strings.ToLower(v) || ec2.VolumeTypeIo2 == strings.ToLower(v) {
if ec2.VolumeTypeIo1 == strings.ToLower(v) || ec2.VolumeTypeIo2 == strings.ToLower(v) || ec2.VolumeTypeGp3 == strings.ToLower(v) {
// Condition: This parameter is required for requests to create io1 or io2
// volumes; it is not used in requests to create gp2, st1, sc1, or
// volumes and optional for gp3; it is not used in requests to create gp2, st1, sc1, or
// standard volumes.
// See: http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_EbsBlockDevice.html
ebs.Iops = aws.Int64(int64(iops))
Expand All @@ -1942,6 +1978,13 @@ func readBlockDeviceMappingsFromConfig(d *schema.ResourceData, conn *ec2.EC2) ([
// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/12667
return nil, fmt.Errorf("error creating resource: iops attribute not supported for ebs_block_device with volume_type %s", v)
}
} else if throughput, ok := bd["throughput"].(int); ok && throughput > 0 {
// `throughput` is only valid for gp3
if ec2.VolumeTypeGp3 == strings.ToLower(v) {
ebs.Throughput = aws.Int64(int64(throughput))
} else {
return nil, fmt.Errorf("error creating resource: throughput attribute not supported for ebs_block_device with volume_type %s", v)
}
}
}

Expand Down Expand Up @@ -1997,8 +2040,8 @@ func readBlockDeviceMappingsFromConfig(d *schema.ResourceData, conn *ec2.EC2) ([
if v, ok := bd["volume_type"].(string); ok && v != "" {
ebs.VolumeType = aws.String(v)
if iops, ok := bd["iops"].(int); ok && iops > 0 {
if ec2.VolumeTypeIo1 == strings.ToLower(v) || ec2.VolumeTypeIo2 == strings.ToLower(v) {
// Only set the iops attribute if the volume type is io1 or io2. Setting otherwise
if ec2.VolumeTypeIo1 == strings.ToLower(v) || ec2.VolumeTypeIo2 == strings.ToLower(v) || ec2.VolumeTypeGp3 == strings.ToLower(v) {
// Only set the iops attribute if the volume type is io1, io2, or gp3. Setting otherwise
// can trigger a refresh/plan loop based on the computed value that is given
// from AWS, and prevent us from specifying 0 as a valid iops.
// See https://github.com/hashicorp/terraform/pull/4146
Expand All @@ -2009,6 +2052,14 @@ func readBlockDeviceMappingsFromConfig(d *schema.ResourceData, conn *ec2.EC2) ([
// Reference: https://github.com/hashicorp/terraform-provider-aws/issues/12667
return nil, fmt.Errorf("error creating resource: iops attribute not supported for root_block_device with volume_type %s", v)
}
} else if throughput, ok := bd["throughput"].(int); ok && throughput > 0 {
// throughput is only valid for gp3
if ec2.VolumeTypeGp3 == strings.ToLower(v) {
ebs.Throughput = aws.Int64(int64(throughput))
} else {
// Enforce throughput usage with a valid volume type
return nil, fmt.Errorf("error creating resource: throughput attribute not supported for root_block_device with volume_type %s", v)
}
}
}

Expand Down
102 changes: 102 additions & 0 deletions aws/resource_aws_instance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,20 @@ func TestAccAWSInstance_EbsBlockDevice_InvalidIopsForVolumeType(t *testing.T) {
})
}

func TestAccAWSInstance_EbsBlockDevice_InvalidThroughputForVolumeType(t *testing.T) {
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccCheckInstanceConfigEBSBlockDeviceInvalidThroughput,
ExpectError: regexp.MustCompile(`error creating resource: throughput attribute not supported for ebs_block_device with volume_type gp2`),
},
},
})
}

func TestAccAWSInstance_RootBlockDevice_KmsKeyArn(t *testing.T) {
var instance ec2.Instance
kmsKeyResourceName := "aws_kms_key.test"
Expand Down Expand Up @@ -494,6 +508,10 @@ func TestAccAWSInstance_blockDevices(t *testing.T) {
return fmt.Errorf("block device doesn't exist: /dev/sdd")
}

if _, ok := blockDevices["/dev/sde"]; !ok {
return fmt.Errorf("block device doesn't exist: /dev/sde")
}
rajivshah3 marked this conversation as resolved.
Show resolved Hide resolved

return nil
}
}
Expand Down Expand Up @@ -530,6 +548,12 @@ func TestAccAWSInstance_blockDevices(t *testing.T) {
"volume_type": "io1",
"iops": "100",
}),
resource.TestCheckTypeSetElemNestedAttrs(resourceName, "ebs_block_device.*", map[string]string{
"device_name": "/dev/sde",
"volume_size": "10",
"volume_type": "gp3",
"throughput": "1000",
}),
rajivshah3 marked this conversation as resolved.
Show resolved Hide resolved
resource.TestMatchTypeSetElemNestedAttrs(resourceName, "ebs_block_device.*", map[string]*regexp.Regexp{
"volume_id": regexp.MustCompile("vol-[a-z0-9]+"),
}),
Expand Down Expand Up @@ -1654,6 +1678,48 @@ func TestAccAWSInstance_EbsRootDevice_ModifyIOPS_Io2(t *testing.T) {
})
}

func TestAccAWSInstance_EbsRootDevice_ModifyThroughput_Gp3(t *testing.T) {
var original ec2.Instance
var updated ec2.Instance
resourceName := "aws_instance.test"

volumeSize := "30"
deleteOnTermination := "true"
volumeType := "gp3"

originalThroughput := "250"
updatedThroughput := "300"

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckInstanceDestroy,
Steps: []resource.TestStep{
{
Config: testAccAwsEc2InstanceRootBlockDeviceWithThroughput(volumeSize, deleteOnTermination, volumeType, originalThroughput),
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists(resourceName, &original),
resource.TestCheckResourceAttr(resourceName, "root_block_device.0.volume_size", volumeSize),
resource.TestCheckResourceAttr(resourceName, "root_block_device.0.delete_on_termination", deleteOnTermination),
resource.TestCheckResourceAttr(resourceName, "root_block_device.0.volume_type", volumeType),
resource.TestCheckResourceAttr(resourceName, "root_block_device.0.throughput", originalThroughput),
),
},
{
Config: testAccAwsEc2InstanceRootBlockDeviceWithThroughput(volumeSize, deleteOnTermination, volumeType, updatedThroughput),
Check: resource.ComposeTestCheckFunc(
testAccCheckInstanceExists(resourceName, &updated),
testAccCheckInstanceNotRecreated(t, &original, &updated),
resource.TestCheckResourceAttr(resourceName, "root_block_device.0.volume_size", volumeSize),
resource.TestCheckResourceAttr(resourceName, "root_block_device.0.delete_on_termination", deleteOnTermination),
resource.TestCheckResourceAttr(resourceName, "root_block_device.0.volume_type", volumeType),
resource.TestCheckResourceAttr(resourceName, "root_block_device.0.throughput", updatedThroughput),
),
},
},
})
}

func TestAccAWSInstance_EbsRootDevice_ModifyDeleteOnTermination(t *testing.T) {
var original ec2.Instance
var updated ec2.Instance
Expand Down Expand Up @@ -3556,6 +3622,27 @@ resource "aws_instance" "test" {
`, size, delete, volumeType, iops))
}

func testAccAwsEc2InstanceRootBlockDeviceWithThroughput(size, delete, volumeType, throughput string) string {
if throughput == "" {
throughput = "null"
}
return composeConfig(testAccAwsEc2InstanceAmiWithEbsRootVolume,
fmt.Sprintf(`
resource "aws_instance" "test" {
ami = data.aws_ami.ami.id

instance_type = "t2.medium"

root_block_device {
volume_size = %[1]s
delete_on_termination = %[2]s
volume_type = %[3]q
throughput = %[4]s
}
}
`, size, delete, volumeType, throughput))
}

const testAccAwsEc2InstanceAmiWithEbsRootVolume = `
data "aws_ami" "ami" {
owners = ["amazon"]
Expand Down Expand Up @@ -3930,6 +4017,21 @@ resource "aws_instance" "test" {
}
`)

var testAccCheckInstanceConfigEBSBlockDeviceInvalidThroughput = composeConfig(testAccAwsEc2InstanceAmiWithEbsRootVolume, `
resource "aws_instance" "test" {
ami = data.aws_ami.ami.id

instance_type = "t2.medium"

ebs_block_device {
device_name = "/dev/sdc"
volume_size = 10
volume_type = "gp2"
throughput = 300
}
}
`)

func testAccCheckInstanceConfigWithVolumeTags() string {
return composeConfig(testAccLatestAmazonLinuxHvmEbsAmiConfig(), fmt.Sprintf(`
resource "aws_instance" "test" {
Expand Down
7 changes: 4 additions & 3 deletions website/docs/r/instance.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ The `root_block_device` mapping supports the following:
* `volume_size` - (Optional) The size of the volume in gibibytes (GiB).
* `iops` - (Optional) The amount of provisioned
[IOPS](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html).
This is only valid for `volume_type` of `"io1/io2"`, and must be specified if
using that type
This is only valid for `volume_type` of `"io1/io2"` (required) and `"gp3"` (optional).
* `throughput` - (Optional) The throughput to provision for a volume in mebibytes per second (MiB/s). This is only valid for `volume_type` of `"gp3"`.
rajivshah3 marked this conversation as resolved.
Show resolved Hide resolved
* `delete_on_termination` - (Optional) Whether the volume should be destroyed
on instance termination (Default: `true`).
* `encrypted` - (Optional) Enable volume encryption. (Default: `false`). Must be configured to perform drift detection.
Expand All @@ -149,7 +149,8 @@ Each `ebs_block_device` supports the following:
* `volume_size` - (Optional) The size of the volume in gibibytes (GiB).
* `iops` - (Optional) The amount of provisioned
[IOPS](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-io-characteristics.html).
This must be set with a `volume_type` of `"io1/io2"`.
This is only valid for `volume_type` of `"io1/io2"` (required) and `"gp3"` (optional).
* `throughput` - (Optional) The throughput to provision for a volume in mebibytes per second (MiB/s). This is only valid for `volume_type` of `"gp3"`.
rajivshah3 marked this conversation as resolved.
Show resolved Hide resolved
* `delete_on_termination` - (Optional) Whether the volume should be destroyed
on instance termination (Default: `true`).
* `encrypted` - (Optional) Enables [EBS
Expand Down