diff --git a/.changelog/24523.txt b/.changelog/24523.txt new file mode 100644 index 00000000000..964f3ccc17e --- /dev/null +++ b/.changelog/24523.txt @@ -0,0 +1,11 @@ +```release-note:bug +resource/aws_cloudformation_stack_set_instance: Fix error when deploying to organizational units with no accounts. +``` + +```release-note:enhancement +resource/aws_cloudformation_stack_set_instance: Changes to `deployment_targets` now force a new resource. +``` + +```release-note:enhancement +resource/aws_cloudformation_stack_set_instance: Added the `stack_instance_summaries` attribute to track all account and stack IDs for deployments to organizational units. +``` diff --git a/internal/service/cloudformation/find.go b/internal/service/cloudformation/find.go index 78e680353bc..29e0cdf7a12 100644 --- a/internal/service/cloudformation/find.go +++ b/internal/service/cloudformation/find.go @@ -77,7 +77,7 @@ func FindStackByID(ctx context.Context, conn *cloudformation.CloudFormation, id return stack, nil } -func FindStackInstanceAccountIdByOrgIDs(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName, region, callAs string, orgIDs []string) (string, error) { +func FindStackInstanceSummariesByOrgIDs(ctx context.Context, conn *cloudformation.CloudFormation, stackSetName, region, callAs string, orgIDs []string) ([]*cloudformation.StackInstanceSummary, error) { input := &cloudformation.ListStackInstancesInput{ StackInstanceRegion: aws.String(region), StackSetName: aws.String(stackSetName), @@ -87,7 +87,7 @@ func FindStackInstanceAccountIdByOrgIDs(ctx context.Context, conn *cloudformatio input.CallAs = aws.String(callAs) } - var result string + var result []*cloudformation.StackInstanceSummary err := conn.ListStackInstancesPagesWithContext(ctx, input, func(page *cloudformation.ListStackInstancesOutput, lastPage bool) bool { if page == nil { @@ -101,8 +101,7 @@ func FindStackInstanceAccountIdByOrgIDs(ctx context.Context, conn *cloudformatio for _, orgID := range orgIDs { if aws.StringValue(s.OrganizationalUnitId) == orgID { - result = aws.StringValue(s.Account) - return false + result = append(result, s) } } } @@ -111,14 +110,14 @@ func FindStackInstanceAccountIdByOrgIDs(ctx context.Context, conn *cloudformatio }) if tfawserr.ErrCodeEquals(err, cloudformation.ErrCodeStackSetNotFoundException) { - return "", &retry.NotFoundError{ + return nil, &retry.NotFoundError{ LastError: err, LastRequest: input, } } if err != nil { - return "", err + return nil, err } return result, nil diff --git a/internal/service/cloudformation/stack_set_instance.go b/internal/service/cloudformation/stack_set_instance.go index 0278898c2e1..aacf18ad930 100644 --- a/internal/service/cloudformation/stack_set_instance.go +++ b/internal/service/cloudformation/stack_set_instance.go @@ -60,6 +60,7 @@ func ResourceStackSetInstance() *schema.Resource { "deployment_targets": { Type: schema.TypeList, Optional: true, + ForceNew: true, MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ @@ -147,6 +148,28 @@ func ResourceStackSetInstance() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "stack_instance_summaries": { + 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{ + "account_id": { + Type: schema.TypeString, + Computed: true, + }, + "organizational_unit_id": { + Type: schema.TypeString, + Computed: true, + }, + "stack_id": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, "stack_set_name": { Type: schema.TypeString, Required: true, @@ -157,6 +180,10 @@ func ResourceStackSetInstance() *schema.Resource { } } +var ( + accountIDRegexp = regexp.MustCompile(`^\d{12}$`) +) + func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var diags diag.Diagnostics conn := meta.(*conns.AWSClient).CloudFormationConn(ctx) @@ -176,22 +203,25 @@ func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData, if v, ok := d.GetOk("account_id"); ok { accountID = v.(string) } - - callAs := d.Get("call_as").(string) - if v, ok := d.GetOk("call_as"); ok { - input.CallAs = aws.String(v.(string)) - } + // accountOrOrgID will either be account_id or a slash-delimited list of + // organizational_unit_id's from the deployment_targets argument. This + // is composed with stack_set_name and region to form the resources ID. + accountOrOrgID := accountID if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { dt := expandDeploymentTargets(v.([]interface{})) - // temporarily set the accountId to the DeploymentTarget IDs - // to later inform the Read CRUD operation if the true accountID needs to be determined - accountID = strings.Join(aws.StringValueSlice(dt.OrganizationalUnitIds), "/") + accountOrOrgID = strings.Join(aws.StringValueSlice(dt.OrganizationalUnitIds), "/") input.DeploymentTargets = dt } else { + d.Set("account_id", accountID) input.Accounts = aws.StringSlice([]string{accountID}) } + callAs := d.Get("call_as").(string) + if v, ok := d.GetOk("call_as"); ok { + input.CallAs = aws.String(v.(string)) + } + if v, ok := d.GetOk("parameter_overrides"); ok { input.ParameterOverrides = expandParameters(v.(map[string]interface{})) } @@ -211,7 +241,7 @@ func resourceStackSetInstanceCreate(ctx context.Context, d *schema.ResourceData, return nil, err } - d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountID, region)) + d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountOrOrgID, region)) operation, err := WaitStackSetOperationSucceeded(ctx, conn, stackSetName, aws.StringValue(output.OperationId), callAs, d.Timeout(schema.TimeoutCreate)) if err != nil { @@ -269,50 +299,56 @@ func resourceStackSetInstanceRead(ctx context.Context, d *schema.ResourceData, m var diags diag.Diagnostics conn := meta.(*conns.AWSClient).CloudFormationConn(ctx) - stackSetName, accountID, region, err := StackSetInstanceParseResourceID(d.Id()) - - callAs := d.Get("call_as").(string) - + stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %s", d.Id(), err) } + if accountOrOrgID == "" { + return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): account_id or organizational_unit_id section empty", d.Id()) + } + d.Set("region", region) + d.Set("stack_set_name", stackSetName) - // Determine correct account ID for the Instance if created with deployment targets; - // we only expect the accountID to be the organization root ID or organizational unit (OU) IDs - // separated by a slash after creation. - if regexp.MustCompile(`(ou-[a-z0-9]{4,32}-[a-z0-9]{8,32}|r-[a-z0-9]{4,32})`).MatchString(accountID) { - orgIDs := strings.Split(accountID, "/") - accountID, err = FindStackInstanceAccountIdByOrgIDs(ctx, conn, stackSetName, region, callAs, orgIDs) + callAs := d.Get("call_as").(string) + if accountIDRegexp.MatchString(accountOrOrgID) { + // Stack instances deployed by account ID + stackInstance, err := FindStackInstanceByName(ctx, conn, stackSetName, accountOrOrgID, region, callAs) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } if err != nil { - return sdkdiag.AppendErrorf(diags, "finding CloudFormation StackSet Instance (%s) Account: %s", d.Id(), err) + return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %s", d.Id(), err) } - d.SetId(StackSetInstanceCreateResourceID(stackSetName, accountID, region)) - } - - stackInstance, err := FindStackInstanceByName(ctx, conn, stackSetName, accountID, region, callAs) - - if !d.IsNewResource() && tfresource.NotFound(err) { - log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id()) - d.SetId("") - return diags - } + d.Set("account_id", stackInstance.Account) + d.Set("organizational_unit_id", stackInstance.OrganizationalUnitId) + if err := d.Set("parameter_overrides", flattenAllParameters(stackInstance.ParameterOverrides)); err != nil { + return sdkdiag.AppendErrorf(diags, "setting parameters: %s", err) + } - if err != nil { - return sdkdiag.AppendErrorf(diags, "reading CloudFormation StackSet Instance (%s): %s", d.Id(), err) - } + d.Set("stack_id", stackInstance.StackId) + d.Set("stack_instance_summaries", nil) + } else { + // Stack instances deployed by organizational unit ID + orgIDs := strings.Split(accountOrOrgID, "/") + + summaries, err := FindStackInstanceSummariesByOrgIDs(ctx, conn, stackSetName, region, callAs, orgIDs) + if !d.IsNewResource() && tfresource.NotFound(err) { + log.Printf("[WARN] CloudFormation StackSet Instance (%s) not found, removing from state", d.Id()) + d.SetId("") + return diags + } + if err != nil { + return sdkdiag.AppendErrorf(diags, "finding CloudFormation StackSet Instance (%s) Account: %s", d.Id(), err) + } - d.Set("account_id", stackInstance.Account) - d.Set("organizational_unit_id", stackInstance.OrganizationalUnitId) - if err := d.Set("parameter_overrides", flattenAllParameters(stackInstance.ParameterOverrides)); err != nil { - return sdkdiag.AppendErrorf(diags, "setting parameters: %s", err) + d.Set("deployment_targets", flattenDeploymentTargetsFromSlice(orgIDs)) + d.Set("stack_instance_summaries", flattenStackInstanceSummaries(summaries)) } - d.Set("region", stackInstance.Region) - d.Set("stack_id", stackInstance.StackId) - d.Set("stack_set_name", stackSetName) - return diags } @@ -321,14 +357,14 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData, conn := meta.(*conns.AWSClient).CloudFormationConn(ctx) if d.HasChanges("deployment_targets", "parameter_overrides", "operation_preferences") { - stackSetName, accountID, region, err := StackSetInstanceParseResourceID(d.Id()) + stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendErrorf(diags, "updating CloudFormation StackSet Instance (%s): %s", d.Id(), err) } input := &cloudformation.UpdateStackInstancesInput{ - Accounts: aws.StringSlice([]string{accountID}), + Accounts: aws.StringSlice([]string{accountOrOrgID}), OperationId: aws.String(id.UniqueId()), ParameterOverrides: []*cloudformation.Parameter{}, Regions: aws.StringSlice([]string{region}), @@ -341,9 +377,10 @@ func resourceStackSetInstanceUpdate(ctx context.Context, d *schema.ResourceData, } 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 = expandDeploymentTargets(v.([]interface{})) + input.DeploymentTargets = dt } if v, ok := d.GetOk("parameter_overrides"); ok { @@ -373,14 +410,14 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData, var diags diag.Diagnostics conn := meta.(*conns.AWSClient).CloudFormationConn(ctx) - stackSetName, accountID, region, err := StackSetInstanceParseResourceID(d.Id()) + stackSetName, accountOrOrgID, region, err := StackSetInstanceParseResourceID(d.Id()) if err != nil { return sdkdiag.AppendErrorf(diags, "deleting CloudFormation StackSet Instance (%s): %s", d.Id(), err) } input := &cloudformation.DeleteStackInstancesInput{ - Accounts: aws.StringSlice([]string{accountID}), + Accounts: aws.StringSlice([]string{accountOrOrgID}), OperationId: aws.String(id.UniqueId()), Regions: aws.StringSlice([]string{region}), RetainStacks: aws.Bool(d.Get("retain_stack").(bool)), @@ -392,13 +429,12 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData, input.CallAs = aws.String(v.(string)) } - if v, ok := d.GetOk("organizational_unit_id"); ok { + if v, ok := d.GetOk("deployment_targets"); ok && len(v.([]interface{})) > 0 && v.([]interface{})[0] != nil { + dt := expandDeploymentTargets(v.([]interface{})) // For instances associated with stack sets that use a self-managed permission model, // the organizational unit must be provided; input.Accounts = nil - input.DeploymentTargets = &cloudformation.DeploymentTargets{ - OrganizationalUnitIds: aws.StringSlice([]string{v.(string)}), - } + input.DeploymentTargets = dt } log.Printf("[DEBUG] Deleting CloudFormation StackSet Instance: %s", d.Id()) @@ -419,22 +455,54 @@ func resourceStackSetInstanceDelete(ctx context.Context, d *schema.ResourceData, return diags } -func expandDeploymentTargets(l []interface{}) *cloudformation.DeploymentTargets { - if len(l) == 0 || l[0] == nil { +func expandDeploymentTargets(tfList []interface{}) *cloudformation.DeploymentTargets { + if len(tfList) == 0 || tfList[0] == nil { return nil } - tfMap, ok := l[0].(map[string]interface{}) - + tfMap, ok := tfList[0].(map[string]interface{}) if !ok { return nil } dt := &cloudformation.DeploymentTargets{} - if v, ok := tfMap["organizational_unit_ids"].(*schema.Set); ok && v.Len() > 0 { dt.OrganizationalUnitIds = flex.ExpandStringSet(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) + } + + m := map[string]interface{}{ + "organizational_unit_ids": tfList, + } + + return []interface{}{m} +} + +func flattenStackInstanceSummaries(apiObject []*cloudformation.StackInstanceSummary) []interface{} { + if len(apiObject) == 0 { + return nil + } + + tfList := []interface{}{} + for _, obj := range apiObject { + m := map[string]interface{}{ + "account_id": obj.Account, + "organizational_unit_id": obj.OrganizationalUnitId, + "stack_id": obj.StackId, + } + tfList = append(tfList, m) + } + + return tfList +} diff --git a/internal/service/cloudformation/stack_set_instance_test.go b/internal/service/cloudformation/stack_set_instance_test.go index aa75120eea7..0d142dd6aee 100644 --- a/internal/service/cloudformation/stack_set_instance_test.go +++ b/internal/service/cloudformation/stack_set_instance_test.go @@ -6,6 +6,7 @@ package cloudformation_test import ( "context" "fmt" + "strings" "testing" "github.com/aws/aws-sdk-go/aws" @@ -235,7 +236,7 @@ func TestAccCloudFormationStackSetInstance_retainStack(t *testing.T) { func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) { ctx := acctest.Context(t) - var stackInstance cloudformation.StackInstance + var stackInstanceSummaries []*cloudformation.StackInstanceSummary rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_cloudformation_stack_set_instance.test" @@ -249,12 +250,57 @@ func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, cloudformation.EndpointsID, "organizations"), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckStackSetInstanceDestroy(ctx), + CheckDestroy: testAccCheckStackSetInstanceForOrganizationalUnitDestroy(ctx), Steps: []resource.TestStep{ { Config: testAccStackSetInstanceConfig_deploymentTargets(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckStackSetInstanceExists(ctx, resourceName, &stackInstance), + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", "1"), + resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.organizational_unit_ids.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{ + "retain_stack", + "call_as", + }, + }, + { + Config: testAccStackSetInstanceConfig_deploymentTargets(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), + ), + }, + }, + }) +} + +func TestAccCloudFormationStackSetInstance_DeploymentTargets_emptyOU(t *testing.T) { + ctx := acctest.Context(t) + var stackInstanceSummaries []*cloudformation.StackInstanceSummary + rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) + resourceName := "aws_cloudformation_stack_set_instance.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, cloudformation.EndpointsID, "organizations"), + ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, + CheckDestroy: testAccCheckStackSetInstanceForOrganizationalUnitDestroy(ctx), + Steps: []resource.TestStep{ + { + Config: testAccStackSetInstanceConfig_DeploymentTargets_emptyOU(rName), + Check: resource.ComposeTestCheckFunc( + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), resource.TestCheckResourceAttr(resourceName, "deployment_targets.#", "1"), resource.TestCheckResourceAttr(resourceName, "deployment_targets.0.organizational_unit_ids.#", "1"), ), @@ -265,14 +311,13 @@ func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) { ImportStateVerify: true, ImportStateVerifyIgnore: []string{ "retain_stack", - "deployment_targets", "call_as", }, }, { - Config: testAccStackSetInstanceConfig_serviceManaged(rName), + Config: testAccStackSetInstanceConfig_DeploymentTargets_emptyOU(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckStackSetInstanceExists(ctx, resourceName, &stackInstance), + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), ), }, }, @@ -281,7 +326,7 @@ func TestAccCloudFormationStackSetInstance_deploymentTargets(t *testing.T) { func TestAccCloudFormationStackSetInstance_operationPreferences(t *testing.T) { ctx := acctest.Context(t) - var stackInstance cloudformation.StackInstance + var stackInstanceSummaries []*cloudformation.StackInstanceSummary rName := sdkacctest.RandomWithPrefix(acctest.ResourcePrefix) resourceName := "aws_cloudformation_stack_set_instance.test" @@ -295,12 +340,12 @@ func TestAccCloudFormationStackSetInstance_operationPreferences(t *testing.T) { }, ErrorCheck: acctest.ErrorCheck(t, cloudformation.EndpointsID), ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories, - CheckDestroy: testAccCheckStackSetInstanceDestroy(ctx), + CheckDestroy: testAccCheckStackSetInstanceForOrganizationalUnitDestroy(ctx), Steps: []resource.TestStep{ { Config: testAccStackSetInstanceConfig_operationPreferences(rName), Check: resource.ComposeTestCheckFunc( - testAccCheckStackSetInstanceExists(ctx, resourceName, &stackInstance), + testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx, resourceName, stackInstanceSummaries), resource.TestCheckResourceAttr(resourceName, "operation_preferences.#", "1"), resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_count", "1"), resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.failure_tolerance_percentage", "0"), @@ -309,12 +354,6 @@ func TestAccCloudFormationStackSetInstance_operationPreferences(t *testing.T) { resource.TestCheckResourceAttr(resourceName, "operation_preferences.0.region_concurrency_type", ""), ), }, - { - Config: testAccStackSetInstanceConfig_serviceManaged(rName), - Check: resource.ComposeTestCheckFunc( - testAccCheckStackSetInstanceExists(ctx, resourceName, &stackInstance), - ), - }, }, }) } @@ -348,6 +387,78 @@ func testAccCheckStackSetInstanceExists(ctx context.Context, resourceName string } } +// testAccCheckStackSetInstanceForOrganizationalUnitExists is a variant of the +// standard CheckExistsFunc which expects the resource ID to contain organizational +// unit IDs rather than an account ID +func testAccCheckStackSetInstanceForOrganizationalUnitExists(ctx context.Context, resourceName string, v []*cloudformation.StackInstanceSummary) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + callAs := rs.Primary.Attributes["call_as"] + + conn := acctest.Provider.Meta().(*conns.AWSClient).CloudFormationConn(ctx) + + stackSetName, accountOrOrgID, region, err := tfcloudformation.StackSetInstanceParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + orgIDs := strings.Split(accountOrOrgID, "/") + + output, err := tfcloudformation.FindStackInstanceSummariesByOrgIDs(ctx, conn, stackSetName, region, callAs, orgIDs) + + if err != nil { + return err + } + + v = output + + return nil + } +} + +// testAccCheckStackSetInstanceForOrganizationalUnitDestroy is a variant of the +// standard CheckDestroyFunc which expects the resource ID to contain organizational +// unit IDs rather than an account ID +func testAccCheckStackSetInstanceForOrganizationalUnitDestroy(ctx context.Context) resource.TestCheckFunc { + return func(s *terraform.State) error { + conn := acctest.Provider.Meta().(*conns.AWSClient).CloudFormationConn(ctx) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "aws_cloudformation_stack_set_instance" { + continue + } + + callAs := rs.Primary.Attributes["call_as"] + + stackSetName, accountOrOrgID, region, err := tfcloudformation.StackSetInstanceParseResourceID(rs.Primary.ID) + if err != nil { + return err + } + orgIDs := strings.Split(accountOrOrgID, "/") + + output, err := tfcloudformation.FindStackInstanceSummariesByOrgIDs(ctx, conn, stackSetName, region, callAs, orgIDs) + + if tfresource.NotFound(err) { + continue + } + if len(output) == 0 { + continue + } + + if err != nil { + return err + } + + return fmt.Errorf("CloudFormation StackSet Instance %s still exists", rs.Primary.ID) + } + + return nil + } +} + func testAccCheckStackSetInstanceStackExists(ctx context.Context, stackInstance *cloudformation.StackInstance, v *cloudformation.Stack) resource.TestCheckFunc { return func(s *terraform.State) error { conn := acctest.Provider.Meta().(*conns.AWSClient).CloudFormationConn(ctx) @@ -718,14 +829,23 @@ resource "aws_cloudformation_stack_set_instance" "test" { `) } -func testAccStackSetInstanceConfig_serviceManaged(rName string) string { - return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName), ` +func testAccStackSetInstanceConfig_DeploymentTargets_emptyOU(rName string) string { + return acctest.ConfigCompose(testAccStackSetInstanceBaseConfig_ServiceManagedStackSet(rName), fmt.Sprintf(` +resource "aws_organizations_organizational_unit" "test" { + name = %[1]q + parent_id = data.aws_organizations_organization.test.roots[0].id +} + resource "aws_cloudformation_stack_set_instance" "test" { depends_on = [aws_iam_role_policy.Administration, aws_iam_role_policy.Execution] + deployment_targets { + organizational_unit_ids = [aws_organizations_organizational_unit.test.id] + } + stack_set_name = aws_cloudformation_stack_set.test.name } -`) +`, rName)) } func testAccStackSetInstanceConfig_operationPreferences(rName string) string { diff --git a/website/docs/r/cloudformation_stack_set_instance.html.markdown b/website/docs/r/cloudformation_stack_set_instance.html.markdown index 18956f5be55..c835d6c6d0b 100644 --- a/website/docs/r/cloudformation_stack_set_instance.html.markdown +++ b/website/docs/r/cloudformation_stack_set_instance.html.markdown @@ -16,6 +16,8 @@ Manages a CloudFormation StackSet Instance. Instances are managed in the account ## Example Usage +### Basic Usage + ```terraform resource "aws_cloudformation_stack_set_instance" "example" { account_id = "123456789012" @@ -113,9 +115,16 @@ The `operation_preferences` configuration block supports the following arguments This resource exports the following attributes in addition to the arguments above: -* `id` - StackSet name, target AWS account ID, and target AWS region separated by commas (`,`) -* `organizational_unit_id` - The organization root ID or organizational unit (OU) IDs specified for `deployment_targets`. -* `stack_id` - Stack identifier +* `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. +* `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). + +### `stack_instance_summaries` Attribute Reference + +* `account_id` - AWS account ID in which the stack is deployed. +* `organizational_unit_id` - Organizational unit ID in which the stack is deployed. +* `stack_id` - Stack identifier. ## Timeouts @@ -127,13 +136,13 @@ This resource exports the following attributes in addition to the arguments abov ## Import -Import CloudFormation StackSet Instances that target an AWS Account ID using the StackSet name, target AWS account ID, and target AWS region separated by commas (`,`). For example: +Import CloudFormation StackSet Instances that target an AWS account using the stack set name, AWS account ID, and region separated by commas (`,`). For example: ``` $ terraform import aws_cloudformation_stack_set_instance.example example,123456789012,us-east-1 ``` -Import CloudFormation StackSet Instances that target AWS Organizational Units using the StackSet name, a slash (`/`) separated list of organizational unit IDs, and target AWS region separated by commas (`,`). For example: +Import CloudFormation StackSet Instances that target AWS organizational units using the stack set name, a slash (`/`) separated list of organizational unit IDs, and region separated by commas (`,`). For example: ``` $ terraform import aws_cloudformation_stack_set_instance.example example,ou-sdas-123123123/ou-sdas-789789789,us-east-1