diff --git a/.changelog/36794.txt b/.changelog/36794.txt new file mode 100644 index 000000000000..bb61b4c682c0 --- /dev/null +++ b/.changelog/36794.txt @@ -0,0 +1,7 @@ +```release-note:new-resource +aws_cloudformation_stack_instances +``` + +```release-note:bug +resource/aws_iam_role: Fix to reduce Terraform reporting differences when a role's ARN temporarily appears as the role's unique ID +``` \ No newline at end of file diff --git a/internal/service/cloudformation/exports_test.go b/internal/service/cloudformation/exports_test.go index a0c5f886c517..16e775e949cd 100644 --- a/internal/service/cloudformation/exports_test.go +++ b/internal/service/cloudformation/exports_test.go @@ -8,12 +8,15 @@ var ( ResourceStack = resourceStack ResourceStackSet = resourceStackSet ResourceStackSetInstance = resourceStackSetInstance + ResourceStackInstances = resourceStackInstances ResourceType = resourceType FindStackInstanceByFourPartKey = findStackInstanceByFourPartKey FindStackInstanceSummariesByFourPartKey = findStackInstanceSummariesByFourPartKey FindStackSetByName = findStackSetByName FindTypeByARN = findTypeByARN + FindStackInstancesByNameCallAs = findStackInstancesByNameCallAs StackSetInstanceResourceIDPartCount = stackSetInstanceResourceIDPartCount + StackInstancesResourceIDPartCount = stackInstancesResourceIDPartCount TypeVersionARNToTypeARNAndVersionID = typeVersionARNToTypeARNAndVersionID ) diff --git a/internal/service/cloudformation/service_package_gen.go b/internal/service/cloudformation/service_package_gen.go index d47138c21fb6..b627b2a40c40 100644 --- a/internal/service/cloudformation/service_package_gen.go +++ b/internal/service/cloudformation/service_package_gen.go @@ -49,6 +49,11 @@ func (p *servicePackage) SDKResources(ctx context.Context) []*types.ServicePacka Name: "Stack", Tags: &types.ServicePackageResourceTags{}, }, + { + Factory: resourceStackInstances, + TypeName: "aws_cloudformation_stack_instances", + Name: "Stack Instances", + }, { Factory: resourceStackSet, TypeName: "aws_cloudformation_stack_set", diff --git a/internal/service/cloudformation/stack_instances.go b/internal/service/cloudformation/stack_instances.go new file mode 100644 index 000000000000..a32f57ce49e7 --- /dev/null +++ b/internal/service/cloudformation/stack_instances.go @@ -0,0 +1,773 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudformation + +import ( + "context" + "fmt" + "log" + "strings" + "time" + + "github.com/YakDriver/regexache" + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/cloudformation" + awstypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + sdkid "github.com/hashicorp/terraform-plugin-sdk/v2/helper/id" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/hashicorp/terraform-provider-aws/internal/conns" + "github.com/hashicorp/terraform-provider-aws/internal/create" + "github.com/hashicorp/terraform-provider-aws/internal/enum" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/internal/verify" + "github.com/hashicorp/terraform-provider-aws/names" +) + +const ( + stackInstancesResourceIDPartCount = 3 // stack_set_name, call_as, OU + regionAcctOrgIDSeparator = "/" + ResNameStackInstances = "Stack Instances" +) + +// @SDKResource("aws_cloudformation_stack_instances", name="Stack Instances") +func resourceStackInstances() *schema.Resource { + return &schema.Resource{ + CreateWithoutTimeout: resourceStackInstancesCreate, + ReadWithoutTimeout: resourceStackInstancesRead, + UpdateWithoutTimeout: resourceStackInstancesUpdate, + DeleteWithoutTimeout: resourceStackInstancesDelete, + + Importer: &schema.ResourceImporter{ + StateContext: resourceStackInstancesImport, + }, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(30 * time.Minute), + Update: schema.DefaultTimeout(30 * time.Minute), + Delete: schema.DefaultTimeout(30 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + AttrAccounts: { // create input, read input (single account), update input, delete input + Type: schema.TypeSet, + Optional: true, + Computed: true, + ConflictsWith: []string{"deployment_targets"}, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: verify.ValidAccountID, + }, + }, + "call_as": { // create input, read input, update input, delete input + Type: schema.TypeString, + Optional: true, + Default: awstypes.CallAsSelf, + ValidateDiagFunc: enum.Validate[awstypes.CallAs](), + }, + "deployment_targets": { // create input, update input, delete input + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "account_filter_type": { // create input + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(enum.Slice(awstypes.AccountFilterType.Values("")...), false), + ForceNew: true, + ConflictsWith: []string{AttrAccounts}, + }, + AttrAccounts: { // create input + Type: schema.TypeSet, + Optional: true, + ConflictsWith: []string{AttrAccounts}, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: verify.ValidAccountID, + }, + }, + "accounts_url": { // create input + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{AttrAccounts}, + ValidateFunc: validation.StringMatch(regexache.MustCompile(`(s3://|http(s?)://).+`), ""), + }, + "organizational_unit_ids": { // create input + Type: schema.TypeSet, + Optional: true, + MinItems: 1, + ConflictsWith: []string{AttrAccounts}, + 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})$`), ""), + }, + }, + }, + }, + ConflictsWith: []string{AttrAccounts}, + }, + "operation_preferences": { // create input, update input, delete input + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "concurrency_mode": { // create input + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.ConcurrencyMode](), + }, + "failure_tolerance_count": { // create input + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(0), + ConflictsWith: []string{"operation_preferences.0.failure_tolerance_percentage"}, + }, + "failure_tolerance_percentage": { // create input + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(0, 100), + ConflictsWith: []string{"operation_preferences.0.failure_tolerance_count"}, + }, + "max_concurrent_count": { // create input + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntAtLeast(1), + ConflictsWith: []string{"operation_preferences.0.max_concurrent_percentage"}, + }, + "max_concurrent_percentage": { // create input + Type: schema.TypeInt, + Optional: true, + ValidateFunc: validation.IntBetween(1, 100), + ConflictsWith: []string{"operation_preferences.0.max_concurrent_count"}, + }, + "region_concurrency_type": { // create input + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: enum.Validate[awstypes.RegionConcurrencyType](), + }, + "region_order": { // create input + Type: schema.TypeList, + Optional: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringMatch(regexache.MustCompile(`^[0-9A-Za-z-]{1,128}$`), ""), + }, + }, + }, + }, + }, + "parameter_overrides": { // create input, update input + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + AttrRegions: { // create input - required, read input (single region), update input - required, delete input - required + Type: schema.TypeSet, + Optional: true, + Computed: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: verify.ValidRegionName, + }, + }, + "retain_stacks": { // delete input - required + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "stack_instance_summaries": { // read output + Type: schema.TypeList, + Computed: true, + Description: "List of stack instances created from an organizational unit deployment target. " + + "This will only be populated when `deployment_targets` is set.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + names.AttrAccountID: { // read output + Type: schema.TypeString, + Computed: true, + }, + "detailed_status": { // read output + Type: schema.TypeString, + Computed: true, + }, + "drift_status": { // read output + Type: schema.TypeString, + Computed: true, + }, + "organizational_unit_id": { // read output + Type: schema.TypeString, + Computed: true, + }, + names.AttrRegion: { // read output + Type: schema.TypeString, + Computed: true, + }, + "stack_id": { // read output + Type: schema.TypeString, + Computed: true, + }, + "stack_set_id": { // read output + Type: schema.TypeString, + Computed: true, + }, + names.AttrStatus: { // read output + Type: schema.TypeString, + Computed: true, + }, + names.AttrStatusReason: { // read output + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + "stack_set_id": { // read output + Type: schema.TypeString, + Computed: true, + }, + "stack_set_name": { // create input - required, read input - required, update input - required, delete input - required + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.NoZeroValues, + }, + }, + } +} + +func resourceStackInstancesCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).CloudFormationClient(ctx) + + stackSetName := d.Get("stack_set_name").(string) + input := &cloudformation.CreateStackInstancesInput{ + StackSetName: aws.String(stackSetName), + } + + if v, ok := d.GetOk(AttrRegions); ok && v.(*schema.Set).Len() > 0 { + input.Regions = flex.ExpandStringValueSet(v.(*schema.Set)) + } + + if v, ok := d.GetOk(AttrRegions); !ok || v.(*schema.Set).Len() == 0 { + input.Regions = []string{meta.(*conns.AWSClient).Region} + } + + if v, ok := d.GetOk(AttrAccounts); ok && v.(*schema.Set).Len() > 0 { + input.Accounts = flex.ExpandStringValueSet(v.(*schema.Set)) + } + + deployedByOU := "" + if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.DeploymentTargets = expandDeploymentTargets(v.([]interface{})) + input.Accounts = nil + + if v, ok := d.GetOk("deployment_targets.0.organizational_unit_ids"); ok && len(v.(*schema.Set).List()) > 0 { + deployedByOU = "OU" + } + } else { + input.Accounts = []string{meta.(*conns.AWSClient).AccountID} + } + + callAs := d.Get("call_as").(string) + if v, ok := d.GetOk("call_as"); ok { + input.CallAs = awstypes.CallAs(v.(string)) + } + + if v, ok := d.GetOk("parameter_overrides"); ok { + input.ParameterOverrides = expandParameters(v.(map[string]interface{})) + } + + if v, ok := d.GetOk("operation_preferences"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.OperationPreferences = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{})) + } + + id, err := flex.FlattenResourceId([]string{stackSetName, callAs, deployedByOU}, stackInstancesResourceIDPartCount, true) + if err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionFlatteningResourceId, ResNameStackInstances, stackSetName, err) + } + + _, err = tfresource.RetryWhen(ctx, propagationTimeout, + func() (interface{}, error) { + input.OperationId = aws.String(sdkid.UniqueId()) + + output, err := conn.CreateStackInstances(ctx, input) + if err != nil { + return nil, err + } + + d.SetId(id) + + operation, err := waitStackSetOperationSucceeded(ctx, conn, stackSetName, aws.ToString(output.OperationId), callAs, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return nil, fmt.Errorf("waiting for create: %w", err) + } + + return operation, nil + }, + func(err error) (bool, error) { + if err == nil { + return false, nil + } + + message := err.Error() + + // IAM eventual consistency + if strings.Contains(message, "AccountGate check failed") { + return true, err + } + + // IAM eventual consistency + // User: XXX is not authorized to perform: cloudformation:CreateStack on resource: YYY + if strings.Contains(message, "is not authorized") { + return true, err + } + + // IAM eventual consistency + // XXX role has insufficient YYY permissions + if strings.Contains(message, "role has insufficient") { + return true, err + } + + // IAM eventual consistency + // Account XXX should have YYY role with trust relationship to Role ZZZ + if strings.Contains(message, "role with trust relationship") { + return true, err + } + + // IAM eventual consistency + if strings.Contains(message, "The security token included in the request is invalid") { + return true, err + } + + return false, err + }, + ) + + if err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionCreating, ResNameStackInstances, id, err) + } + + return append(diags, resourceStackInstancesRead(ctx, d, meta)...) +} + +func resourceStackInstancesRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + // Dramatic simplification of the ID from stack_set_instance removing regions and accounts. The upside + // is simplicity. The downside is that we hoover up all stack instances for the stack set. + parts, err := flex.ExpandResourceId(d.Id(), stackInstancesResourceIDPartCount, true) + if err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionExpandingResourceId, ResNameStackInstances, d.Id(), err) + } + + stackSetName, callAs, deployedByOU := parts[0], parts[1], parts[2] + d.Set("stack_set_name", stackSetName) + + if callAs == "" { + callAs = d.Get("call_as").(string) + } + + stackInstances, err := findStackInstancesByNameCallAs(ctx, meta, stackSetName, callAs, deployedByOU == "OU", flex.ExpandStringValueSet(d.Get(AttrAccounts).(*schema.Set)), flex.ExpandStringValueSet(d.Get(AttrRegions).(*schema.Set))) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] CloudFormation Stack Instances (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } + + if err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionReading, ResNameStackInstances, d.Id(), err) + } + + if len(stackInstances.OUs) > 0 { + d.Set("deployment_targets", replaceOrganizationalUnitIDs(d.Get("deployment_targets").([]interface{}), stackInstances.OUs)) + } + + d.Set(AttrAccounts, flex.FlattenStringValueList(stackInstances.Accounts)) + d.Set(AttrRegions, flex.FlattenStringValueList(stackInstances.Regions)) + d.Set("stack_instance_summaries", flattenStackInstancesSummaries(stackInstances.Summaries)) + d.Set("stack_set_id", stackInstances.StackSetID) + + if err := d.Set("parameter_overrides", flattenAllParameters(stackInstances.ParameterOverrides)); err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionReading, ResNameStackInstances, d.Id(), err) + } + + return diags +} + +const ( + AttrAccounts = "accounts" + AttrDTAccounts = "deployment_targets.0.accounts" + AttrDTOUs = "deployment_targets.0.organizational_unit_ids" + AttrRegions = "regions" +) + +func resourceStackInstancesUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + conn := meta.(*conns.AWSClient).CloudFormationClient(ctx) + + accounts := flex.ExpandStringValueSet(d.Get(AttrAccounts).(*schema.Set)) + regions := flex.ExpandStringValueSet(d.Get(AttrRegions).(*schema.Set)) + dtAccounts := flex.ExpandStringValueSet(d.Get(AttrDTAccounts).(*schema.Set)) + dtOUs := flex.ExpandStringValueSet(d.Get(AttrDTOUs).(*schema.Set)) + + if d.HasChange(AttrRegions) { + oRaw, nRaw := d.GetChange(AttrRegions) + o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) + + if axe := o.Difference(n); axe.Len() > 0 { + if err := deleteStackInstances(ctx, d, meta, accounts, flex.ExpandStringValueSet(axe), dtAccounts, dtOUs); err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + } + } + + if add := n.Difference(o); add.Len() > 0 { + diags = append(diags, resourceStackInstancesCreate(ctx, d, meta)...) + if diags.HasError() { + return diags + } + } + } + + if d.HasChange(AttrAccounts) { + oRaw, nRaw := d.GetChange(AttrAccounts) + o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) + + if axe := o.Difference(n); axe.Len() > 0 { + if err := deleteStackInstances(ctx, d, meta, flex.ExpandStringValueSet(axe), regions, dtAccounts, dtOUs); err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + } + } + + if add := n.Difference(o); add.Len() > 0 { + diags = append(diags, resourceStackInstancesCreate(ctx, d, meta)...) + if diags.HasError() { + return diags + } + } + } + + if d.HasChange(AttrDTAccounts) { + oRaw, nRaw := d.GetChange(AttrDTAccounts) + o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) + + if axe := o.Difference(n); axe.Len() > 0 { + if err := deleteStackInstances(ctx, d, meta, accounts, regions, flex.ExpandStringValueSet(axe), dtOUs); err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + } + } + + if add := n.Difference(o); add.Len() > 0 { + diags = append(diags, resourceStackInstancesCreate(ctx, d, meta)...) + if diags.HasError() { + return diags + } + } + } + + if d.HasChange(AttrDTOUs) { + oRaw, nRaw := d.GetChange(AttrDTOUs) + o, n := oRaw.(*schema.Set), nRaw.(*schema.Set) + + if axe := o.Difference(n); axe.Len() > 0 { + if err := deleteStackInstances(ctx, d, meta, accounts, regions, dtAccounts, flex.ExpandStringValueSet(axe)); err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + } + } + + if add := n.Difference(o); add.Len() > 0 { + diags = append(diags, resourceStackInstancesCreate(ctx, d, meta)...) + if diags.HasError() { + return diags + } + } + } + + if d.HasChanges( + "call_as", + "deployment_targets.0.accounts_url", + "operation_preferences", + "parameter_overrides", + ) { + input := &cloudformation.UpdateStackInstancesInput{ + OperationId: aws.String(sdkid.UniqueId()), + ParameterOverrides: []awstypes.Parameter{}, + Regions: flex.ExpandStringValueSet(d.Get(AttrRegions).(*schema.Set)), + StackSetName: aws.String(d.Get("stack_set_name").(string)), + } + + // can only give either accounts or deployment_targets + input.Accounts = []string{meta.(*conns.AWSClient).AccountID} + if v, ok := d.GetOk(AttrAccounts); ok && v.(*schema.Set).Len() > 0 { + input.Accounts = flex.ExpandStringValueSet(v.(*schema.Set)) + } + + if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.DeploymentTargets = expandDeploymentTargets(v.([]interface{})) + input.Accounts = nil + } + + if v, ok := d.GetOk("call_as"); ok { + input.CallAs = awstypes.CallAs(v.(string)) + } + + if v, ok := d.GetOk("operation_preferences"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.OperationPreferences = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{})) + } + + if v, ok := d.GetOk("parameter_overrides"); ok { + input.ParameterOverrides = expandParameters(v.(map[string]interface{})) + } + + output, err := conn.UpdateStackInstances(ctx, input) + if err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionUpdating, ResNameStackInstances, d.Id(), err) + } + + if _, err := waitStackSetOperationSucceeded(ctx, conn, d.Get("stack_set_name").(string), aws.ToString(output.OperationId), d.Get("call_as").(string), d.Timeout(schema.TimeoutUpdate)); err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionWaitingForUpdate, ResNameStackInstances, d.Id(), err) + } + } + + return append(diags, resourceStackInstancesRead(ctx, d, meta)...) +} + +func resourceStackInstancesDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var diags diag.Diagnostics + + accounts := flex.ExpandStringValueSet(d.Get(AttrAccounts).(*schema.Set)) + regions := flex.ExpandStringValueSet(d.Get(AttrRegions).(*schema.Set)) + dtAccounts := flex.ExpandStringValueSet(d.Get(AttrDTAccounts).(*schema.Set)) + dtOUs := flex.ExpandStringValueSet(d.Get(AttrDTOUs).(*schema.Set)) + + if err := deleteStackInstances(ctx, d, meta, accounts, regions, dtAccounts, dtOUs); err != nil { + return create.AppendDiagError(diags, names.CloudFormation, create.ErrActionDeleting, ResNameStackInstances, d.Id(), err) + } + + return diag.Diagnostics{} +} + +func deleteStackInstances(ctx context.Context, d *schema.ResourceData, meta interface{}, accounts, regions, dtAccounts, dtOUs []string) error { + conn := meta.(*conns.AWSClient).CloudFormationClient(ctx) + + input := &cloudformation.DeleteStackInstancesInput{ + OperationId: aws.String(sdkid.UniqueId()), + Accounts: accounts, + Regions: regions, + RetainStacks: aws.Bool(d.Get("retain_stacks").(bool)), + StackSetName: aws.String(d.Get("stack_set_name").(string)), + } + + // can only give either accounts or deployment_targets + if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.DeploymentTargets = expandDeploymentTargets(v.([]interface{})) + input.DeploymentTargets.Accounts = dtAccounts + input.DeploymentTargets.OrganizationalUnitIds = dtOUs + input.Accounts = nil + } + + if v, ok := d.GetOk("call_as"); ok { + input.CallAs = awstypes.CallAs(v.(string)) + } + + if v, ok := d.GetOk("operation_preferences"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + input.OperationPreferences = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{})) + } + + log.Printf("[DEBUG] Deleting CloudFormation Stack Instances: %s", d.Id()) + outputRaw, err := tfresource.RetryWhenIsA[*awstypes.OperationInProgressException](ctx, d.Timeout(schema.TimeoutDelete), func() (interface{}, error) { + return conn.DeleteStackInstances(ctx, input) + }) + + if errs.IsA[*awstypes.StackInstanceNotFoundException](err) || errs.IsA[*awstypes.StackSetNotFoundException](err) { + return nil + } + + if err != nil { + return err + } + + if _, err := waitStackSetOperationSucceeded(ctx, conn, d.Get("stack_set_name").(string), aws.ToString(outputRaw.(*cloudformation.DeleteStackInstancesOutput).OperationId), d.Get("call_as").(string), d.Timeout(schema.TimeoutDelete)); err != nil { + return err + } + + return nil +} + +func resourceStackInstancesImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + switch parts := strings.Split(d.Id(), flex.ResourceIdSeparator); len(parts) { + case 1: + case 2: + case 3: + d.SetId(strings.Join([]string{parts[0], parts[1], parts[2]}, flex.ResourceIdSeparator)) + d.Set("call_as", parts[1]) + default: + return []*schema.ResourceData{}, fmt.Errorf("unexpected format for import ID (%[1]s), use: STACKSET_NAME or STACKSET_NAME%[2]sCALLAS or STACKSET_NAME%[2]sCALLAS%[2]sOU", d.Id(), flex.ResourceIdSeparator) + } + + return []*schema.ResourceData{d}, nil +} + +// StackInstances is a helper struct to hold the describeable/listable info for stack instances. +type StackInstances struct { + Accounts []string + OUs []string + Regions []string + StackSetID string + Summaries []awstypes.StackInstanceSummary + ParameterOverrides []awstypes.Parameter +} + +// findStackInstancesByNameCallAs is a helper function to find stack instances by stackSetName and callAs. +// accounts and regions are not used unless no summaries are returned. When no summaries are returned, +// drift detection is limited because we're just using the accounts and regions from config--we have no +// other choice. When there are no summaries, we use the first account and region to find a stack instance +// to get the parameter_overrides and stack_set_id. +func findStackInstancesByNameCallAs(ctx context.Context, meta interface{}, stackSetName, callAs string, deployedByOU bool, accounts, regions []string) (StackInstances, error) { + conn := meta.(*conns.AWSClient).CloudFormationClient(ctx) + + input := &cloudformation.ListStackInstancesInput{ + StackSetName: aws.String(stackSetName), + } + + if callAs != "" { + input.CallAs = awstypes.CallAs(callAs) + } + + var output StackInstances + none := true + pages := cloudformation.NewListStackInstancesPaginator(conn, input) + for pages.HasMorePages() { + page, err := pages.NextPage(ctx) + + if errs.IsA[*awstypes.StackInstanceNotFoundException](err) || errs.IsA[*awstypes.StackSetNotFoundException](err) { + return output, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil { + return output, err + } + + none = false + + for _, v := range page.Summaries { + if aws.ToString(v.StackSetId) != "" && output.StackSetID == "" { + output.StackSetID = aws.ToString(v.StackSetId) + } + + output.Summaries = append(output.Summaries, v) + + if aws.ToString(v.Account) != "" { + output.Accounts = append(output.Accounts, aws.ToString(v.Account)) + } + + if aws.ToString(v.Region) != "" { + output.Regions = append(output.Regions, aws.ToString(v.Region)) + } + + if aws.ToString(v.OrganizationalUnitId) != "" { + output.OUs = append(output.OUs, aws.ToString(v.OrganizationalUnitId)) + } + } + } + + if len(output.Accounts) == 0 && len(accounts) > 0 { + output.Accounts = accounts + } + + if len(output.Accounts) == 0 && len(accounts) == 0 { + output.Accounts = []string{meta.(*conns.AWSClient).AccountID} + } + + if len(output.Regions) == 0 && len(regions) > 0 { + output.Regions = regions + } + + if len(output.Regions) == 0 && len(regions) == 0 { + output.Regions = []string{meta.(*conns.AWSClient).Region} + } + + if deployedByOU { + return output, nil + } + + // set based on the first account and region which means they may not be accurate for all stack instances + stackInstance, err := findStackInstanceByFourPartKey(ctx, conn, stackSetName, output.Accounts[0], output.Regions[0], callAs) + if none || tfresource.NotFound(err) { + return output, &retry.NotFoundError{ + LastError: err, + LastRequest: input, + } + } + + if err != nil && !tfresource.NotFound(err) { + return output, err + } + + if stackInstance != nil && output.StackSetID == "" { + output.StackSetID = aws.ToString(stackInstance.StackSetId) + } + + if stackInstance != nil && stackInstance.ParameterOverrides != nil { + output.ParameterOverrides = stackInstance.ParameterOverrides + } + + return output, nil +} + +func replaceOrganizationalUnitIDs(tfList []interface{}, newOUIDs []string) []interface{} { + if len(tfList) == 0 || tfList[0] == nil { + return nil + } + + tfMap, ok := tfList[0].(map[string]interface{}) + if !ok { + return nil + } + + // Update the "organizational_unit_ids" with the new value + tfMap["organizational_unit_ids"] = flex.FlattenStringValueList(newOUIDs) + + return []interface{}{tfMap} +} + +func flattenStackInstancesSummaries(apiObject []awstypes.StackInstanceSummary) []interface{} { + if len(apiObject) == 0 { + return nil + } + + tfList := []interface{}{} + for _, obj := range apiObject { + m := map[string]interface{}{ + names.AttrAccountID: obj.Account, + "drift_status": obj.DriftStatus, + "organizational_unit_id": obj.OrganizationalUnitId, + names.AttrRegion: obj.Region, + "stack_id": obj.StackId, + "stack_set_id": obj.StackSetId, + names.AttrStatus: obj.Status, + names.AttrStatusReason: obj.StatusReason, + } + + if obj.StackInstanceStatus != nil { + m["detailed_status"] = obj.StackInstanceStatus.DetailedStatus + } + + tfList = append(tfList, m) + } + + return tfList +} diff --git a/internal/service/cloudformation/stack_instances_test.go b/internal/service/cloudformation/stack_instances_test.go new file mode 100644 index 000000000000..b4a123ce6826 --- /dev/null +++ b/internal/service/cloudformation/stack_instances_test.go @@ -0,0 +1,1127 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package cloudformation_test + +import ( + "context" + "fmt" + "strconv" + "strings" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + awstypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + sdkacctest "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/hashicorp/terraform-provider-aws/internal/acctest" + "github.com/hashicorp/terraform-provider-aws/internal/errs" + "github.com/hashicorp/terraform-provider-aws/internal/flex" + tfcloudformation "github.com/hashicorp/terraform-provider-aws/internal/service/cloudformation" + "github.com/hashicorp/terraform-provider-aws/internal/tfresource" + "github.com/hashicorp/terraform-provider-aws/names" +) + +func TestAccCloudFormationStackInstances_basic(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + cloudformationStackSetResourceName := "aws_cloudformation_stack_set.test" + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_basic(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + resource.TestCheckResourceAttr(resourceName, "accounts.#", acctest.Ct1), + acctest.CheckResourceAttrAccountID(resourceName, "accounts.0"), + resource.TestCheckResourceAttr(resourceName, "call_as", "SELF"), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "regions.0", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "retain_stacks", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct1), + acctest.CheckResourceAttrAccountID(resourceName, "stack_instance_summaries.0.account_id"), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.0.drift_status", "NOT_CHECKED"), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.0.region", acctest.Region()), + resource.TestCheckResourceAttrSet(resourceName, "stack_instance_summaries.0.stack_id"), + resource.TestCheckResourceAttrSet(resourceName, "stack_instance_summaries.0.stack_set_id"), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.0.status", "CURRENT"), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stacks", + }, + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_disappears(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcloudformation.ResourceStackInstances(), resourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_Disappears_stackSet(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1 tfcloudformation.StackInstances + var stackSet1 awstypes.StackSet + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + stackSetResourceName := "aws_cloudformation_stack_set.test" + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetExists(ctx, stackSetResourceName, &stackSet1), + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcloudformation.ResourceStackInstances(), resourceName), + acctest.CheckResourceDisappears(ctx, acctest.Provider, tfcloudformation.ResourceStackSet(), stackSetResourceName), + ), + ExpectNonEmptyPlan: true, + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_Multi_increaseRegions(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1, stackInstances2 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + cloudformationStackSetResourceName := "aws_cloudformation_stack_set.test" + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.Region()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + resource.TestCheckResourceAttr(resourceName, "accounts.#", acctest.Ct1), + acctest.CheckResourceAttrAccountID(resourceName, "accounts.0"), + resource.TestCheckResourceAttr(resourceName, "call_as", "SELF"), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "retain_stacks", acctest.CtFalse), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct1), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.Region(), acctest.AlternateRegion()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances2), + testAccCheckStackInstancesNotRecreated(&stackInstances1, &stackInstances2), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct2), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.Region()), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.AlternateRegion()), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct2), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_Multi_decreaseRegions(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1, stackInstances2 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + cloudformationStackSetResourceName := "aws_cloudformation_stack_set.test" + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.Region(), acctest.AlternateRegion()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct2), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.Region()), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.AlternateRegion()), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct2), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.Region()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances2), + testAccCheckStackInstancesNotRecreated(&stackInstances1, &stackInstances2), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct1), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_Multi_swapRegions(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1, stackInstances2 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + cloudformationStackSetResourceName := "aws_cloudformation_stack_set.test" + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.Region()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.Region()), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct1), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + { + Config: testAccStackInstancesConfig_regions(rName, []string{acctest.AlternateRegion()}), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances2), + testAccCheckStackInstancesNotRecreated(&stackInstances1, &stackInstances2), + resource.TestCheckResourceAttr(resourceName, "regions.#", acctest.Ct1), + resource.TestCheckTypeSetElemAttr(resourceName, "regions.*", acctest.AlternateRegion()), + resource.TestCheckResourceAttr(resourceName, "stack_instance_summaries.#", acctest.Ct1), + resource.TestCheckResourceAttrPair(resourceName, "stack_set_name", cloudformationStackSetResourceName, names.AttrName), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_parameterOverrides(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances1, stackInstances2, stackInstances3, stackInstances4 tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(ctx, t); testAccPreCheckStackSet(ctx, t) }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_parameterOverrides1(rName, "overridevalue1"), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances1), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.Parameter1", "overridevalue1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stacks", + "call_as", + }, + }, + { + Config: testAccStackInstancesConfig_parameterOverrides2(rName, "overridevalue1updated", "overridevalue2"), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances2), + testAccCheckStackInstancesNotRecreated(&stackInstances1, &stackInstances2), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct2), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.Parameter1", "overridevalue1updated"), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.Parameter2", "overridevalue2"), + ), + }, + { + Config: testAccStackInstancesConfig_parameterOverrides1(rName, "overridevalue1updated"), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances3), + testAccCheckStackInstancesNotRecreated(&stackInstances2, &stackInstances3), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.Parameter1", "overridevalue1updated"), + ), + }, + { + Config: testAccStackInstancesConfig_basic(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesExists(ctx, resourceName, &stackInstances4), + testAccCheckStackInstancesNotRecreated(&stackInstances3, &stackInstances4), + resource.TestCheckResourceAttr(resourceName, "parameter_overrides.%", acctest.Ct0), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_deploymentTargets(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckStackSet(ctx, t) + acctest.PreCheckOrganizationsEnabled(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + acctest.PreCheckIAMServiceLinkedRole(ctx, t, "/aws-service-role/stacksets.cloudformation.amazonaws.com") + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationEndpointID, "organizations"), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_deploymentTargets(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + 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", ""), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stacks", + "call_as", + "deployment_targets", + }, + }, + { + Config: testAccStackInstancesConfig_deploymentTargets(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_DeploymentTargets_emptyOU(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckStackSet(ctx, t) + acctest.PreCheckOrganizationsEnabled(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + acctest.PreCheckIAMServiceLinkedRole(ctx, t, "/aws-service-role/stacksets.cloudformation.amazonaws.com") + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationEndpointID, "organizations"), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_DeploymentTargets_emptyOU(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.organizational_unit_ids.#", acctest.Ct1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stacks", + "call_as", + "deployment_targets", + }, + }, + { + Config: testAccStackInstancesConfig_DeploymentTargets_emptyOU(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_operationPreferences(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckStackSet(ctx, t) + acctest.PreCheckOrganizationsEnabled(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + acctest.PreCheckIAMServiceLinkedRole(ctx, t, "/aws-service-role/stacksets.cloudformation.amazonaws.com") + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_operationPreferences(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.concurrency_mode", ""), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_count", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_percentage", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.max_concurrent_count", acctest.Ct10), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.max_concurrent_percentage", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.region_concurrency_type", ""), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackInstances_concurrencyMode(t *testing.T) { + ctx := acctest.Context(t) + var stackInstances tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckStackSet(ctx, t) + acctest.PreCheckOrganizationsEnabled(ctx, t) + acctest.PreCheckOrganizationManagementAccount(ctx, t) + acctest.PreCheckIAMServiceLinkedRole(ctx, t, "/aws-service-role/stacksets.cloudformation.amazonaws.com") + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationServiceID), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackInstancesConfig_concurrencyMode(rName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.concurrency_mode", "SOFT_FAILURE_TOLERANCE"), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_count", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_percentage", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.max_concurrent_count", acctest.Ct10), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.max_concurrent_percentage", acctest.Ct0), + resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.region_concurrency_type", ""), + ), + }, + }, + }) +} + +// https://github.com/hashicorp/terraform-provider-aws/issues/32536. +func TestAccCloudFormationStackInstances_delegatedAdministrator(t *testing.T) { + ctx := acctest.Context(t) + providers := make(map[string]*schema.Provider) + var stackInstances tfcloudformation.StackInstances + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_instances.test" + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { + acctest.PreCheck(ctx, t) + testAccPreCheckStackSet(ctx, t) + acctest.PreCheckAlternateAccount(t) + acctest.PreCheckOrganizationMemberAccount(ctx, t) + acctest.PreCheckIAMServiceLinkedRole(ctx, t, "/aws-service-role/member.org.stacksets.cloudformation.amazonaws.com") + }, + ErrorCheck: acctest.ErrorCheck(t, names.CloudFormationEndpointID, "organizations"), + ProtoV5ProviderFactories: acctest.ProtoV5FactoriesNamedAlternate(ctx, t, providers), + CheckDestroy: testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + // Run a simple configuration to initialize the alternate providers + Config: testAccStackSetConfig_delegatedAdministratorInit, + }, + { + PreConfig: func() { + // Can only run check here because the provider is not available until the previous step. + acctest.PreCheckOrganizationManagementAccountWithProvider(ctx, t, acctest.NamedProviderFunc(acctest.ProviderNameAlternate, providers)) + acctest.PreCheckIAMServiceLinkedRoleWithProvider(ctx, t, acctest.NamedProviderFunc(acctest.ProviderNameAlternate, providers), "/aws-service-role/stacksets.cloudformation.amazonaws.com") + }, + Config: testAccStackInstancesConfig_delegatedAdministrator(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackInstancesForOrganizationalUnitExists(ctx, resourceName, stackInstances), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", acctest.Ct1), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.organizational_unit_ids.#", acctest.Ct1), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return "", fmt.Errorf("Not found: %s", resourceName) + } + + return fmt.Sprintf("%s,DELEGATED_ADMIN", rs.Primary.ID), nil + }, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stacks", + "call_as", + }, + }, + }, + }) +} + +func testAccCheckStackInstancesExists(ctx context.Context, resourceName string, v *tfcloudformation.StackInstances) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + parts, err := flex.ExpandResourceId(rs.Primary.ID, tfcloudformation.StackInstancesResourceIDPartCount, true) + if err != nil { + return err + } + + stackSetName := parts[0] + callAs := rs.Primary.Attributes["call_as"] + + var accounts []string + for i := 0; i < attributeLength(rs.Primary.Attributes["accounts.#"]); i++ { + accounts = append(accounts, rs.Primary.Attributes[fmt.Sprintf("accounts.%d", i)]) + } + + var regions []string + for i := 0; i < attributeLength(rs.Primary.Attributes["regions.#"]); i++ { + regions = append(regions, rs.Primary.Attributes[fmt.Sprintf("regions.%d", i)]) + } + + deployedByOU := false + if rs.Primary.Attributes["deployment_targets.#"] != acctest.Ct0 && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != acctest.Ct0 { + deployedByOU = true + } + + output, err := tfcloudformation.FindStackInstancesByNameCallAs(ctx, acctest.Provider.Meta(), stackSetName, callAs, deployedByOU, accounts, regions) + + if err != nil { + return err + } + + *v = output + + return nil + } +} + +func attributeLength(attribute string) int { + return errs.Must(strconv.Atoi(attribute)) +} + +// testAccCheckStackInstancesForOrganizationalUnitExists is a variant of the +// standard CheckExistsFunc which expects the resource ID to contain organizational +// unit IDs rather than an account ID +func testAccCheckStackInstancesForOrganizationalUnitExists(ctx context.Context, resourceName string, v tfcloudformation.StackInstances) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + parts, err := flex.ExpandResourceId(rs.Primary.ID, tfcloudformation.StackInstancesResourceIDPartCount, false) + if err != nil { + return err + } + + stackSetName := parts[0] + callAs := rs.Primary.Attributes["call_as"] + var accounts []string + for i := 0; i < attributeLength(rs.Primary.Attributes["accounts.#"]); i++ { + accounts = append(accounts, rs.Primary.Attributes[fmt.Sprintf("accounts.%d", i)]) + } + + var regions []string + for i := 0; i < attributeLength(rs.Primary.Attributes["regions.#"]); i++ { + regions = append(regions, rs.Primary.Attributes[fmt.Sprintf("regions.%d", i)]) + } + + deployedByOU := false + if rs.Primary.Attributes["deployment_targets.#"] != acctest.Ct0 && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != acctest.Ct0 { + deployedByOU = true + } + + output, err := tfcloudformation.FindStackInstancesByNameCallAs(ctx, acctest.Provider.Meta(), stackSetName, callAs, deployedByOU, accounts, regions) + + if err != nil { + return err + } + + v = output + + return nil + } +} + +// testAccCheckStackInstancesForOrganizationalUnitDestroy is a variant of the +// standard CheckDestroyFunc which expects the resource ID to contain organizational +// unit IDs rather than an account ID +func testAccCheckStackInstancesForOrganizationalUnitDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudformation_stack_instances" { + continue + } + + parts, err := flex.ExpandResourceId(rs.Primary.ID, tfcloudformation.StackInstancesResourceIDPartCount, false) + if err != nil { + return err + } + + stackSetName := parts[0] + callAs := rs.Primary.Attributes["call_as"] + var accounts []string + for i := 0; i < attributeLength(rs.Primary.Attributes["accounts.#"]); i++ { + accounts = append(accounts, rs.Primary.Attributes[fmt.Sprintf("accounts.%d", i)]) + } + + var regions []string + for i := 0; i < attributeLength(rs.Primary.Attributes["regions.#"]); i++ { + regions = append(regions, rs.Primary.Attributes[fmt.Sprintf("regions.%d", i)]) + } + + deployedByOU := false + if rs.Primary.Attributes["deployment_targets.#"] != acctest.Ct0 && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != acctest.Ct0 { + deployedByOU = true + } + + output, err := tfcloudformation.FindStackInstancesByNameCallAs(ctx, acctest.Provider.Meta(), stackSetName, callAs, deployedByOU, accounts, regions) + + if tfresource.NotFound(err) { + continue + } + if output.StackSetID == "" { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("CloudFormation Stack Instances %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckStackInstancesDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudformation_stack_instances" { + continue + } + + parts, err := flex.ExpandResourceId(rs.Primary.ID, tfcloudformation.StackInstancesResourceIDPartCount, true) + if err != nil { + return err + } + + stackSetName := parts[0] + callAs := rs.Primary.Attributes["call_as"] + var accounts []string + for i := 0; i < attributeLength(rs.Primary.Attributes["accounts.#"]); i++ { + accounts = append(accounts, rs.Primary.Attributes[fmt.Sprintf("accounts.%d", i)]) + } + + var regions []string + for i := 0; i < attributeLength(rs.Primary.Attributes["regions.#"]); i++ { + regions = append(regions, rs.Primary.Attributes[fmt.Sprintf("regions.%d", i)]) + } + + deployedByOU := false + if rs.Primary.Attributes["deployment_targets.#"] != acctest.Ct0 && rs.Primary.Attributes["deployment_targets.0.organizational_unit_ids.#"] != acctest.Ct0 { + deployedByOU = true + } + + _, err = tfcloudformation.FindStackInstancesByNameCallAs(ctx, acctest.Provider.Meta(), stackSetName, callAs, deployedByOU, accounts, regions) + + if tfresource.NotFound(err) { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("CloudFormation Stack Instances %s still exists", rs.Primary.ID) + } + + return nil + } +} + +func testAccCheckStackInstancesNotRecreated(i, j *tfcloudformation.StackInstances) resource.TestCheckFunc { + return func(s *terraform.State) error { + if aws.ToString(&i.StackSetID) != aws.ToString(&j.StackSetID) { + return fmt.Errorf("CloudFormation Stack Instances (%s vs %s) recreated", i.StackSetID, j.StackSetID) + } + for _, v := range i.Summaries { + for _, w := range j.Summaries { + if aws.ToString(v.Region) != aws.ToString(w.Region) { + continue + } + if aws.ToString(v.StackId) != aws.ToString(w.StackId) { + return fmt.Errorf("CloudFormation Stack Instances (%s) recreated:\n\tregions:\n\t\t%s\n\t\t%s\n\tstack_ids:\n\t\t%s\n\t\t%s", i.StackSetID, aws.ToString(v.Region), aws.ToString(w.Region), aws.ToString(v.StackId), aws.ToString(w.StackId)) + } + } + } + + return nil + } +} + +func testAccStackInstancesBaseConfig(rName string) string { + return fmt.Sprintf(` +resource "aws_iam_role" "Administration" { + assume_role_policy = < 0 && v.([]interface{})[0] != nil { + input.OperationPreferences = expandOperationPreferences(v.([]interface{})[0].(map[string]interface{})) + } + log.Printf("[DEBUG] Deleting CloudFormation StackSet Instance: %s", d.Id()) outputRaw, err := tfresource.RetryWhenIsA[*awstypes.OperationInProgressException](ctx, d.Timeout(schema.TimeoutDelete), func() (interface{}, error) { return conn.DeleteStackInstances(ctx, input) diff --git a/internal/service/iam/wait.go b/internal/service/iam/wait.go index ef25229ecbca..3e99d4c32a1b 100644 --- a/internal/service/iam/wait.go +++ b/internal/service/iam/wait.go @@ -41,6 +41,7 @@ func waitRoleARNIsNotUniqueID(ctx context.Context, conn *iam.Client, id string, Timeout: propagationTimeout, NotFoundChecks: 10, ContinuousTargetOccurence: 5, + Delay: 10 * time.Second, } outputRaw, err := stateConf.WaitForStateContext(ctx) diff --git a/website/docs/r/cloudformation_stack_instances.html.markdown b/website/docs/r/cloudformation_stack_instances.html.markdown new file mode 100644 index 000000000000..7a8e21a59e6f --- /dev/null +++ b/website/docs/r/cloudformation_stack_instances.html.markdown @@ -0,0 +1,180 @@ +--- +subcategory: "CloudFormation" +layout: "aws" +page_title: "AWS: aws_cloudformation_stack_instances" +description: |- + Manages CloudFormation stack instances. +--- + +# Resource: aws_cloudformation_stack_instances + +Manages CloudFormation stack instances for the specified accounts, within the specified regions. A stack instance refers to a stack in a specific account and region. Additional information about stacks can be found in the [AWS CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacks.html). + +~> **NOTE:** This resource will manage all stack instances for the specified `stack_set_name`. If you create stack instances outside of Terraform or import existing infrastructure, ensure that your configuration includes all accounts and regions where stack instances exist for the stack set. Failing to include all accounts and regions will cause Terraform to continuously report differences between your configuration and the actual infrastructure. + +~> **NOTE:** All target accounts must have an IAM Role created that matches the name of the execution role configured in the stack (the `execution_role_name` argument in the `aws_cloudformation_stack_set` resource) in a trust relationship with the administrative account or administration IAM Role. The execution role must have appropriate permissions to manage resources defined in the template along with those required for stacks to operate. See the [AWS CloudFormation User Guide](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs.html) for more details. + +~> **NOTE:** To retain the Stack during Terraform resource destroy, ensure `retain_stacks = true` has been successfully applied into the Terraform state first. This must be completed _before_ an apply that would destroy the resource. + +## Example Usage + +### Basic Usage + +```terraform +resource "aws_cloudformation_stack_instances" "example" { + accounts = ["123456789012", "234567890123"] + regions = ["us-east-1", "us-west-2"] + stack_set_name = aws_cloudformation_stack_set.example.name +} +``` + +### Example IAM Setup in Target Account + +```terraform +data "aws_iam_policy_document" "AWSCloudFormationStackSetExecutionRole_assume_role_policy" { + statement { + actions = ["sts:AssumeRole"] + effect = "Allow" + + principals { + identifiers = [aws_iam_role.AWSCloudFormationStackSetAdministrationRole.arn] + type = "AWS" + } + } +} + +resource "aws_iam_role" "AWSCloudFormationStackSetExecutionRole" { + assume_role_policy = data.aws_iam_policy_document.AWSCloudFormationStackSetExecutionRole_assume_role_policy.json + name = "AWSCloudFormationStackSetExecutionRole" +} + +# Documentation: https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/stacksets-prereqs.html +# Additional IAM permissions necessary depend on the resources defined in the StackSet template +data "aws_iam_policy_document" "AWSCloudFormationStackSetExecutionRole_MinimumExecutionPolicy" { + statement { + actions = [ + "cloudformation:*", + "s3:*", + "sns:*", + ] + + effect = "Allow" + resources = ["*"] + } +} + +resource "aws_iam_role_policy" "AWSCloudFormationStackSetExecutionRole_MinimumExecutionPolicy" { + name = "MinimumExecutionPolicy" + policy = data.aws_iam_policy_document.AWSCloudFormationStackSetExecutionRole_MinimumExecutionPolicy.json + role = aws_iam_role.AWSCloudFormationStackSetExecutionRole.name +} +``` + +### Example Deployment across Organizations account + +```terraform +resource "aws_cloudformation_stack_instances" "example" { + deployment_targets { + organizational_unit_ids = [aws_organizations_organization.example.roots[0].id] + } + + regions = ["us-west-2", "us-east-1"] + stack_set_name = aws_cloudformation_stack_set.example.name +} +``` + +## Argument Reference + +The following arguments are required: + +* `stack_set_name` - (Required, Force new) Name of the stack set. + +The following arguments are optional: + +* `accounts` - (Optional) Accounts where you want to create stack instances in the specified `regions`. You can specify either `accounts` or `deployment_targets`, but not both. +* `deployment_targets` - (Optional) AWS Organizations accounts for which to create stack instances in the `regions`. stack sets 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 most of this argument. See [deployment_targets](#deployment_targets) below. +* `parameter_overrides` - (Optional) Key-value map of input parameters to override from the stack set for these instances. This argument's drift detection is limited to the first account and region since each instance can have unique parameters. +* `regions` - (Optional) Regions where you want to create stack instances in the specified `accounts`. +* `retain_stacks` - (Optional) Whether to remove the stack instances from the stack set, but not delete the stacks. You can't reassociate a retained stack or add an existing, saved stack to a new stack set. To retain the stack, ensure `retain_stacks = true` has been successfully applied _before_ an apply that would destroy the resource. Defaults to `false`. +* `call_as` - (Optional) Whether you are acting as an account administrator in the organization's management account or as a delegated administrator in a member account. Valid values: `SELF` (default), `DELEGATED_ADMIN`. +* `operation_preferences` - (Optional) Preferences for how AWS CloudFormation performs a stack set operation. See [operation_preferences](#operation_preferences) below. + +### `deployment_targets` + +The `deployment_targets` configuration block supports the following arguments: + +* `account_filter_type` - (Optional, Force new) 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. +* `organizational_unit_ids` - (Optional) Organization root ID or organizational unit (OU) IDs to which stack sets deploy. + +### `operation_preferences` + +The `operation_preferences` configuration block supports the following arguments: + +* `concurrency_mode` - (Optional) How the concurrency level behaves during the operation execution. Valid values are `STRICT_FAILURE_TOLERANCE` and `SOFT_FAILURE_TOLERANCE`. +* `failure_tolerance_count` - (Optional) Number of accounts, per region, for which this operation can fail before CloudFormation stops the operation in that region. +* `failure_tolerance_percentage` - (Optional) Percentage of accounts, per region, for which this stack operation can fail before 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 stack sets 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 where you want to perform the stack operation. + +## Attribute Reference + +This resource exports the following attributes in addition to the arguments above: + +* `stack_instance_summaries` - List of stack instances created from an organizational unit deployment target. This may not always be set depending on whether CloudFormation returns summaries for your configuration. See [`stack_instance_summaries`](#stack_instance_summaries-attribute-reference). +* `stack_set_id` - Unique identifier of the stack set. + +### `stack_instance_summaries` + +* `account_id` - Account ID in which the instance is deployed. +* `detailed_status` - Detailed status of the stack instance. Values include `PENDING`, `RUNNING`, `SUCCEEDED`, `FAILED`, `CANCELLED`, `INOPERABLE`, `SKIPPED_SUSPENDED_ACCOUNT`, `FAILED_IMPORT`. +* `drift_status` - Status of the stack instance's actual configuration compared to the expected template and parameter configuration of the stack set to which it belongs. Values include `DRIFTED`, `IN_SYNC`, `UNKNOWN`, `NOT_CHECKED`. +* `organizational_unit_id` - Organization root ID or organizational unit (OU) IDs that you specified for `deployment_targets`. +* `region` - Region that the stack instance is associated with. +* `stack_id` - ID of the stack instance. +* `stack_set_id` - Name or unique ID of the stack set that the stack instance is associated with. +* `status` - Status of the stack instance, in terms of its synchronization with its associated stack set. Values include `CURRENT`, `OUTDATED`, `INOPERABLE`. +* `status_reason` - Explanation for the specific status code assigned to this stack instance. + +## Timeouts + +[Configuration options](https://developer.hashicorp.com/terraform/language/resources/syntax#operation-timeouts): + +* `create` - (Default `30m`) +* `update` - (Default `30m`) +* `delete` - (Default `30m`) + +## Import + +In Terraform v1.5.0 and later, use an [`import` block](https://developer.hashicorp.com/terraform/language/import) to import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). If you are importing a stack instance targeting OUs, see the example below. For example: + +```terraform +import { + to = aws_cloudformation_stack_instances.example + id = "example,SELF" +} +``` + +Import CloudFormation stack instances that target OUs, using the stack set name, `call_as`, and "OU" separated by commas (`,`). For example: + +```terraform +import { + to = aws_cloudformation_stack_instances.example + id = "example,SELF,OU" +} +``` + +Using `terraform import`, import CloudFormation stack instances using the stack set name and `call_as` separated by commas (`,`). If you are importing a stack instance targeting OUs, see the example below. For example: + +```console +% terraform import aws_cloudformation_stack_instances.example example,SELF +``` + +Using `terraform import`, Import CloudFormation stack instances that target OUs, using the stack set name, `call_as`, and "OU" separated by commas (`,`). For example: + +```console +% terraform import aws_cloudformation_stack_instances.example example,SELF,OU +```