Skip to content

CloudFormation: update_stack_instances aborts the whole batch instead of processing each account/region independently #10173

Description

@bellmatt

Environment

  • moto 5.2.2
  • boto3 1.43.63

Description

Following up on #10035 / #10041, thanks for adding StackInstanceNotFoundException, note this issue builds upon that, there is no regression.

StackInstances.update() now raises StackInstanceNotFound as soon as it hits the first requested (account, region) target that doesn't have an existing instance:

def update(self, accounts, regions, parameters):
    for account in accounts:
        for region in regions:
            instance = self.get_instance(account, region)
            if instance is None:
                raise StackInstanceNotFound()
            instance.parameters = parameters or []

StackInstanceNotFoundException is a documented error for this API (UpdateStackInstances reference), so raising it isn't wrong in isolation. The issue is that raising immediately abandons the rest of the batch, so any targets ordered after the missing one in the Accounts/Regions lists never get updated, even though they do have an existing instance and should have been. The result depends entirely on request-list ordering, which isn't how AWS documents this operation behaving.

See StackSet operation options:

If a stack can't update in the first Region, the update operation continues in that Region, and then moves on to the next Region.

And the documented per-instance status codes (SUCCEEDED, FAILED, PENDING, etc.) model each account/Region's outcome independently within a single operation, not as an all-or-nothing, order-dependent result.

Reproduction

import boto3
from moto import mock_aws

@mock_aws
def repro():
    c = boto3.client("cloudformation", region_name="eu-west-1")
    c.create_stack_set(
        StackSetName="demo",
        TemplateBody='{"Resources": {}}',
        PermissionModel="SELF_MANAGED",
        Parameters=[{"ParameterKey": "Foo", "ParameterValue": "original"}],
    )
    # Only accountA (111...) has an instance; accountC (333...) does not.
    c.create_stack_instances(StackSetName="demo", Accounts=["111111111111"], Regions=["eu-west-1"])

    try:
        # accountC is ordered before accountA.
        c.update_stack_instances(
            StackSetName="demo",
            Accounts=["333333333333", "111111111111"],
            Regions=["eu-west-1"],
            ParameterOverrides=[{"ParameterKey": "Foo", "ParameterValue": "updated"}],
        )
    except Exception as e:
        print("raised:", type(e).__name__, e)

    for i in c.list_stack_instances(StackSetName="demo")["Summaries"]:
        print(i["Account"], i["Region"], i.get("ParameterOverrides"))

repro()

Actual behaviour

raised: StackInstanceNotFoundException An error occurred (StackInstanceNotFoundException) when calling the UpdateStackInstances operation: The specified stack instance doesn't exist.
111111111111 eu-west-1 None

accountA's existing instance was never updated (ParameterOverrides stayed None), purely because accountC happened to be listed first.

Expected behaviour

accountA's instance should be updated regardless of the position of accountC in the request, and regardless of whether accountC has an instance yet. StackInstanceNotFoundException seems like the right error for the operation as a whole to surface, but it shouldn't come at the cost of silently dropping updates to valid, existing targets elsewhere in the same request.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions