Skip to content

Commit 3be52da

Browse files
committed
Support multi-region custom security groups/NSGs
security_group_id / network_security_group / network_security_group_id were flat single-value fields, but security groups and NSGs are scoped per VPC/region, so they only worked for single-region backends. Fixed to mirror the existing vpc_ids/subnet_ids/subnet_ids_per_region conventions: - AWS: security_group_name (a name that must exist in every region's VPC — AWS allows reusing the same group name across regions) and security_group_ids (an explicit region -> ID map for when names differ). - Azure: network_security_group_ids (location -> NSG name map). Azure NSG names are unique per resource group regardless of location, so a single name can never cover more than one region. - OCI: network_security_group_ids (region -> NSG OCID map). Regions/locations not covered by the mapping fall back to dstack's auto-created security group, so partial custom-SG adoption across regions works too. GCP is unaffected (its firewall rules are VPC-wide, not per-region).
1 parent 5caf274 commit 3be52da

15 files changed

Lines changed: 357 additions & 95 deletions

File tree

mkdocs/docs/concepts/backends.md

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,8 @@ There are two ways to configure AWS: using an access key or using the default cr
245245
??? info "Custom security group"
246246
By default, `dstack` creates and manages its own security group per project (opening SSH to `0.0.0.0/0`
247247
and allowing all traffic within the group so multi-node clusters work out of the box).
248-
To use a security group you manage yourself instead, set `security_group_id`:
248+
To use a security group you manage yourself instead, set `security_group_name` if you create a
249+
security group with the same name in every configured region's VPC:
249250

250251
```yaml
251252
projects:
@@ -255,12 +256,30 @@ There are two ways to configure AWS: using an access key or using the default cr
255256
creds:
256257
type: default
257258

258-
security_group_id: sg-0a1b2c3d4e5f6g7h8
259+
security_group_name: my-security-group
259260
```
260261

261-
When `security_group_id` is set, `dstack` attaches it to instances as-is and never adds, removes, or modifies
262-
its rules. You're responsible for SSH reachability (from wherever the `dstack` server and users connect from)
263-
and, for multi-node clusters, for allowing traffic between instances in the group.
262+
If your security groups have different names (or IDs are more convenient) per region, use
263+
`security_group_ids` instead:
264+
265+
```yaml
266+
projects:
267+
- name: main
268+
backends:
269+
- type: aws
270+
creds:
271+
type: default
272+
273+
security_group_ids:
274+
us-east-1: sg-0a1b2c3d4e5f6g7h8
275+
us-west-2: sg-1b2c3d4e5f6g7h8i9
276+
```
277+
278+
Regions not covered by `security_group_ids` fall back to `security_group_name` if set, or to
279+
dstack's auto-created security group otherwise. Either way, `dstack` attaches the security group
280+
to instances as-is and never adds, removes, or modifies its rules. You're responsible for SSH
281+
reachability (from wherever the `dstack` server and users connect from) and, for multi-node
282+
clusters, for allowing traffic between instances in the group.
264283

265284
You can also override this per fleet or run using the `security_group` profile property.
266285

@@ -469,7 +488,8 @@ There are two ways to configure Azure: using a client secret or using the defaul
469488
??? info "Custom network security group"
470489
By default, `dstack` creates and manages its own network security group (opening SSH to the internet
471490
and allowing all traffic within the group so multi-node clusters work out of the box).
472-
To use a network security group you manage yourself instead, set `network_security_group`:
491+
Azure NSG names must be unique within a resource group regardless of region, so a custom NSG is
492+
configured per location via `network_security_group_ids`:
473493

474494
```yaml
475495
projects:
@@ -478,12 +498,16 @@ There are two ways to configure Azure: using a client secret or using the defaul
478498
- type: azure
479499
creds:
480500
type: default
481-
network_security_group: my-network-security-group
501+
regions: [westeurope, eastus]
502+
network_security_group_ids:
503+
westeurope: my-network-security-group-we
504+
eastus: my-network-security-group-eus
482505
```
483506

484-
When `network_security_group` is set, `dstack` attaches it to instances as-is and never adds, removes, or
485-
modifies its rules. You're responsible for SSH reachability and, for multi-node clusters, for allowing
486-
traffic between instances in the group.
507+
Locations not covered by `network_security_group_ids` fall back to dstack's auto-created network
508+
security group. Either way, `dstack` attaches the network security group to instances as-is and
509+
never adds, removes, or modifies its rules. You're responsible for SSH reachability and, for
510+
multi-node clusters, for allowing traffic between instances in the group.
487511

488512
You can also override this per fleet or run using the `security_group` profile property.
489513

@@ -1136,7 +1160,8 @@ There are two ways to configure OCI: using client credentials or using the defau
11361160
??? info "Custom network security group"
11371161
By default, `dstack` creates and manages its own network security group per project (opening SSH to
11381162
`0.0.0.0/0` and allowing all traffic within the VCN so multi-node clusters work out of the box).
1139-
To use a network security group you manage yourself instead, set `network_security_group_id`:
1163+
OCI network security groups are region-scoped, so a custom NSG is configured per region via
1164+
`network_security_group_ids`:
11401165

11411166
```yaml
11421167
projects:
@@ -1145,12 +1170,15 @@ There are two ways to configure OCI: using client credentials or using the defau
11451170
- type: oci
11461171
creds:
11471172
type: default
1148-
network_security_group_id: ocid1.networksecuritygroup.oc1..aaaaaaaa
1173+
network_security_group_ids:
1174+
eu-frankfurt-1: ocid1.networksecuritygroup.oc1..aaaaaaaa
1175+
us-ashburn-1: ocid1.networksecuritygroup.oc1..bbbbbbbb
11491176
```
11501177

1151-
When `network_security_group_id` is set, `dstack` attaches it to instances as-is and never adds, removes,
1152-
or modifies its rules. You're responsible for SSH reachability and, for multi-node clusters, for allowing
1153-
traffic between instances in the group.
1178+
Regions not covered by `network_security_group_ids` fall back to dstack's auto-created network
1179+
security group. Either way, `dstack` attaches the network security group to instances as-is and
1180+
never adds, removes, or modifies its rules. You're responsible for SSH reachability and, for
1181+
multi-node clusters, for allowing traffic between instances in the group.
11541182

11551183
You can also override this per fleet or run using the `security_group` profile property.
11561184

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,20 @@ def create_instance(
331331
instance_type=instance_offer.instance.name,
332332
image_config=self.config.os_images,
333333
)
334-
security_group_id = instance_config.security_group or self.config.security_group_id
334+
security_group_id = instance_config.security_group
335+
if security_group_id is None and self.config.security_group_ids is not None:
336+
security_group_id = self.config.security_group_ids.get(instance_offer.region)
337+
if security_group_id is None and self.config.security_group_name is not None:
338+
security_group_id = aws_resources.get_security_group_id_by_name(
339+
ec2_client=ec2_client,
340+
name=self.config.security_group_name,
341+
vpc_id=vpc_id,
342+
)
343+
if security_group_id is None:
344+
raise ComputeError(
345+
f"Security group '{self.config.security_group_name}' not found in"
346+
f" VPC {vpc_id} (region {instance_offer.region})"
347+
)
335348
if security_group_id is None:
336349
security_group_id = self._create_security_group(
337350
ec2_client=ec2_client,

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ def validate_config(self, config: AWSBackendConfigWithCreds, default_creds_enabl
7777
raise_invalid_credentials_error(fields=[["creds"]])
7878
self._check_config_tags(config)
7979
self._check_config_iam_instance_profile(session, config)
80+
self._check_config_security_group(config)
8081
self._check_config_vpc(session, config)
8182

8283
def create_backend(
@@ -146,6 +147,12 @@ def _check_config_iam_instance_profile(
146147
f"Failed to check IAM instance profile {config.iam_instance_profile}"
147148
)
148149

150+
def _check_config_security_group(self, config: AWSBackendConfigWithCreds):
151+
if config.security_group_name is not None and config.security_group_ids is not None:
152+
raise ServerClientError(
153+
msg="Only one of `security_group_name` and `security_group_ids` can be specified"
154+
)
155+
149156
def _check_config_vpc(self, session: Session, config: AWSBackendConfigWithCreds):
150157
allocate_public_ip = config.public_ips if config.public_ips is not None else True
151158
use_default_vpcs = config.default_vpcs if config.default_vpcs is not None else True

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,37 @@ class AWSBackendConfig(CoreModel):
9696
)
9797
),
9898
] = None
99-
security_group_id: Annotated[
99+
security_group_name: Annotated[
100100
Optional[str],
101101
Field(
102102
description=(
103-
"The ID of an existing security group to use for instances instead of the one"
103+
"The name of an existing security group to use for instances instead of the one"
104104
" `dstack` creates and manages automatically (`dstack_security_group_<project>`)."
105+
" The security group must exist in every VPC `dstack` provisions into."
106+
" If your custom security groups don't have names or have different names in"
107+
" different regions, use `security_group_ids` instead."
105108
" When set, `dstack` does not add, remove, or modify any rules on this security group"
106109
" — you are responsible for SSH reachability and, for multi-node clusters,"
107110
" for allowing traffic between instances in the group"
108111
)
109112
),
110113
] = None
114+
security_group_ids: Annotated[
115+
Optional[Dict[str, str]],
116+
Field(
117+
description=(
118+
"The mapping from AWS regions to the IDs of existing security groups to use for"
119+
" instances instead of the one `dstack` creates and manages automatically."
120+
" Use this instead of `security_group_name` when your security groups don't have"
121+
" names or have different names in different regions."
122+
" Regions not present in this mapping fall back to `security_group_name` if set,"
123+
" or to dstack's auto-created security group otherwise."
124+
" When set, `dstack` does not add, remove, or modify any rules on these security"
125+
" groups — you are responsible for SSH reachability and, for multi-node clusters,"
126+
" for allowing traffic between instances in the group"
127+
)
128+
),
129+
] = None
111130
tags: Annotated[
112131
Optional[Dict[str, str]],
113132
Field(description="The tags that will be assigned to resources created by `dstack`"),

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,21 @@ def create_security_group(
136136
return security_group_id
137137

138138

139+
def get_security_group_id_by_name(
140+
ec2_client: botocore.client.BaseClient,
141+
name: str,
142+
vpc_id: Optional[str],
143+
) -> Optional[str]:
144+
filters = [{"Name": "group-name", "Values": [name]}]
145+
if vpc_id is not None:
146+
filters.append({"Name": "vpc-id", "Values": [vpc_id]})
147+
response = ec2_client.describe_security_groups(Filters=filters)
148+
groups = response.get("SecurityGroups")
149+
if not groups:
150+
return None
151+
return groups[0]["GroupId"]
152+
153+
139154
def create_instances_struct(
140155
disk_size: int,
141156
image_id: str,

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

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,14 +148,14 @@ def create_instance(
148148
location=location,
149149
allocate_public_ip=allocate_public_ip,
150150
)
151-
network_security_group = (
152-
instance_config.security_group
153-
or self.config.network_security_group
154-
or azure_utils.get_default_network_security_group_name(
151+
network_security_group = instance_config.security_group
152+
if network_security_group is None and self.config.network_security_group_ids is not None:
153+
network_security_group = self.config.network_security_group_ids.get(location)
154+
if network_security_group is None:
155+
network_security_group = azure_utils.get_default_network_security_group_name(
155156
resource_group=self.config.resource_group,
156157
location=location,
157158
)
158-
)
159159

160160
managed_identity_resource_group, managed_identity_name = parse_vm_managed_identity(
161161
self.config.vm_managed_identity

src/dstack/_internal/core/backends/azure/configurator.py

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import json
22
from concurrent.futures import ThreadPoolExecutor, as_completed
3-
from typing import List, Optional, Tuple
3+
from typing import Dict, List, Optional, Tuple
44

55
import azure.core.exceptions
66
from azure.core.credentials import TokenCredential
@@ -126,7 +126,7 @@ def create_backend(
126126
resource_group=config.resource_group,
127127
locations=config.regions,
128128
create_default_network=config.vpc_ids is None and config.subnet_ids is None,
129-
create_instance_network_security_group=config.network_security_group is None,
129+
network_security_group_ids=config.network_security_group_ids,
130130
)
131131
return BackendRecord(
132132
config=AzureStoredConfig(
@@ -341,7 +341,7 @@ def _create_network_resources(
341341
resource_group: str,
342342
locations: List[str],
343343
create_default_network: bool,
344-
create_instance_network_security_group: bool = True,
344+
network_security_group_ids: Optional[Dict[str, str]] = None,
345345
):
346346
def func(location: str):
347347
network_manager = NetworkManager(
@@ -354,9 +354,10 @@ def func(location: str):
354354
name=azure_utils.get_default_network_name(resource_group, location),
355355
subnet_name=azure_utils.get_default_subnet_name(resource_group, location),
356356
)
357-
if create_instance_network_security_group:
358-
# Skipped when the user supplies their own network security group via
359-
# `network_security_group` - dstack does not create or manage it in that case.
357+
if location not in (network_security_group_ids or {}):
358+
# Skipped when the user supplies their own network security group for this
359+
# location via `network_security_group_ids` - dstack does not create or manage
360+
# it in that case.
360361
network_manager.create_network_security_group(
361362
resource_group=resource_group,
362363
location=location,

src/dstack/_internal/core/backends/azure/models.py

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,17 @@ class AzureBackendConfig(CoreModel):
8181
)
8282
),
8383
] = None
84-
network_security_group: Annotated[
85-
Optional[str],
84+
network_security_group_ids: Annotated[
85+
Optional[Dict[str, str]],
8686
Field(
8787
description=(
88-
"The name of an existing network security group (in the configured resource group)"
89-
" to use for instances instead of the one `dstack` creates and manages automatically."
90-
" When set, `dstack` does not add, remove, or modify any rules on this network"
91-
" security group — you are responsible for SSH reachability and, for multi-node"
88+
"The mapping from Azure locations to the names of existing network security groups"
89+
" (in the configured resource group) to use for instances instead of the one `dstack`"
90+
" creates and manages automatically."
91+
" Locations not present in this mapping fall back to dstack's auto-created"
92+
" network security group."
93+
" When set, `dstack` does not add, remove, or modify any rules on these network"
94+
" security groups — you are responsible for SSH reachability and, for multi-node"
9295
" clusters, for allowing traffic between instances in the group"
9396
)
9497
),

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,11 @@ def create_instance(
138138
subnet: oci.core.models.Subnet = region.virtual_network_client.get_subnet(
139139
self.config.subnet_ids_per_region[instance_offer.region]
140140
).data
141-
security_group_id = (
142-
instance_config.security_group or self.config.network_security_group_id
143-
)
141+
security_group_id = instance_config.security_group
142+
if security_group_id is None and self.config.network_security_group_ids is not None:
143+
security_group_id = self.config.network_security_group_ids.get(
144+
instance_offer.region
145+
)
144146
if security_group_id is None:
145147
security_group = resources.get_or_create_security_group(
146148
f"dstack-{instance_config.project_name}-default-security-group",

src/dstack/_internal/core/backends/oci/models.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,16 @@ class OCIBackendConfig(CoreModel):
6969
)
7070
),
7171
] = None
72-
network_security_group_id: Annotated[
73-
Optional[str],
72+
network_security_group_ids: Annotated[
73+
Optional[Dict[str, str]],
7474
Field(
7575
description=(
76-
"The OCID of an existing network security group to use for instances instead of the"
77-
" one `dstack` creates and manages automatically."
78-
" When set, `dstack` does not add, remove, or modify any rules on this network"
79-
" security group — you are responsible for SSH reachability and, for multi-node"
76+
"The mapping from OCI regions to the OCIDs of existing network security groups to"
77+
" use for instances instead of the one `dstack` creates and manages automatically."
78+
" Regions not present in this mapping fall back to dstack's auto-created network"
79+
" security group."
80+
" When set, `dstack` does not add, remove, or modify any rules on these network"
81+
" security groups — you are responsible for SSH reachability and, for multi-node"
8082
" clusters, for allowing traffic between instances in the group"
8183
)
8284
),

0 commit comments

Comments
 (0)