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
27 changes: 0 additions & 27 deletions .github/workflows/build-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -163,33 +163,6 @@ jobs:
runner/dstack-shim-${{ matrix.goos }}-${{ matrix.goarch }}
retention-days: 1

gateway-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up uv
uses: astral-sh/setup-uv@v5
with:
python-version: 3.11
- name: Build package
working-directory: gateway
run: |
echo "__version__ = \"${{ inputs.version }}\"" > src/dstack/gateway/version.py
# TODO: depend on a specific dstackai/dstack commit for staging builds?
if [[ "${{ inputs.staging }}" == "false" ]]; then
sed \
-i.old \
"s|@ https://github.com/dstackai/dstack/archive/refs/heads/master.tar.gz|== ${{ inputs.version }}|" \
pyproject.toml
diff pyproject.toml pyproject.toml.old > /dev/null && echo "Could not set version" && exit 1
fi
uv build
- uses: actions/upload-artifact@v4
with:
name: dstack-gateway
path: gateway/dist/dstack_gateway-${{ inputs.version }}-py3-none-any.whl
retention-days: 1

python-build:
needs: [code-lint, frontend-build]
runs-on: ubuntu-latest
Expand Down
15 changes: 0 additions & 15 deletions .github/workflows/upload-pre-pypi-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,6 @@ jobs:
ignore-empty-workdir: true
- name: Install AWS
run: uv tool install awscli
- name: Download dstack-gateway
uses: actions/download-artifact@v4
with:
name: dstack-gateway
path: gateway
- name: Upload dstack-gateway to S3
working-directory: gateway
run: |
CHANNEL=release
if [ "${{ inputs.staging }}" = "true" ]; then
CHANNEL=stgn
fi
WHEEL=dstack_gateway-${{ inputs.version }}-py3-none-any.whl
aws s3 cp $WHEEL "s3://dstack-gateway-downloads/$CHANNEL/$WHEEL"
echo "${{ inputs.version }}" | aws s3 cp - "s3://dstack-gateway-downloads/$CHANNEL/latest-version"
- name: Download dstack-runner
uses: actions/download-artifact@v4
with:
Expand Down
4 changes: 2 additions & 2 deletions contributing/AUTOSCALING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

`dstack` features auto-scaling for services published via the gateway. The general flow is:

- STEP 1: `dstack-gateway` parses nginx `access.log` to collect per-second statistics about requests to the service and request times.
- STEP 2: `dstack-gateway` aggregates statistics over several predefined windows.
- STEP 1: The gateway parses nginx `access.log` to collect per-second statistics about requests to the service and request times.
- STEP 2: The gateway aggregates statistics over several predefined windows.
- STEP 3: The server keeps gateway connections alive in the scheduled `process_gateways_connections` task and continuously collects stats from active gateways. This is separate from `GatewayPipeline`, which handles gateway provisioning and deletion.
- STEP 4: When `RunPipeline` processes a service run, it loads the latest collected gateway stats for that service.
- STEP 5: The autoscaler (configured via `dstack.yml`) computes the desired replica count for each replica group.
Expand Down
62 changes: 12 additions & 50 deletions contributing/PROXY.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Provisioning happens as follows:
1. Launch a non-GPU instance (usually the smallest) with all ports exposed.
2. Install Nginx, Certbot, and patch configs.
3. Create blue-green virtual environments.
4. Install the latest `dstack-gateway` package from the S3 bucket. `dstack-gateway` is a thin package that depends on the `dstack` package, which contains the actual gateway implementation.
4. Install the `dstack[gateway]` Python package.
5. Run the systemd service `dstack.gateway.service`.

### Update
Expand All @@ -152,56 +152,18 @@ The update process looks like this:

## Gateway development

The gateway app needs to interact with Nginx and certbot, so running it locally can be challenging. One way to test your code is to upload your development branch to an existing gateway and run the gateway app from source.
The gateway app needs to interact with Nginx and certbot, so running it locally can be challenging. You can test your development version by deploying it to a live gateway.

1. Run `dstack server` with `DSTACK_SKIP_GATEWAY_UPDATE=1` environment variable. This will prevent `dstack` from updating and starting the standard gateway version on each server restart.
For example, push your version to GitHub and run `dstack server` with the `DSTACK_GATEWAY_PACKAGE_URL` environment variable, which will instruct `dstack` to deploy that version to all gateways.

1. Provision a gateway through `dstack`:

```shell
dstack apply -f my-gateway.dstack.yml
```

1. Save the gateway key to a file:

```shell
sqlite3 ~/.dstack/server/data/sqlite.db "SELECT ssh_private_key FROM gateway_computes WHERE deleted = 0 AND ip_address = '<gateway-ip-addr>'" > /tmp/gateway.key
chmod 600 /tmp/gateway.key
```

1. Deliver your code to the gateway. For example, clone it from a remote repo:

```shell
ssh -i /tmp/gateway.key ubuntu@gateway.example "git clone https://github.com/dstackai/dstack.git ~/dstack-repo"
```

Or push it from your machine:

```shell
ssh -i /tmp/gateway.key ubuntu@gateway.example "git init ~/dstack-repo"
git remote add gateway ubuntu@gateway.example:~/dstack-repo
GIT_SSH_COMMAND='ssh -i /tmp/gateway.key' git push gateway branch_name
```

1. Connect to the gateway:

```shell
ssh -i /tmp/gateway.key ubuntu@gateway.example
```

1. Prepare an environment with your development branch on the gateway:

```shell
cd ~/dstack-repo
git checkout branch_name
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.local/bin/env
uv sync --extra gateway
```
```shell
DSTACK_GATEWAY_PACKAGE_URL=https://github.com/dstackai/dstack/archive/refs/heads/my_development_branch.zip dstack server
```

1. Stop the gateway service and start your development version from source:
To access logs or troubleshoot, extract the gateway's private key from the database and connect to it via SSH. Logs are available in `journalctl`.

```shell
sudo systemctl stop dstack.gateway.service
uv run uvicorn dstack._internal.proxy.gateway.main:app
```
```shell
sqlite3 ~/.dstack/server/data/sqlite.db "SELECT ssh_private_key FROM gateway_computes WHERE deleted = 0 AND ip_address = '$GATEWAY_IP_ADDR'" > /tmp/gateway.key
chmod 600 /tmp/gateway.key
ssh -i /tmp/gateway.key ubuntu@$GATEWAY_IP_ADDR sudo journalctl -u dstack.gateway.service
```
2 changes: 1 addition & 1 deletion contributing/RUNNER-AND-SHIM.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,6 @@ All communication between the `dstack` server and `dstack-runner` happens via HT

## SSH tunnels

`dstack` expects a running SSH server right next to the `dstack-runner`. It provides a secure channel for communication with the runner API and forwarding any ports without listening for `0.0.0.0`. The `dstack-gateway` also uses this SSH server for forwarding requests from public endpoints.
`dstack` expects a running SSH server right next to the `dstack-runner`. It provides a secure channel for communication with the runner API and forwarding any ports without listening for `0.0.0.0`. The gateway also uses this SSH server for forwarding requests from public endpoints.

`dstack-shim` must also be running next to the SSH server. The `dstack` server connects to this SSH server for interacting with both `dstack-shim` and `dstack-runner`. The CLI uses this SSH server as a jump host because the user wants to connect to the container.
5 changes: 0 additions & 5 deletions gateway/README.md

This file was deleted.

23 changes: 0 additions & 23 deletions gateway/pyproject.toml

This file was deleted.

Empty file.
1 change: 0 additions & 1 deletion gateway/src/dstack/gateway/main.py

This file was deleted.

Empty file.
1 change: 0 additions & 1 deletion gateway/src/dstack/gateway/version.py

This file was deleted.

Empty file removed gateway/src/tests/__init__.py
Empty file.
1 change: 1 addition & 0 deletions mkdocs/docs/reference/env.md
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ slows down processing and may cause CPU spikes due to frequent SSH-connection es
`https://dstack.example.com/{arch}/{version}/dstack-runner`.
* `DSTACK_SHIM_DOWNLOAD_URL` – Overrides `dstack-shim` binary download URL. The URL can contain `{version}` and/or `{arch}` placeholders,
see `DSTACK_RUNNER_DOWNLOAD_URL` for the details.
* `DSTACK_GATEWAY_PACKAGE_URL` – Overrides the URL the `dstack` package is installed from on gateway instances (used as `dstack[gateway] @ <URL>`).
* `DSTACK_DEFAULT_CREDS_DISABLED` – Disables default credentials detection if set. Defaults to `None`.

## CLI
Expand Down
20 changes: 9 additions & 11 deletions src/dstack/_internal/core/backends/base/compute.py
Original file line number Diff line number Diff line change
Expand Up @@ -1218,25 +1218,23 @@ def get_latest_runner_build() -> Optional[str]:
return None


def get_dstack_gateway_wheel(build: str) -> str:
channel = "release" if settings.DSTACK_RELEASE else "stgn"
base_url = f"https://dstack-gateway-downloads.s3.amazonaws.com/{channel}"
if build == "latest":
build = _fetch_version(f"{base_url}/latest-version") or "latest"
logger.debug("Found the latest gateway build: %s", build)
wheel = f"{base_url}/dstack_gateway-{build}-py3-none-any.whl"
return f"dstack-gateway @ {wheel}"
def get_dstack_gateway_package_and_target_version() -> tuple[str, str | None]:
if settings.DSTACK_GATEWAY_PACKAGE_URL:
return f"dstack[gateway] @ {settings.DSTACK_GATEWAY_PACKAGE_URL}", None
if settings.DSTACK_VERSION is not None:
return f"dstack[gateway]=={settings.DSTACK_VERSION}", settings.DSTACK_VERSION
package = "dstack[gateway] @ https://github.com/dstackai/dstack/archive/refs/heads/master.zip"
return package, None


def get_dstack_gateway_commands() -> List[str]:
build = get_dstack_runner_version() or "latest"
gateway_package = get_dstack_gateway_wheel(build)
gateway_package, _ = get_dstack_gateway_package_and_target_version()
return [
"mkdir -p /home/ubuntu/dstack",
"python3 -m venv /home/ubuntu/dstack/blue",
"python3 -m venv /home/ubuntu/dstack/green",
f"/home/ubuntu/dstack/blue/bin/pip install '{gateway_package}'",
"sudo /home/ubuntu/dstack/blue/bin/python -m dstack.gateway.systemd install --run",
"sudo /home/ubuntu/dstack/blue/bin/python -m dstack._internal.proxy.gateway.systemd install --run",
]


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ else
version="blue"
echo "$version" > "$root/version"
fi
"$root/$version/bin/uvicorn" dstack.gateway.main:app
"$root/$version/bin/uvicorn" dstack._internal.proxy.gateway.main:app
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,12 @@ def install_action(args):
uid, gid = user.pw_uid, user.pw_gid

print("Writing service file...")
service_file = importlib.resources.read_text(
"dstack.gateway.resources.systemd", service_path.name
)
service_file = read_resource(service_path.name)
service_path.write_text(service_file.format(working_dir=working_dir.as_posix()))

for script_name in ["start.sh"]:
print(f"Writing {script_name} script...")
script = importlib.resources.read_text("dstack.gateway.resources.systemd", script_name)
script = read_resource(script_name)
script_path = working_dir / script_name
script_path.write_text(script)
os.chown(script_path, uid, gid)
Expand All @@ -50,5 +48,13 @@ def install_action(args):
assert subprocess.run(["systemctl", "enable", service_name] + args).returncode == 0


def read_resource(file: str) -> str:
return (
importlib.resources.files("dstack._internal.proxy.gateway")
.joinpath(f"resources/systemd/{file}")
.read_text()
)


if __name__ == "__main__":
main()
30 changes: 17 additions & 13 deletions src/dstack/_internal/server/services/gateways/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@

import dstack._internal.utils.random_names as random_names
from dstack._internal.core.backends.base.compute import (
get_dstack_gateway_wheel,
get_dstack_runner_version,
get_dstack_gateway_package_and_target_version,
)
from dstack._internal.core.backends.features import (
BACKENDS_WITH_GATEWAY_SUPPORT,
Expand Down Expand Up @@ -782,11 +781,11 @@ async def init_gateways(session: AsyncSession):
"Skipping gateway replicas update due to DSTACK_SKIP_GATEWAY_UPDATE env variable"
)
else:
build = get_dstack_runner_version() or "latest"
gateway_package, target_version = get_dstack_gateway_package_and_target_version()

for gateway_replica, res in await gather_map_async(
gateway_replicas,
lambda r: _update_gateway_replica(r, build),
lambda r: _update_gateway_replica(r, gateway_package, target_version),
return_exceptions=True,
):
if isinstance(res, Exception):
Expand All @@ -808,7 +807,11 @@ async def init_gateways(session: AsyncSession):
)


async def _update_gateway_replica(gateway_replica_model: GatewayReplicaModel, build: str) -> bool:
async def _update_gateway_replica(
gateway_replica_model: GatewayReplicaModel,
gateway_package: str,
target_version: str | None,
) -> bool:
if gateway_replica_model.ip_address is None:
logger.warning(
"Gateway replica %s has no ip_address, cannot update", gateway_replica_model.id
Expand All @@ -826,15 +829,13 @@ async def _update_gateway_replica(gateway_replica_model: GatewayReplicaModel, bu
)
logger.debug("Updating gateway replica %s", connection.ip_address)

# Build package spec with extras and wheel URL
gateway_package = get_dstack_gateway_wheel(build)
command = (
"/bin/sh -c "
+ shlex.quote(_GATEWAY_UPDATE_SCRIPT)
+ " sh " # $0 placeholder
+ shlex.quote(gateway_package)
+ " "
+ shlex.quote(build)
+ shlex.quote(target_version or "")
)
stdout = await connection.tunnel.aexec(command)
if "Update successfully completed" in stdout:
Expand All @@ -854,20 +855,23 @@ async def _update_gateway_replica(gateway_replica_model: GatewayReplicaModel, bu
else
version=blue
fi
current_build=$("$root/$version/bin/pip" show dstack-gateway | grep Version | awk '{print $2}')
if [ "$current_build" = "$build" ]; then
echo "The build $build is already installed. Skipping..."
exit 0
if [ -n "$build" ]; then
current_build=$("$root/$version/bin/pip" show dstack | grep Version | awk '{print $2}')
if [ "$current_build" = "$build" ]; then
echo "The build $build is already installed. Skipping..."
exit 0
fi
fi
if [ "$version" = blue ]; then
version=green
else
version=blue
fi
# dstack-gateway is a pre-0.21.4 package that may still be installed and require a conflicting dstack version
"$root/$version/bin/pip" uninstall -y dstack-gateway dstack
"$root/$version/bin/pip" cache remove dstack
"$root/$version/bin/pip" install "$gateway_package"
sudo "$root/$version/bin/python" -m dstack.gateway.systemd install
sudo "$root/$version/bin/python" -m dstack._internal.proxy.gateway.systemd install
echo "$version" > "$root/version"
sudo systemctl daemon-reload
sudo systemctl restart dstack.gateway
Expand Down
1 change: 1 addition & 0 deletions src/dstack/_internal/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
DSTACK_SHIM_VERSION_URL = os.getenv("DSTACK_SHIM_VERSION_URL")
DSTACK_SHIM_DOWNLOAD_URL = os.getenv("DSTACK_SHIM_DOWNLOAD_URL")
DSTACK_USE_LATEST_FROM_BRANCH = os.getenv("DSTACK_USE_LATEST_FROM_BRANCH") is not None
DSTACK_GATEWAY_PACKAGE_URL = os.getenv("DSTACK_GATEWAY_PACKAGE_URL")


DSTACK_DOCKER_BASE_IMAGE = os.getenv("DSTACK_DOCKER_BASE_IMAGE", "dstackai/base")
Expand Down
Loading