diff --git a/moto/ec2/models/fleets.py b/moto/ec2/models/fleets.py index 1fd2accfe45d..8afbdd5590da 100644 --- a/moto/ec2/models/fleets.py +++ b/moto/ec2/models/fleets.py @@ -171,31 +171,31 @@ def instances(self) -> Optional[list[dict[str, Any]]]: """ Return instances for instant fleets, None for other fleet types. This is part of the CreateFleet response for instant fleets only. + AWS groups instances that share the same launch spec and lifecycle into + a single entry with aggregated InstanceIds. """ if self.fleet_type != "instant": return None - instances = [] - - # Process on-demand instances - for item in self.on_demand_instances: - instance_data = self._build_instance_data( - instance=item["instance"], - lifecycle="on-demand", - launch_spec=item.get("launch_spec"), - ) - instances.append(instance_data) - - # Process spot instances - for spot_request in self.spot_requests: - instance_data = self._build_instance_data( - instance=spot_request.instance, - lifecycle="spot", - launch_spec=spot_request.launch_spec, - ) - instances.append(instance_data) + # Flatten all launched instances into (lifecycle, launch_spec, instance) tuples. + # SpotFleetLaunchSpec is hashable and compares by content, so identical configs + # collapse into one group entry, matching AWS behaviour. + all_items: list[tuple[str, Optional[SpotFleetLaunchSpec], Any]] = [ + ("on-demand", item.get("launch_spec"), item["instance"]) + for item in self.on_demand_instances + ] + [("spot", req.launch_spec, req.instance) for req in self.spot_requests] + + groups: dict[tuple[str, Optional[SpotFleetLaunchSpec]], dict[str, Any]] = {} + for lifecycle, launch_spec, instance in all_items: + key = (lifecycle, launch_spec) + if key not in groups: + groups[key] = self._build_instance_data( + instance, lifecycle, launch_spec + ) + else: + groups[key]["InstanceIds"].append(instance.id) - return instances + return list(groups.values()) @staticmethod def _build_instance_data( diff --git a/tests/test_ec2/test_fleets.py b/tests/test_ec2/test_fleets.py index 914993aaff05..fd4bc3d77870 100644 --- a/tests/test_ec2/test_fleets.py +++ b/tests/test_ec2/test_fleets.py @@ -833,18 +833,25 @@ def test_create_fleet_api(): assert fleet_res["FleetId"].startswith("fleet-") is True assert "Instances" in fleet_res - assert len(fleet_res["Instances"]) == 3 - instance_ids = [i["InstanceIds"] for i in fleet_res["Instances"]] - for instance_id in instance_ids: - assert instance_id[0].startswith("i-") is True + instances = fleet_res["Instances"] - instance_types = [i["InstanceType"] for i in fleet_res["Instances"]] - assert instance_types == ["t2.micro", "t2.micro", "t2.micro"] + # AWS groups instances with the same config (lifecycle/template) into one entry; + # with 1 on-demand + 2 spot we expect 2 groups + assert len(instances) == 2 + + # Total instance IDs across all groups should equal the requested capacity + all_instance_ids = [iid for i in instances for iid in i["InstanceIds"]] + assert len(all_instance_ids) == 3 + for instance_id in all_instance_ids: + assert instance_id.startswith("i-") is True + + all_instance_types = {i["InstanceType"] for i in instances} + assert all_instance_types == {"t2.micro"} - lifecycle = [i["Lifecycle"] for i in fleet_res["Instances"]] - assert "spot" in lifecycle - assert "on-demand" in lifecycle + lifecycles = {i["Lifecycle"] for i in instances} + assert "spot" in lifecycles + assert "on-demand" in lifecycles @mock_aws