Skip to content

Commit 5e01dff

Browse files
Apply Capacity Reservation tenancy to AWS instance launch (#3992)
A Capacity Reservation created with non-default tenancy (e.g. `dedicated`) only accepts instances launched with a matching `Placement.Tenancy`. Previously dstack always launched with the implicit `default` tenancy, so runs targeting a dedicated-tenancy reservation failed to match it. Read `Tenancy` from the reservation dstack already queries in `create_instance` and pass it through to `create_instances_struct`, which sets `Placement.Tenancy` when it is non-default. No user-facing config; it is detected automatically from the reservation and merges cleanly with the placement-group `Placement` entry. Assisted-by: Claude
1 parent 412a58b commit 5e01dff

3 files changed

Lines changed: 48 additions & 0 deletions

File tree

src/dstack/_internal/core/backends/aws/compute.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,7 @@ def create_instance(
277277
)
278278
enable_efa = max_efa_interfaces > 0
279279
is_capacity_block = False
280+
reservation_tenancy = None
280281
try:
281282
vpc_id, subnets_ids = self._get_vpc_id_subnets_ids_or_error(
282283
ec2_client=ec2_client,
@@ -297,6 +298,7 @@ def create_instance(
297298
instance_count=1,
298299
)
299300
if reservation is not None:
301+
reservation_tenancy = reservation.get("Tenancy")
300302
# Filter out az different from capacity reservation
301303
subnet_id_to_az_map = {
302304
k: v
@@ -355,6 +357,7 @@ def create_instance(
355357
max_efa_interfaces=max_efa_interfaces,
356358
reservation_id=instance_config.reservation,
357359
is_capacity_block=is_capacity_block,
360+
tenancy=reservation_tenancy,
358361
)
359362
)
360363
except botocore.exceptions.ClientError as e:

src/dstack/_internal/core/backends/aws/resources.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ def create_instances_struct(
152152
max_efa_interfaces: int = 0,
153153
reservation_id: Optional[str] = None,
154154
is_capacity_block: bool = False,
155+
tenancy: Optional[str] = None,
155156
) -> Dict[str, Any]:
156157
struct: Dict[str, Any] = dict(
157158
BlockDeviceMappings=[
@@ -213,6 +214,12 @@ def create_instances_struct(
213214
"CapacityReservationTarget": {"CapacityReservationId": reservation_id}
214215
}
215216

217+
# A Capacity Reservation created with non-default tenancy (e.g. `dedicated`) only
218+
# accepts instances launched with a matching `Placement.Tenancy`. Apply it
219+
# automatically so users don't have to configure tenancy explicitly.
220+
if tenancy is not None and tenancy != "default":
221+
struct.setdefault("Placement", {})["Tenancy"] = tenancy
222+
216223
return struct
217224

218225

src/tests/_internal/core/backends/aws/test_resources.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
_create_network_interfaces_struct,
99
_is_valid_tag_key,
1010
_is_valid_tag_value,
11+
create_instances_struct,
1112
get_image_id_and_username,
1213
validate_tags,
1314
)
@@ -238,6 +239,43 @@ def test_raises_resource_not_found_if_image_config_property_not_set(
238239
assert "cpu image not configured" in caplog.text
239240

240241

242+
class TestCreateInstancesStruct:
243+
def _struct(self, **kwargs):
244+
return create_instances_struct(
245+
disk_size=100,
246+
image_id="ami-1",
247+
instance_type="m5.large",
248+
iam_instance_profile=None,
249+
user_data="",
250+
tags=[],
251+
security_group_id="sg-1",
252+
spot=False,
253+
**kwargs,
254+
)
255+
256+
def test_no_tenancy_by_default(self):
257+
struct = self._struct(reservation_id="cr-1")
258+
assert "Placement" not in struct
259+
260+
def test_default_tenancy_not_set(self):
261+
# `default` is the implicit AWS behavior, so it should not be sent explicitly
262+
struct = self._struct(reservation_id="cr-1", tenancy="default")
263+
assert "Placement" not in struct
264+
265+
def test_dedicated_tenancy_applied(self):
266+
struct = self._struct(reservation_id="cr-1", tenancy="dedicated")
267+
assert struct["Placement"]["Tenancy"] == "dedicated"
268+
269+
def test_tenancy_merged_with_placement_group(self):
270+
struct = self._struct(
271+
reservation_id="cr-1",
272+
tenancy="dedicated",
273+
placement_group_name="pg-1",
274+
)
275+
assert struct["Placement"]["GroupName"] == "pg-1"
276+
assert struct["Placement"]["Tenancy"] == "dedicated"
277+
278+
241279
class TestCreateNetworkInterfacesStruct:
242280
def test_non_efa_instance_single_interface(self):
243281
interfaces = _create_network_interfaces_struct(

0 commit comments

Comments
 (0)