Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 59 additions & 32 deletions docs/docs/concepts/gateways.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ domain: example.com

</div>

A domain name is required to create a gateway.

To create or update the gateway, simply call the [`dstack apply`](../reference/cli/dstack/apply.md) command:

<div class="termy">
Expand All @@ -53,6 +51,12 @@ Provisioning...

## Configuration options

### Domain

A gateway requires a `domain` to be specified in the configuration before creation. The domain is used to generate service endpoints (e.g. `<run name>.<gateway domain>`).

Once the gateway is created and assigned a hostname, configure your DNS by adding a wildcard record for `*.<gateway domain>` (e.g. `*.example.com`). The record should point to the gateway's hostname and should be of type `A` if the hostname is an IP address (most cases), or of type `CNAME` if the hostname is another domain (some private gateways and Kubernetes).

### Backend

You can create gateways with the `aws`, `azure`, `gcp`, or `kubernetes` backends, but that does not limit where services run. A gateway can use one backend while services run on any other backend supported by dstack, including backends where gateways themselves cannot be created.
Expand All @@ -61,27 +65,6 @@ You can create gateways with the `aws`, `azure`, `gcp`, or `kubernetes` backends
Gateways in `kubernetes` backend require an external load balancer. Managed Kubernetes solutions usually include a load balancer.
For self-hosted Kubernetes, you must provide a load balancer by yourself.

### Instance type

By default, `dstack` provisions a small, low-cost instance for the gateway. If you expect to run high-traffic services, you can configure a larger instance type using the `instance_type` property.

<div editor-title="gateway.dstack.yml">

```yaml
type: gateway
name: example-gateway

backend: aws
region: eu-west-1

# (Optional) Override the gateway instance type
instance_type: t3.large

domain: example.com
```

</div>

### Router

By default, the gateway uses its own load balancer to route traffic between replicas. However, you can delegate this responsibility to a specific router by setting the `router` property. Currently, the only supported external router is `sglang`.
Expand Down Expand Up @@ -123,21 +106,65 @@ router:
>
> Support for prefill/decode disaggregation and auto-scaling based on inter-token latency is coming soon.

### Certificate

By default, when you run a service with a gateway, `dstack` provisions an SSL certificate via Let's Encrypt for the configured domain. This automatically enables HTTPS for the service endpoint.

If you disable [public IP](#public-ip) (e.g. to make the gateway private) or if you simply don't need HTTPS, you can set `certificate` to `null`.

> Note, by default services set [`https`](../reference/dstack.yml/service.md#https) to `true` which requires a certificate. You can set `https` to `auto` to detect if the gateway supports HTTPS or not automatically.

??? info "Certificate types"
`dstack` supports the following certificate types:

* `lets-encrypt` (default) — Automatic certificates via [Let's Encrypt](https://letsencrypt.org/). Requires a [public IP](#public-ip).
* `acm` — Certificates managed by [AWS Certificate Manager](https://aws.amazon.com/certificate-manager/). AWS-only. TLS is terminated at the load balancer, not at the gateway.
* `null` — No certificate. Services will use HTTP.

### Public IP

If you don't need/want a public IP for the gateway, you can set the `public_ip` to `false` (the default value is `true`), making the gateway private.
If you don't need a public IP for the gateway, you can set `public_ip` to `false` (the default is `true`), making the gateway private.

Private gateways are currently supported in `aws` and `gcp` backends.

!!! info "Reference"
For all gateway configuration options, refer to the [reference](../reference/dstack.yml/gateway.md).
<div editor-title="gateway.dstack.yml">

## Update DNS records
```yaml
type: gateway
name: private-gateway

backend: aws
region: eu-west-1
domain: example.com

public_ip: false
certificate: null
```

</div>

### Instance type

By default, `dstack` provisions a small, low-cost instance for the gateway. If you expect to run high-traffic services, you can configure a larger instance type using the `instance_type` property.

<div editor-title="gateway.dstack.yml">

Once the gateway is assigned a hostname, go to your domain's DNS settings
and add a DNS record for `*.<gateway domain>`, e.g. `*.example.com`.
The record should point to the gateway's hostname shown in `dstack`
and should be of type `A` if the hostname is an IP address (most cases),
or of type `CNAME` if the hostname is another domain (some private gateways and Kubernetes).
```yaml
type: gateway
name: example-gateway

backend: aws
region: eu-west-1

instance_type: t3.large

domain: example.com
```

</div>

!!! info "Reference"
For all gateway configuration options, refer to the [reference](../reference/dstack.yml/gateway.md).

## Manage gateways

Expand Down
2 changes: 2 additions & 0 deletions docs/docs/reference/dstack.yml/gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ The `gateway` configuration type allows creating and updating [gateways](../../c

### `certificate`

Set to `null` to disable certificates (e.g. for [private gateways](../../concepts/gateways.md#public-ip)).

=== "Let's encrypt"

#SCHEMA# dstack._internal.core.models.gateways.LetsEncryptGatewayCertificate
Expand Down
10 changes: 7 additions & 3 deletions src/dstack/_internal/core/models/configurations.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,9 +856,13 @@ class ServiceConfigurationParams(CoreModel):
)
),
] = None
https: Annotated[bool, Field(description="Enable HTTPS if running with a gateway")] = (
SERVICE_HTTPS_DEFAULT
)
https: Annotated[
Union[bool, Literal["auto"]],
Field(
description="Enable HTTPS if running with a gateway."
" Set to `auto` to determine automatically based on gateway configuration"
),
] = SERVICE_HTTPS_DEFAULT
auth: Annotated[bool, Field(description="Enable the authorization")] = True

scaling: Annotated[
Expand Down
5 changes: 4 additions & 1 deletion src/dstack/_internal/core/models/gateways.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,10 @@ class GatewayConfiguration(CoreModel):
public_ip: Annotated[bool, Field(description="Allocate public IP for the gateway")] = True
certificate: Annotated[
Optional[AnyGatewayCertificate],
Field(description="The SSL certificate configuration. Defaults to `type: lets-encrypt`"),
Field(
description="The SSL certificate configuration."
" Set to `null` to disable. Defaults to `type: lets-encrypt`"
),
] = LetsEncryptGatewayCertificate()
tags: Annotated[
Optional[Dict[str, str]],
Expand Down
12 changes: 9 additions & 3 deletions src/dstack/_internal/server/services/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
)
from dstack._internal.core.models.configurations import (
DEFAULT_REPLICA_GROUP_NAME,
SERVICE_HTTPS_DEFAULT,
ServiceConfiguration,
)
from dstack._internal.core.models.gateways import GatewayConfiguration, GatewayStatus
Expand Down Expand Up @@ -241,7 +240,7 @@ def _register_service_in_server(run_model: RunModel, run_spec: RunSpec) -> Servi
"Service with SGLang router configuration requires a gateway. "
"Please configure a gateway with the SGLang router enabled."
)
if run_spec.configuration.https != SERVICE_HTTPS_DEFAULT:
if run_spec.configuration.https is False:
# Note: if the user sets `https: <default-value>`, it will be ignored silently
# TODO: in 0.19, make `https` Optional to be able to tell if it was set or omitted
raise ServerClientError(
Expand Down Expand Up @@ -416,7 +415,14 @@ async def unregister_replica(session: AsyncSession, job_model: JobModel):

def _get_service_https(run_spec: RunSpec, configuration: GatewayConfiguration) -> bool:
assert run_spec.configuration.type == "service"
if not run_spec.configuration.https:
https = run_spec.configuration.https
if https == "auto":
if configuration.certificate is None:
return False
if configuration.certificate.type == "acm":
return False
return True
if not https:
return False
if configuration.certificate is not None and configuration.certificate.type == "acm":
return False
Expand Down
100 changes: 100 additions & 0 deletions src/tests/_internal/server/services/services/test_services.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from typing import Literal, Optional, Union
from unittest.mock import MagicMock

import pytest

from dstack._internal.core.errors import ServerClientError
from dstack._internal.core.models.backends.base import BackendType
from dstack._internal.core.models.configurations import ServiceConfiguration
from dstack._internal.core.models.gateways import (
ACMGatewayCertificate,
AnyGatewayCertificate,
GatewayConfiguration,
LetsEncryptGatewayCertificate,
)
from dstack._internal.core.models.runs import RunSpec
from dstack._internal.server.services.services import (
_get_service_https,
_register_service_in_server,
)
from dstack._internal.server.testing.common import get_run_spec


def _service_run_spec(https: Union[bool, Literal["auto"]] = "auto") -> RunSpec:
return get_run_spec(
repo_id="test-repo",
configuration=ServiceConfiguration(commands=["python serve.py"], port=8000, https=https),
)


def _gateway_config(
certificate: Optional[AnyGatewayCertificate] = LetsEncryptGatewayCertificate(),
) -> GatewayConfiguration:
return GatewayConfiguration(
backend=BackendType.AWS,
region="us-east-1",
certificate=certificate,
)


def _mock_run_model() -> MagicMock:
run_model = MagicMock()
run_model.project.name = "test-project"
run_model.run_name = "test-run"
return run_model


class TestServiceConfigurationHttps:
def test_default_is_true(self) -> None:
conf = ServiceConfiguration(commands=["python serve.py"], port=8000)
assert conf.https is True

def test_accepts_auto(self) -> None:
conf = ServiceConfiguration(commands=["python serve.py"], port=8000, https="auto")
assert conf.https == "auto"


class TestGetServiceHttps:
def test_auto_resolves_to_true_with_lets_encrypt_gateway(self) -> None:
run_spec = _service_run_spec(https="auto")
gw = _gateway_config(certificate=LetsEncryptGatewayCertificate())
assert _get_service_https(run_spec, gw) is True

def test_auto_resolves_to_false_when_gateway_has_no_certificate(self) -> None:
run_spec = _service_run_spec(https="auto")
gw = _gateway_config(certificate=None)
assert _get_service_https(run_spec, gw) is False

def test_auto_resolves_to_false_with_acm_gateway(self) -> None:
run_spec = _service_run_spec(https="auto")
gw = _gateway_config(
certificate=ACMGatewayCertificate(arn="arn:aws:acm:us-east-1:123:cert/abc")
)
assert _get_service_https(run_spec, gw) is False

def test_true_enables_https_regardless_of_gateway_certificate(self) -> None:
run_spec = _service_run_spec(https=True)
gw = _gateway_config(certificate=None)
assert _get_service_https(run_spec, gw) is True

def test_false_disables_https_regardless_of_gateway_certificate(self) -> None:
run_spec = _service_run_spec(https=False)
gw = _gateway_config(certificate=LetsEncryptGatewayCertificate())
assert _get_service_https(run_spec, gw) is False


class TestRegisterServiceInServerHttps:
def test_allows_default_true_without_gateway(self) -> None:
run_spec = _service_run_spec(https=True)
result = _register_service_in_server(_mock_run_model(), run_spec)
assert result is not None

def test_allows_auto_without_gateway(self) -> None:
run_spec = _service_run_spec(https="auto")
result = _register_service_in_server(_mock_run_model(), run_spec)
assert result is not None

def test_rejects_explicit_false_without_gateway(self) -> None:
run_spec = _service_run_spec(https=False)
with pytest.raises(ServerClientError, match="not applicable"):
_register_service_in_server(_mock_run_model(), run_spec)