diff --git a/.changelog/37898.txt b/.changelog/37898.txt new file mode 100644 index 00000000000..5b228b6e1ed --- /dev/null +++ b/.changelog/37898.txt @@ -0,0 +1,7 @@ +```release-note:enhancement +resource/aws_cloudformation_stack_set_instance: Extend `deployment_targets` argument. +``` + +```release-note:bug +resource/aws_cloudformation_stack_set_instance: Add `ForceNew` to deployment_targets attributes to ensure a new resource is recreated when the deployment_targets argument is changed, which was not the case previously. +``` diff --git a/internal/service/cloudformation/stack_set_instance.go b/internal/service/cloudformation/stack_set_instance.go index a7edde6fbfa..de0f381e6e5 100644 --- a/internal/service/cloudformation/stack_set_instance.go +++ b/internal/service/cloudformation/stack_set_instance.go @@ -76,14 +76,41 @@ func resourceStackSetInstance() *schema.Resource { Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "organizational_unit_ids": { - Type: schema.TypeSet, - Optional: true, - MinItems: 1, + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + MinItems: 1, + ConflictsWith: []string{names.AttrAccountID}, Elem: &schema.Schema{ Type: schema.TypeString, ValidateFunc: validation.StringMatch(regexache.MustCompile(`^(ou-[0-9a-z]{4,32}-[0-9a-z]{8,32}|r-[0-9a-z]{4,32})$`), ""), }, }, + "account_filter_type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateFunc: validation.StringInSlice(enum.Slice(awstypes.AccountFilterType.Values("")...), false), + ConflictsWith: []string{names.AttrAccountID}, + }, + "accounts": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + ConflictsWith: []string{names.AttrAccountID}, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: verify.ValidAccountID, + }, + }, + "accounts_url": { + Type: schema.TypeString, + ForceNew: true, + Optional: true, + ConflictsWith: []string{names.AttrAccountID}, + ValidateFunc: validation.StringMatch(regexache.MustCompile(`(s3://|http(s?)://).+`), ""), + }, }, }, ConflictsWith: []string{names.AttrAccountID}, @@ -357,7 +384,6 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m return sdkdiag.AppendErrorf(diags, "finding CloudFormation StackSet Instance (%s): %s", d.Id(), err) } - d.Set("deployment_targets", flattenDeploymentTargetsFromSlice(orgIDs)) d.Set("stack_instance_summaries", flattenStackInstanceSummaries(summaries)) } @@ -368,7 +394,7 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData, var diags diag.Diagnostics conn := meta.(*conns.AWSClient).CloudFormationClient(ctx) - if d.HasChanges("deployment_targets", "parameter_overrides", "operation_preferences") { + if d.HasChanges("parameter_overrides", "operation_preferences") { parts, err := flex.ExpandResourceId(d.Id(), stackSetInstanceResourceIDPartCount, false) if err != nil { return sdkdiag.AppendFromErr(diags, err) @@ -388,13 +414,6 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData, input.CallAs = awstypes.CallAs(v.(string)) } - if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { - dt := expandDeploymentTargets(v.([]interface{})) - // reset input Accounts as the API accepts only 1 of Accounts and DeploymentTargets - input.Accounts = nil - input.DeploymentTargets = dt - } - if v, ok := d.GetOk("parameter_overrides"); ok { input.ParameterOverrides = expandParameters(v.(map[string]interface{})) } @@ -560,24 +579,17 @@ func expandDeploymentTargets(tfList []interface{}) *awstypes.DeploymentTargets { if v, ok := tfMap["organizational_unit_ids"].(*schema.Set); ok && v.Len() > 0 { dt.OrganizationalUnitIds = flex.ExpandStringValueSet(v) } - - return dt -} - -// flattenDeployment targets converts a list of organizational units (typically -// parsed from the resource ID) into the Terraform representation of the -// deployment_targets attribute. -func flattenDeploymentTargetsFromSlice(orgIDs []string) []interface{} { - tfList := []interface{}{} - for _, ou := range orgIDs { - tfList = append(tfList, ou) + if v, ok := tfMap["account_filter_type"].(string); ok && len(v) > 0 { + dt.AccountFilterType = awstypes.AccountFilterType(v) } - - m := map[string]interface{}{ - "organizational_unit_ids": tfList, + if v, ok := tfMap["accounts"].(*schema.Set); ok && v.Len() > 0 { + dt.Accounts = flex.ExpandStringValueSet(v) + } + if v, ok := tfMap["accounts_url"].(string); ok && len(v) > 0 { + dt.AccountsUrl = aws.String(v) } - return []interface{}{m} + return dt } func flattenStackInstanceSummaries(apiObject []awstypes.StackInstanceSummary) []interface{} { diff --git a/internal/service/cloudformation/stack_set_instance_test.go b/internal/service/cloudformation/stack_set_instance_test.go index 08ee3a87120..255935248e4 100644 --- a/internal/service/cloudformation/stack_set_instance_test.go +++ b/internal/service/cloudformation/stack_set_instance_test.go @@ -219,6 +219,9 @@ func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) { testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", acctest.Ct1), resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.organizational_unit_ids.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.account_filter_type", "INTERSECTION"), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.accounts.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.accounts_url", ""), ), }, { @@ -228,6 +231,7 @@ func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) { ImportStateVerifyIgnore: []string{ "retain_stack", "call_as", + "deployment_targets", }, }, { @@ -273,6 +277,7 @@ func TestAccCloudFormationStackSetInstance_DeploymentTargets_emptyOU(t *testing. ImportStateVerifyIgnore: []string{ "retain_stack", "call_as", + "deployment_targets", }, }, { @@ -812,6 +817,8 @@ resource "aws_cloudformation_stack_set_instance" "test" { deployment_targets { organizational_unit_ids = [data.aws_organizations_organization.test.roots[0].id] + account_filter_type = "INTERSECTION" + accounts = [data.aws_organizations_organization.test.non_master_accounts[0].id] } stack_set_name = aws_cloudformation_stack_set.test.name diff --git a/website/docs/r/cloudformation_stack_set_instance.html.markdown b/website/docs/r/cloudformation_stack_set_instance.html.markdown index 9fed412cb58..e180b086d57 100644 --- a/website/docs/r/cloudformation_stack_set_instance.html.markdown +++ b/website/docs/r/cloudformation_stack_set_instance.html.markdown @@ -87,7 +87,7 @@ This resource supports the following arguments: * `stack_set_name` - (Required) Name of the StackSet. * `account_id` - (Optional) Target AWS Account ID to create a Stack based on the StackSet. Defaults to current account. -* `deployment_targets` - (Optional) The AWS Organizations accounts to which StackSets deploys. StackSets doesn't deploy stack instances to the organization management account, even if the organization management account is in your organization or in an OU in your organization. Drift detection is not possible for this argument. See [deployment_targets](#deployment_targets-argument-reference) below. +* `deployment_targets` - (Optional) AWS Organizations accounts to which StackSets deploys. StackSets doesn't deploy stack instances to the organization management account, even if the organization management account is in your organization or in an OU in your organization. Drift detection is not possible for this argument. See [deployment_targets](#deployment_targets-argument-reference) below. * `parameter_overrides` - (Optional) Key-value map of input parameters to override from the StackSet for this Instance. * `region` - (Optional) Target AWS Region to create a Stack based on the StackSet. Defaults to current region. * `retain_stack` - (Optional) During Terraform resource destroy, remove Instance from StackSet while keeping the Stack and its associated resources. Must be enabled in Terraform state _before_ destroy operation to take effect. You cannot reassociate a retained Stack or add an existing, saved Stack to a new StackSet. Defaults to `false`. @@ -98,25 +98,28 @@ This resource supports the following arguments: The `deployment_targets` configuration block supports the following arguments: -* `organizational_unit_ids` - (Optional) The organization root ID or organizational unit (OU) IDs to which StackSets deploys. +* `organizational_unit_ids` - (Optional) Organization root ID or organizational unit (OU) IDs to which StackSets deploys. +* `account_filter_type` - (Optional) Limit deployment targets to individual accounts or include additional accounts with provided OUs. Valid values: `INTERSECTION`, `DIFFERENCE`, `UNION`, `NONE`. +* `accounts` - (Optional) List of accounts to deploy stack set updates. +* `accounts_url` - (Optional) S3 URL of the file containing the list of accounts. ### `operation_preferences` Argument Reference The `operation_preferences` configuration block supports the following arguments: -* `failure_tolerance_count` - (Optional) The number of accounts, per Region, for which this operation can fail before AWS CloudFormation stops the operation in that Region. -* `failure_tolerance_percentage` - (Optional) The percentage of accounts, per Region, for which this stack operation can fail before AWS CloudFormation stops the operation in that Region. -* `max_concurrent_count` - (Optional) The maximum number of accounts in which to perform this operation at one time. -* `max_concurrent_percentage` - (Optional) The maximum percentage of accounts in which to perform this operation at one time. -* `region_concurrency_type` - (Optional) The concurrency type of deploying StackSets operations in Regions, could be in parallel or one Region at a time. Valid values are `SEQUENTIAL` and `PARALLEL`. -* `region_order` - (Optional) The order of the Regions in where you want to perform the stack operation. +* `failure_tolerance_count` - (Optional) Number of accounts, per Region, for which this operation can fail before AWS CloudFormation stops the operation in that Region. +* `failure_tolerance_percentage` - (Optional) Percentage of accounts, per Region, for which this stack operation can fail before AWS CloudFormation stops the operation in that Region. +* `max_concurrent_count` - (Optional) Maximum number of accounts in which to perform this operation at one time. +* `max_concurrent_percentage` - (Optional) Maximum percentage of accounts in which to perform this operation at one time. +* `region_concurrency_type` - (Optional) Concurrency type of deploying StackSets operations in Regions, could be in parallel or one Region at a time. Valid values are `SEQUENTIAL` and `PARALLEL`. +* `region_order` - (Optional) Order of the Regions in where you want to perform the stack operation. ## Attribute Reference This resource exports the following attributes in addition to the arguments above: * `id` - Unique identifier for the resource. If `deployment_targets` is set, this is a comma-delimited string combining stack set name, organizational unit IDs (`/`-delimited), and region (ie. `mystack,ou-123/ou-456,us-east-1`). Otherwise, this is a comma-delimited string combining stack set name, AWS account ID, and region (ie. `mystack,123456789012,us-east-1`). -* `organizational_unit_id` - The organization root ID or organizational unit (OU) ID in which the stack is deployed. +* `organizational_unit_id` - Organization root ID or organizational unit (OU) ID in which the stack is deployed. * `stack_id` - Stack identifier. * `stack_instance_summaries` - List of stack instances created from an organizational unit deployment target. This will only be populated when `deployment_targets` is set. See [`stack_instance_summaries`](#stack_instance_summaries-attribute-reference).