diff --git a/.changelog/25174.txt b/.changelog/25174.txt new file mode 100644 index 000000000000..d3094cf24fd2 --- /dev/null +++ b/.changelog/25174.txt @@ -0,0 +1,3 @@ +```release-note:bug +resource/aws_ssm_parameter: Allow `Intelligent-Tiering` to upgrade to `Advanced` tier as needed. +``` diff --git a/internal/service/ssm/parameter.go b/internal/service/ssm/parameter.go index b286e1313331..4590449d829b 100644 --- a/internal/service/ssm/parameter.go +++ b/internal/service/ssm/parameter.go @@ -49,10 +49,13 @@ func ResourceParameter() *schema.Resource { "tier": { Type: schema.TypeString, Optional: true, - Default: ssm.ParameterTierStandard, + Computed: true, ValidateFunc: validation.StringInSlice(ssm.ParameterTier_Values(), false), DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - return d.Get("tier").(string) == ssm.ParameterTierIntelligentTiering + if old != "" { + return new == ssm.ParameterTierIntelligentTiering + } + return false }, }, "type": { @@ -104,10 +107,8 @@ func ResourceParameter() *schema.Resource { CustomizeDiff: customdiff.Sequence( // Prevent the following error during tier update from Advanced to Standard: // ValidationException: This parameter uses the advanced-parameter tier. You can't downgrade a parameter from the advanced-parameter tier to the standard-parameter tier. If necessary, you can delete the advanced parameter and recreate it as a standard parameter. - // In the case of Advanced to Intelligent-Tiering, a ValidationException is not thrown - // but rather no change occurs without resource re-creation customdiff.ForceNewIfChange("tier", func(_ context.Context, old, new, meta interface{}) bool { - return old.(string) == ssm.ParameterTierAdvanced && (new.(string) == ssm.ParameterTierStandard || new.(string) == ssm.ParameterTierIntelligentTiering) + return old.(string) == ssm.ParameterTierAdvanced && new.(string) == ssm.ParameterTierStandard }), customdiff.ComputedIf("version", func(_ context.Context, diff *schema.ResourceDiff, meta interface{}) bool { return diff.HasChange("value") @@ -127,12 +128,14 @@ func resourceParameterCreate(d *schema.ResourceData, meta interface{}) error { paramInput := &ssm.PutParameterInput{ Name: aws.String(name), Type: aws.String(d.Get("type").(string)), - Tier: aws.String(d.Get("tier").(string)), Value: aws.String(d.Get("value").(string)), - Overwrite: aws.Bool(ShouldUpdateParameter(d)), AllowedPattern: aws.String(d.Get("allowed_pattern").(string)), } + if v, ok := d.GetOk("tier"); ok { + paramInput.Tier = aws.String(v.(string)) + } + if v, ok := d.GetOk("data_type"); ok { paramInput.DataType = aws.String(v.(string)) } @@ -145,33 +148,20 @@ func resourceParameterCreate(d *schema.ResourceData, meta interface{}) error { paramInput.SetKeyId(keyID.(string)) } - // AWS SSM Service only supports PutParameter requests with Tags - // iff Overwrite is not provided or is false; in this resource's case, - // the Overwrite value is always set in the paramInput so we check for the value - if len(tags) > 0 && !aws.BoolValue(paramInput.Overwrite) { + if len(tags) > 0 { paramInput.Tags = Tags(tags.IgnoreAWS()) } _, err := conn.PutParameter(paramInput) if tfawserr.ErrMessageContains(err, "ValidationException", "Tier is not supported") { + log.Printf("[WARN] Creating SSM Parameter (%s): tier %q not supported, using default", name, d.Get("tier").(string)) paramInput.Tier = nil _, err = conn.PutParameter(paramInput) } if err != nil { - return fmt.Errorf("error creating SSM parameter (%s): %w", name, err) - } - - // Since the AWS SSM Service does not support PutParameter requests with - // Tags and Overwrite set to true, we make an additional API call - // to Update the resource's tags if necessary - if d.HasChange("tags_all") && paramInput.Tags == nil { - o, n := d.GetChange("tags_all") - - if err := UpdateTags(conn, name, ssm.ResourceTypeForTaggingParameter, o, n); err != nil { - return fmt.Errorf("error updating SSM Parameter (%s) tags: %w", name, err) - } + return fmt.Errorf("error creating SSM Parameter (%s): %w", name, err) } d.SetId(name) @@ -249,10 +239,7 @@ func resourceParameterRead(d *schema.ResourceData, meta interface{}) error { detail := describeResp.Parameters[0] d.Set("key_id", detail.KeyId) d.Set("description", detail.Description) - d.Set("tier", ssm.ParameterTierStandard) - if detail.Tier != nil { - d.Set("tier", detail.Tier) - } + d.Set("tier", detail.Tier) d.Set("allowed_pattern", detail.AllowedPattern) d.Set("data_type", detail.DataType) @@ -291,6 +278,12 @@ func resourceParameterUpdate(d *schema.ResourceData, meta interface{}) error { AllowedPattern: aws.String(d.Get("allowed_pattern").(string)), } + // Retrieve the value set in the config directly to counteract the DiffSuppressFunc above + tier := d.GetRawConfig().GetAttr("tier") + if tier.IsKnown() && !tier.IsNull() { + paramInput.Tier = aws.String(tier.AsString()) + } + if d.HasChange("data_type") { paramInput.DataType = aws.String(d.Get("data_type").(string)) } @@ -306,12 +299,13 @@ func resourceParameterUpdate(d *schema.ResourceData, meta interface{}) error { _, err := conn.PutParameter(paramInput) if tfawserr.ErrMessageContains(err, "ValidationException", "Tier is not supported") { + log.Printf("[WARN] Updating SSM Parameter (%s): tier %q not supported, using default", d.Get("name").(string), d.Get("tier").(string)) paramInput.Tier = nil _, err = conn.PutParameter(paramInput) } if err != nil { - return fmt.Errorf("error updating SSM parameter (%s): %w", d.Id(), err) + return fmt.Errorf("error updating SSM Parameter (%s): %w", d.Id(), err) } } diff --git a/internal/service/ssm/parameter_test.go b/internal/service/ssm/parameter_test.go index 6c0ad7def532..bd502560c4e2 100644 --- a/internal/service/ssm/parameter_test.go +++ b/internal/service/ssm/parameter_test.go @@ -33,7 +33,7 @@ func TestAccSSMParameter_basic(t *testing.T) { acctest.CheckResourceAttrRegionalARN(resourceName, "arn", "ssm", fmt.Sprintf("parameter/%s", name)), resource.TestCheckResourceAttr(resourceName, "value", "test2"), resource.TestCheckResourceAttr(resourceName, "type", "String"), - resource.TestCheckResourceAttr(resourceName, "tier", "Standard"), + resource.TestCheckResourceAttr(resourceName, "tier", ssm.ParameterTierStandard), resource.TestCheckResourceAttr(resourceName, "tags.%", "0"), resource.TestCheckResourceAttrSet(resourceName, "version"), resource.TestCheckResourceAttr(resourceName, "data_type", "text"), @@ -171,10 +171,11 @@ func TestAccSSMParameter_Tier_intelligentTieringToAdvanced(t *testing.T) { ), }, { + // Intelligent-Tiering will not downgrade an existing parameter to Standard Config: testAccParameterConfig_tier(rName, ssm.ParameterTierIntelligentTiering), Check: resource.ComposeTestCheckFunc( testAccCheckParameterExists(resourceName, ¶meter2), - resource.TestCheckResourceAttr(resourceName, "tier", ssm.ParameterTierStandard), + resource.TestCheckResourceAttr(resourceName, "tier", ssm.ParameterTierAdvanced), ), }, { @@ -187,6 +188,68 @@ func TestAccSSMParameter_Tier_intelligentTieringToAdvanced(t *testing.T) { }) } +func TestAccSSMParameter_Tier_intelligentTieringOnCreation(t *testing.T) { + var parameter ssm.Parameter + rName := fmt.Sprintf("%s_%s", t.Name(), sdkacctest.RandString(10)) + resourceName := "aws_ssm_parameter.test" + + value := sdkacctest.RandString(5000) // Maximum size for Standard tier is 4 KB + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ssm.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckParameterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccParameterConfig_tierWithValue(rName, ssm.ParameterTierIntelligentTiering, value), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterExists(resourceName, ¶meter), + resource.TestCheckResourceAttr(resourceName, "tier", ssm.ParameterTierAdvanced), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"overwrite"}, + }, + }, + }) +} + +func TestAccSSMParameter_Tier_intelligentTieringOnUpdate(t *testing.T) { + var parameter ssm.Parameter + rName := fmt.Sprintf("%s_%s", t.Name(), sdkacctest.RandString(10)) + resourceName := "aws_ssm_parameter.test" + + standardSizedValue := sdkacctest.RandString(10) + advancedSizedValue := sdkacctest.RandString(5000) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ErrorCheck: acctest.ErrorCheck(t, ssm.EndpointsID), + ProviderFactories: acctest.ProviderFactories, + CheckDestroy: testAccCheckParameterDestroy, + Steps: []resource.TestStep{ + { + Config: testAccParameterConfig_tierWithValue(rName, ssm.ParameterTierIntelligentTiering, standardSizedValue), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterExists(resourceName, ¶meter), + resource.TestCheckResourceAttr(resourceName, "tier", ssm.ParameterTierStandard), + ), + }, + { + Config: testAccParameterConfig_tierWithValue(rName, ssm.ParameterTierIntelligentTiering, advancedSizedValue), + Check: resource.ComposeTestCheckFunc( + testAccCheckParameterExists(resourceName, ¶meter), + resource.TestCheckResourceAttr(resourceName, "tier", ssm.ParameterTierAdvanced), + ), + }, + }, + }) +} + func TestAccSSMParameter_disappears(t *testing.T) { var param ssm.Parameter name := fmt.Sprintf("%s_%s", t.Name(), sdkacctest.RandString(10)) @@ -770,6 +833,17 @@ resource "aws_ssm_parameter" "test" { `, rName, tier) } +func testAccParameterConfig_tierWithValue(rName, tier, value string) string { + return fmt.Sprintf(` +resource "aws_ssm_parameter" "test" { + name = %[1]q + tier = %[2]q + type = "String" + value = %[3]q +} +`, rName, tier, value) +} + func testAccParameterConfig_dataTypeEC2Image(rName string) string { return acctest.ConfigCompose( acctest.ConfigLatestAmazonLinuxHVMEBSAMI(), diff --git a/website/docs/r/ssm_parameter.html.markdown b/website/docs/r/ssm_parameter.html.markdown index 124926fe0a3f..2d019b88fd08 100644 --- a/website/docs/r/ssm_parameter.html.markdown +++ b/website/docs/r/ssm_parameter.html.markdown @@ -61,7 +61,11 @@ The following arguments are supported: * `type` - (Required) The type of the parameter. Valid types are `String`, `StringList` and `SecureString`. * `value` - (Required) The value of the parameter. This value is always marked as sensitive in the Terraform plan output, regardless of `type`. In Terraform CLI version 0.15 and later, this may require additional configuration handling for certain scenarios. For more information, see the [Terraform v0.15 Upgrade Guide](https://www.terraform.io/upgrade-guides/0-15.html#sensitive-output-values). * `description` - (Optional) The description of the parameter. -* `tier` - (Optional) The tier of the parameter. If not specified, will default to `Standard`. Valid tiers are `Standard`, `Advanced`, and `Intelligent-Tiering`. For more information on parameter tiers, see the [AWS SSM Parameter tier comparison and guide](https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-advanced-parameters.html). +* `tier` - (Optional) The parameter tier to assign to the parameter. + If not specified, will use the default parameter tier for the region. + Valid tiers are `Standard`, `Advanced`, and `Intelligent-Tiering`. + Downgrading an `Advanced` tier parameter to `Standard` will recreate the resource. + For more information on parameter tiers, see the [AWS SSM Parameter tier comparison and guide](https://docs.aws.amazon.com/systems-manager/latest/userguide/parameter-store-advanced-parameters.html). * `key_id` - (Optional) The KMS key id or arn for encrypting a SecureString. * `overwrite` - (Optional) Overwrite an existing parameter. If not specified, will default to `false` if the resource has not been created by terraform to avoid overwrite of existing resource and will default to `true` otherwise (terraform lifecycle rules should then be used to manage the update behavior). * `allowed_pattern` - (Optional) A regular expression used to validate the parameter value.