diff --git a/aws/data_source_aws_ebs_volume.go b/aws/data_source_aws_ebs_volume.go index 0ff85a21d2c..ec718a173d5 100644 --- a/aws/data_source_aws_ebs_volume.go +++ b/aws/data_source_aws_ebs_volume.go @@ -67,6 +67,10 @@ func dataSourceAwsEbsVolume() *schema.Resource { Computed: true, }, "tags": tagsSchemaComputed(), + "throughput": { + Type: schema.TypeInt, + Computed: true, + }, }, } } @@ -150,6 +154,7 @@ func volumeDescriptionAttributes(d *schema.ResourceData, client *AWSClient, volu d.Set("volume_type", volume.VolumeType) d.Set("outpost_arn", volume.OutpostArn) d.Set("multi_attach_enabled", volume.MultiAttachEnabled) + d.Set("throughput", volume.Throughput) if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(volume.Tags).IgnoreAws().IgnoreConfig(client.IgnoreTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %s", err) diff --git a/aws/data_source_aws_ebs_volume_test.go b/aws/data_source_aws_ebs_volume_test.go index 6ceaef16bf6..a956929510d 100644 --- a/aws/data_source_aws_ebs_volume_test.go +++ b/aws/data_source_aws_ebs_volume_test.go @@ -25,6 +25,7 @@ func TestAccAWSEbsVolumeDataSource_basic(t *testing.T) { resource.TestCheckResourceAttrPair(dataSourceName, "tags", resourceName, "tags"), resource.TestCheckResourceAttrPair(dataSourceName, "outpost_arn", resourceName, "outpost_arn"), resource.TestCheckResourceAttrPair(dataSourceName, "multi_attach_enabled", resourceName, "multi_attach_enabled"), + resource.TestCheckResourceAttrPair(dataSourceName, "throughput", resourceName, "throughput"), ), }, }, diff --git a/aws/resource_aws_ebs_volume.go b/aws/resource_aws_ebs_volume.go index 439287fada5..1002f1cff7d 100644 --- a/aws/resource_aws_ebs_volume.go +++ b/aws/resource_aws_ebs_volume.go @@ -1,6 +1,7 @@ package aws import ( + "context" "fmt" "log" "time" @@ -11,6 +12,7 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/terraform-providers/terraform-provider-aws/aws/internal/keyvaluetags" ) @@ -24,6 +26,8 @@ func resourceAwsEbsVolume() *schema.Resource { State: schema.ImportStatePassthrough, }, + CustomizeDiff: resourceAwsEbsVolumeCustomizeDiff, + Schema: map[string]*schema.Schema{ "arn": { Type: schema.TypeString, @@ -44,9 +48,6 @@ func resourceAwsEbsVolume() *schema.Resource { Type: schema.TypeInt, Optional: true, Computed: true, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return (d.Get("type").(string) != ec2.VolumeTypeIo1 && new == "0") || (d.Get("type").(string) != ec2.VolumeTypeIo2 && new == "0") - }, }, "kms_key_id": { Type: schema.TypeString, @@ -61,15 +62,17 @@ func resourceAwsEbsVolume() *schema.Resource { ForceNew: true, }, "size": { - Type: schema.TypeInt, - Optional: true, - Computed: true, + Type: schema.TypeInt, + Optional: true, + Computed: true, + ExactlyOneOf: []string{"size", "snapshot_id"}, }, "snapshot_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, + Type: schema.TypeString, + Optional: true, + Computed: true, + ForceNew: true, + ExactlyOneOf: []string{"size", "snapshot_id"}, }, "outpost_arn": { Type: schema.TypeString, @@ -83,6 +86,12 @@ func resourceAwsEbsVolume() *schema.Resource { Computed: true, }, "tags": tagsSchema(), + "throughput": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validation.IntBetween(125, 1000), + }, }, } } @@ -97,6 +106,9 @@ func resourceAwsEbsVolumeCreate(d *schema.ResourceData, meta interface{}) error if value, ok := d.GetOk("encrypted"); ok { request.Encrypted = aws.Bool(value.(bool)) } + if value, ok := d.GetOk("iops"); ok { + request.Iops = aws.Int64(int64(value.(int))) + } if value, ok := d.GetOk("kms_key_id"); ok { request.KmsKeyId = aws.String(value.(string)) } @@ -112,28 +124,11 @@ func resourceAwsEbsVolumeCreate(d *schema.ResourceData, meta interface{}) error if value, ok := d.GetOk("outpost_arn"); ok { request.OutpostArn = aws.String(value.(string)) } - - // IOPs are only valid, and required for, storage type io1 and io2. The current minimum - // is 100. Hard validation in place to return an error if IOPs are provided - // for an unsupported storage type. - // Reference: https://github.com/hashicorp/terraform-provider-aws/issues/12667 - var t string - if value, ok := d.GetOk("type"); ok { - t = value.(string) - request.VolumeType = aws.String(t) + if value, ok := d.GetOk("throughput"); ok { + request.Throughput = aws.Int64(int64(value.(int))) } - - if iops := d.Get("iops").(int); iops > 0 { - if t != ec2.VolumeTypeIo1 && t != ec2.VolumeTypeIo2 { - if t == "" { - // Volume creation would default to gp2 - t = ec2.VolumeTypeGp2 - } - return fmt.Errorf("error creating ebs_volume: iops attribute not supported for type %s", t) - } - // We add the iops value without validating it's size, to allow AWS to - // enforce a size requirement (currently 100) - request.Iops = aws.Int64(int64(iops)) + if value, ok := d.GetOk("type"); ok { + request.VolumeType = aws.String(value.(string)) } log.Printf("[DEBUG] EBS Volume create opts: %s", request) @@ -168,27 +163,29 @@ func resourceAwsEbsVolumeCreate(d *schema.ResourceData, meta interface{}) error func resourceAWSEbsVolumeUpdate(d *schema.ResourceData, meta interface{}) error { conn := meta.(*AWSClient).ec2conn - requestUpdate := false - params := &ec2.ModifyVolumeInput{ - VolumeId: aws.String(d.Id()), - } + if d.HasChangesExcept("tags") { + params := &ec2.ModifyVolumeInput{ + VolumeId: aws.String(d.Id()), + } - if d.HasChange("size") { - requestUpdate = true - params.Size = aws.Int64(int64(d.Get("size").(int))) - } + if d.HasChange("size") { + params.Size = aws.Int64(int64(d.Get("size").(int))) + } - if d.HasChange("type") { - requestUpdate = true - params.VolumeType = aws.String(d.Get("type").(string)) - } + if d.HasChange("type") { + params.VolumeType = aws.String(d.Get("type").(string)) + } - if d.HasChange("iops") { - requestUpdate = true - params.Iops = aws.Int64(int64(d.Get("iops").(int))) - } + if d.HasChange("iops") { + params.Iops = aws.Int64(int64(d.Get("iops").(int))) + } + + // "If no throughput value is specified, the existing value is retained." + // Not currently correct, so always specify any non-zero throughput value. + if v := d.Get("throughput").(int); v > 0 { + params.Throughput = aws.Int64(int64(v)) + } - if requestUpdate { result, err := conn.ModifyVolume(params) if err != nil { return err @@ -278,20 +275,21 @@ func resourceAwsEbsVolumeRead(d *schema.ResourceData, meta interface{}) error { Service: "ec2", } d.Set("arn", arn.String()) - d.Set("availability_zone", aws.StringValue(volume.AvailabilityZone)) - d.Set("encrypted", aws.BoolValue(volume.Encrypted)) - d.Set("iops", aws.Int64Value(volume.Iops)) - d.Set("kms_key_id", aws.StringValue(volume.KmsKeyId)) - d.Set("size", aws.Int64Value(volume.Size)) - d.Set("snapshot_id", aws.StringValue(volume.SnapshotId)) - d.Set("outpost_arn", aws.StringValue(volume.OutpostArn)) + d.Set("availability_zone", volume.AvailabilityZone) + d.Set("encrypted", volume.Encrypted) + d.Set("iops", volume.Iops) + d.Set("kms_key_id", volume.KmsKeyId) + d.Set("size", volume.Size) + d.Set("snapshot_id", volume.SnapshotId) + d.Set("outpost_arn", volume.OutpostArn) d.Set("multi_attach_enabled", volume.MultiAttachEnabled) + d.Set("throughput", volume.Throughput) if err := d.Set("tags", keyvaluetags.Ec2KeyValueTags(volume.Tags).IgnoreAws().IgnoreConfig(ignoreTagsConfig).Map()); err != nil { return fmt.Errorf("error setting tags: %s", err) } - d.Set("type", aws.StringValue(volume.VolumeType)) + d.Set("type", volume.VolumeType) return nil } @@ -373,3 +371,53 @@ func resourceAwsEbsVolumeDelete(d *schema.ResourceData, meta interface{}) error return nil } + +func resourceAwsEbsVolumeCustomizeDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error { + iops := diff.Get("iops").(int) + multiAttachEnabled := diff.Get("multi_attach_enabled").(bool) + throughput := diff.Get("throughput").(int) + volumeType := diff.Get("type").(string) + + if diff.Id() == "" { + // Create. + + // Iops is required for io1 and io2 volumes. + // The default for gp3 volumes is 3,000 IOPS. + // This parameter is not supported for gp2, st1, sc1, or standard volumes. + // Hard validation in place to return an error if IOPs are provided + // for an unsupported storage type. + // Reference: https://github.com/hashicorp/terraform-provider-aws/issues/12667 + switch volumeType { + case ec2.VolumeTypeIo1, ec2.VolumeTypeIo2: + if iops == 0 { + return fmt.Errorf("'iops' must be set when 'type' is '%s'", volumeType) + } + + case ec2.VolumeTypeGp3: + + default: + if iops != 0 { + return fmt.Errorf("'iops' must not be set when 'type' is '%s'", volumeType) + } + } + + // MultiAttachEnabled is supported with io1 volumes only. + if multiAttachEnabled && volumeType != ec2.VolumeTypeIo1 { + return fmt.Errorf("'multi_attach_enabled' must not be set when 'type' is '%s'", volumeType) + } + + // Throughput is valid only for gp3 volumes. + if throughput > 0 && volumeType != ec2.VolumeTypeGp3 { + return fmt.Errorf("'throughput' must not be set when 'type' is '%s'", volumeType) + } + } else { + // Update. + + // Setting 'iops = 0' is a no-op if the volume type does not require Iops to be specified. + if diff.HasChange("iops") && volumeType != ec2.VolumeTypeIo1 && volumeType != ec2.VolumeTypeIo2 && iops == 0 { + return diff.Clear("iops") + } + } + + return nil +} diff --git a/aws/resource_aws_ebs_volume_test.go b/aws/resource_aws_ebs_volume_test.go index 36214365714..8550bbebe5e 100644 --- a/aws/resource_aws_ebs_volume_test.go +++ b/aws/resource_aws_ebs_volume_test.go @@ -89,6 +89,7 @@ func TestAccAWSEBSVolume_basic(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "type", "gp2"), resource.TestCheckResourceAttr(resourceName, "outpost_arn", ""), resource.TestCheckResourceAttr(resourceName, "multi_attach_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, { @@ -115,6 +116,7 @@ func TestAccAWSEBSVolume_updateAttachedEbsVolume(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "size", "10"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, { @@ -127,6 +129,7 @@ func TestAccAWSEBSVolume_updateAttachedEbsVolume(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "size", "20"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, }, @@ -148,6 +151,7 @@ func TestAccAWSEBSVolume_updateSize(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "size", "1"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, { @@ -160,6 +164,7 @@ func TestAccAWSEBSVolume_updateSize(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "size", "10"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, }, @@ -181,6 +186,7 @@ func TestAccAWSEBSVolume_updateType(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "type", "gp2"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, { @@ -193,6 +199,7 @@ func TestAccAWSEBSVolume_updateType(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "type", "sc1"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, }, @@ -214,6 +221,7 @@ func TestAccAWSEBSVolume_updateIops_Io1(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "iops", "100"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, { @@ -226,6 +234,7 @@ func TestAccAWSEBSVolume_updateIops_Io1(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "iops", "200"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, }, @@ -247,6 +256,7 @@ func TestAccAWSEBSVolume_updateIops_Io2(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "iops", "100"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, { @@ -259,6 +269,7 @@ func TestAccAWSEBSVolume_updateIops_Io2(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "iops", "200"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, }, @@ -284,6 +295,7 @@ func TestAccAWSEBSVolume_kmsKey(t *testing.T) { testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "encrypted", "true"), resource.TestCheckResourceAttrPair(resourceName, "kms_key_id", kmsKeyResourceName, "arn"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, { @@ -308,6 +320,7 @@ func TestAccAWSEBSVolume_NoIops(t *testing.T) { Config: testAccAwsEbsVolumeConfigWithNoIops, Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, { @@ -329,7 +342,22 @@ func TestAccAWSEBSVolume_InvalidIopsForType(t *testing.T) { Steps: []resource.TestStep{ { Config: testAccAwsEbsVolumeConfigWithInvalidIopsForType, - ExpectError: regexp.MustCompile(`error creating ebs_volume: iops attribute not supported for type gp2`), + ExpectError: regexp.MustCompile(`'iops' must not be set when 'type' is`), + }, + }, + }) +} + +func TestAccAWSEBSVolume_InvalidThroughputForType(t *testing.T) { + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsEbsVolumeConfigWithInvalidThroughputForType, + ExpectError: regexp.MustCompile(`'throughput' must not be set when 'type' is`), }, }, }) @@ -378,6 +406,7 @@ func TestAccAWSEBSVolume_multiAttach(t *testing.T) { Check: resource.ComposeTestCheckFunc( testAccCheckVolumeExists(resourceName, &v), resource.TestCheckResourceAttr(resourceName, "multi_attach_enabled", "true"), + resource.TestCheckResourceAttr(resourceName, "throughput", "0"), ), }, { @@ -416,6 +445,156 @@ func TestAccAWSEBSVolume_outpost(t *testing.T) { }) } +func TestAccAWSEBSVolume_gp3_basic(t *testing.T) { + var v ec2.Volume + resourceName := "aws_ebs_volume.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: resourceName, + Providers: testAccProviders, + CheckDestroy: testAccCheckVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsEbsVolumeConfigSizeType(rName, 10, "gp3"), + Check: resource.ComposeTestCheckFunc( + testAccCheckVolumeExists(resourceName, &v), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "ec2", regexp.MustCompile(`volume/vol-.+`)), + resource.TestCheckResourceAttr(resourceName, "encrypted", "false"), + resource.TestCheckResourceAttr(resourceName, "iops", "3000"), + resource.TestCheckResourceAttr(resourceName, "kms_key_id", ""), + resource.TestCheckResourceAttr(resourceName, "multi_attach_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "outpost_arn", ""), + resource.TestCheckResourceAttr(resourceName, "size", "10"), + resource.TestCheckResourceAttr(resourceName, "snapshot_id", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + resource.TestCheckResourceAttr(resourceName, "throughput", "125"), + resource.TestCheckResourceAttr(resourceName, "type", "gp3"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAWSEBSVolume_gp3_iops(t *testing.T) { + var v ec2.Volume + resourceName := "aws_ebs_volume.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: resourceName, + Providers: testAccProviders, + CheckDestroy: testAccCheckVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsEbsVolumeConfigGp3Iops(rName, 4000), + Check: resource.ComposeTestCheckFunc( + testAccCheckVolumeExists(resourceName, &v), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "ec2", regexp.MustCompile(`volume/vol-.+`)), + resource.TestCheckResourceAttr(resourceName, "encrypted", "false"), + resource.TestCheckResourceAttr(resourceName, "iops", "4000"), + resource.TestCheckResourceAttr(resourceName, "kms_key_id", ""), + resource.TestCheckResourceAttr(resourceName, "multi_attach_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "outpost_arn", ""), + resource.TestCheckResourceAttr(resourceName, "size", "10"), + resource.TestCheckResourceAttr(resourceName, "snapshot_id", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + resource.TestCheckResourceAttr(resourceName, "throughput", "200"), + resource.TestCheckResourceAttr(resourceName, "type", "gp3"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsEbsVolumeConfigGp3Iops(rName, 5000), + Check: resource.ComposeTestCheckFunc( + testAccCheckVolumeExists(resourceName, &v), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "ec2", regexp.MustCompile(`volume/vol-.+`)), + resource.TestCheckResourceAttr(resourceName, "encrypted", "false"), + resource.TestCheckResourceAttr(resourceName, "iops", "5000"), + resource.TestCheckResourceAttr(resourceName, "kms_key_id", ""), + resource.TestCheckResourceAttr(resourceName, "multi_attach_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "outpost_arn", ""), + resource.TestCheckResourceAttr(resourceName, "size", "10"), + resource.TestCheckResourceAttr(resourceName, "snapshot_id", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + resource.TestCheckResourceAttr(resourceName, "throughput", "200"), + resource.TestCheckResourceAttr(resourceName, "type", "gp3"), + ), + }, + }, + }) +} + +func TestAccAWSEBSVolume_gp3_throughput(t *testing.T) { + var v ec2.Volume + resourceName := "aws_ebs_volume.test" + rName := acctest.RandomWithPrefix("tf-acc-test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + IDRefreshName: resourceName, + Providers: testAccProviders, + CheckDestroy: testAccCheckVolumeDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAwsEbsVolumeConfigGp3Throughput(rName, 400), + Check: resource.ComposeTestCheckFunc( + testAccCheckVolumeExists(resourceName, &v), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "ec2", regexp.MustCompile(`volume/vol-.+`)), + resource.TestCheckResourceAttr(resourceName, "encrypted", "false"), + resource.TestCheckResourceAttr(resourceName, "iops", "3000"), + resource.TestCheckResourceAttr(resourceName, "kms_key_id", ""), + resource.TestCheckResourceAttr(resourceName, "multi_attach_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "outpost_arn", ""), + resource.TestCheckResourceAttr(resourceName, "size", "10"), + resource.TestCheckResourceAttr(resourceName, "snapshot_id", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + resource.TestCheckResourceAttr(resourceName, "throughput", "400"), + resource.TestCheckResourceAttr(resourceName, "type", "gp3"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAwsEbsVolumeConfigGp3Throughput(rName, 600), + Check: resource.ComposeTestCheckFunc( + testAccCheckVolumeExists(resourceName, &v), + testAccMatchResourceAttrRegionalARN(resourceName, "arn", "ec2", regexp.MustCompile(`volume/vol-.+`)), + resource.TestCheckResourceAttr(resourceName, "encrypted", "false"), + resource.TestCheckResourceAttr(resourceName, "iops", "3000"), + resource.TestCheckResourceAttr(resourceName, "kms_key_id", ""), + resource.TestCheckResourceAttr(resourceName, "multi_attach_enabled", "false"), + resource.TestCheckResourceAttr(resourceName, "outpost_arn", ""), + resource.TestCheckResourceAttr(resourceName, "size", "10"), + resource.TestCheckResourceAttr(resourceName, "snapshot_id", ""), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.Name", rName), + resource.TestCheckResourceAttr(resourceName, "throughput", "600"), + resource.TestCheckResourceAttr(resourceName, "type", "gp3"), + ), + }, + }, + }) +} + func TestAccAWSEBSVolume_disappears(t *testing.T) { var v ec2.Volume resourceName := "aws_ebs_volume.test" @@ -869,6 +1048,29 @@ resource "aws_ebs_volume" "test" { } ` +const testAccAwsEbsVolumeConfigWithInvalidThroughputForType = ` +data "aws_availability_zones" "available" { + state = "available" + + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +resource "aws_ebs_volume" "test" { + availability_zone = data.aws_availability_zones.available.names[0] + size = 10 + iops = 100 + throughput = 500 + type = "io1" + + tags = { + Name = "TerraformTest" + } +} +` + func testAccAwsEbsVolumeConfigOutpost() string { return ` data "aws_outposts_outposts" "test" {} @@ -913,3 +1115,54 @@ resource "aws_ebs_volume" "test" { } `, rName) } + +func testAccAwsEbsVolumeConfigSizeType(rName string, size int, volumeType string) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_ebs_volume" "test" { + availability_zone = data.aws_availability_zones.available.names[0] + type = %[3]q + size = %[2]d + + tags = { + Name = %[1]q + } +} +`, rName, size, volumeType)) +} + +func testAccAwsEbsVolumeConfigGp3Throughput(rName string, throughput int) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_ebs_volume" "test" { + availability_zone = data.aws_availability_zones.available.names[0] + type = "gp3" + size = 10 + throughput = %[2]d + + tags = { + Name = %[1]q + } +} +`, rName, throughput)) +} + +func testAccAwsEbsVolumeConfigGp3Iops(rName string, iops int) string { + return composeConfig( + testAccAvailableAZsNoOptInConfig(), + fmt.Sprintf(` +resource "aws_ebs_volume" "test" { + availability_zone = data.aws_availability_zones.available.names[0] + type = "gp3" + iops = %[2]d + size = 10 + throughput = 200 + + tags = { + Name = %[1]q + } +} +`, rName, iops)) +} diff --git a/website/docs/d/ebs_volume.html.markdown b/website/docs/d/ebs_volume.html.markdown index 0a4d9d5c72b..a3d74495dc7 100644 --- a/website/docs/d/ebs_volume.html.markdown +++ b/website/docs/d/ebs_volume.html.markdown @@ -57,5 +57,6 @@ In addition to all arguments above, the following attributes are exported: * `volume_type` - The type of EBS volume. * `kms_key_id` - The ARN for the KMS encryption key. * `tags` - A map of tags for the resource. +* `throughput` - The throughput that the volume supports, in MiB/s. [1]: http://docs.aws.amazon.com/cli/latest/reference/ec2/describe-volumes.html diff --git a/website/docs/r/ebs_volume.html.markdown b/website/docs/r/ebs_volume.html.markdown index 4bf9dced640..e0c1e716ead 100644 --- a/website/docs/r/ebs_volume.html.markdown +++ b/website/docs/r/ebs_volume.html.markdown @@ -31,16 +31,17 @@ The following arguments are supported: * `availability_zone` - (Required) The AZ where the EBS volume will exist. * `encrypted` - (Optional) If true, the disk will be encrypted. -* `iops` - (Optional) The amount of IOPS to provision for the disk. Only valid for `type` of `io1` or `io2`. +* `iops` - (Optional) The amount of IOPS to provision for the disk. Only valid for `type` of `io1`, `io2` or `gp3`. * `multi_attach_enabled` - (Optional) Specifies whether to enable Amazon EBS Multi-Attach. Multi-Attach is supported exclusively on `io1` volumes. * `size` - (Optional) The size of the drive in GiBs. * `snapshot_id` (Optional) A snapshot to base the EBS volume off of. * `outpost_arn` - (Optional) The Amazon Resource Name (ARN) of the Outpost. -* `type` - (Optional) The type of EBS volume. Can be "standard", "gp2", "io1", "io2", "sc1" or "st1" (Default: "gp2"). +* `type` - (Optional) The type of EBS volume. Can be `standard`, `gp2`, `gp3`, `io1`, `io2`, `sc1` or `st1` (Default: `gp2`). * `kms_key_id` - (Optional) The ARN for the KMS encryption key. When specifying `kms_key_id`, `encrypted` needs to be set to true. * `tags` - (Optional) A map of tags to assign to the resource. +* `throughput` - (Optional) The throughput that the volume supports, in MiB/s. Only valid for `type` of `gp3`. -~> **NOTE**: When changing the `size`, `iops` or `type` of an instance, there are [considerations](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/considerations.html) to be aware of that Amazon have written about this. +~> **NOTE**: When changing the `size`, `iops` or `type` of an instance, there are [considerations](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/considerations.html) to be aware of. ## Attributes Reference