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
1 change: 1 addition & 0 deletions apps/docs/content/homepage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export const containers = [
{ name: "Ubuntu", link: "/ubuntu/overview", icon: <Icons.ubuntu/> },
{ name: "Alpine", link: "/alpine/overview", icon: <Icons.alpine/> },
{ name: "Docker", link: "/docker/overview", icon: <Icons.docker/> },
{ name: "Swarm", link: "/swarm/overview", icon: <Icons.servers-connected/> },
]

export const databases = [
Expand Down
4 changes: 4 additions & 0 deletions apps/docs/content/references/import-yaml/type-list.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ Versions listed on the same line are aliases of the same underlying version.
<td className="w-fit">Ubuntu</td>
<td className="w-fit"><UnorderedCodeList data={data.ubuntu.import} /></td>
</tr>
<tr>
<td className="w-fit">Swarm</td>
<td className="w-fit"><UnorderedCodeList data={data.swarm.import} /></td>
</tr>
</tbody>
</table>

Expand Down
145 changes: 145 additions & 0 deletions apps/docs/content/swarm/how-to/connect.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
---
title: Connect & authenticate
description: Reach the Swarm orchestrator from the Zerops VPN, from your other services and from the GUI, and understand the API token, the admin token and SSH access to pool containers.
---

The orchestrator serves everything on one address inside the project's private network:

```
http://orch.<hostname>.zerops
```

For a Swarm service with the hostname `sandbox` that is `http://orch.sandbox.zerops`. The control API, the web UI at `/` and the API reference at `/swagger` all live there. The orchestrator has no public address.

## Ways to connect

<table className="w-full my-1.5">
<thead>
<tr>
<th className="w-fit" style={{ minWidth: '150px' }}>From</th>
<th className="w-fit">How</th>
<th className="w-fit" style={{ minWidth: '130px' }}>API token</th>
</tr>
</thead>
<tbody>
<tr>
<td className="w-fit"><b>Your workstation</b></td>
<td className="w-fit">Start the <a href="/references/networking/vpn">Zerops VPN</a> and call <code>{'http://orch.<hostname>.zerops'}</code>, or open it in a browser for the UI.</td>
<td className="w-fit">Not needed</td>
</tr>
<tr>
<td className="w-fit"><b>A service in the project</b></td>
<td className="w-fit">Call the same address over the private network.</td>
<td className="w-fit">Required</td>
</tr>
<tr>
<td className="w-fit"><b>The Zerops GUI</b></td>
<td className="w-fit">Open the orchestrator UI from the service detail. Zerops creates a link that is valid for one hour and signs you in.</td>
<td className="w-fit">Added for you</td>
</tr>
</tbody>
</table>

### From your workstation

Connect with `zcli vpn up` and use the API directly. Requests that come from the VPN skip the API token check, so there is nothing to configure:

```bash
curl -s http://orch.sandbox.zerops/container
```

### From a service in the project

This is the usual setup: a backend, a CI runner or an agent in the same project drives the pool. It has to send the API token as a bearer token:

```bash
curl -s http://orch.sandbox.zerops/container \
-H "Authorization: Bearer $SANDBOX_API_TOKEN"
```

Pass the token to your service by [referencing](/features/env-variables#referencing-variables) the Swarm service's variable in its `zerops.yaml`:

```yaml title="zerops.yaml"
zerops:
- setup: api
run:
envVariables:
SANDBOX_URL: http://orch.sandbox.zerops
SANDBOX_API_TOKEN: ${sandbox_API_TOKEN}
```

### From the GUI

The link from the service detail goes through a Zerops proxy that adds the API token to every request, so the UI works without the VPN. The proxy never adds the admin token. To force an action in the UI you enter the admin token there yourself.

The UI covers the whole API, including a shell to run commands, and is handy for watching what your code does with the pool.

## Tokens

Both tokens are generated when the service is created. You find them in the service detail under **Environment variables**.

<table className="w-full my-1.5">
<thead>
<tr>
<th className="w-fit" style={{ minWidth: '130px' }}>Variable</th>
<th className="w-fit" style={{ minWidth: '220px' }}>Sent as</th>
<th className="w-fit">What it allows</th>
</tr>
</thead>
<tbody>
<tr>
<td className="w-fit"><code>API_TOKEN</code></td>
<td className="w-fit"><code>{'Authorization: Bearer <token>'}</code></td>
<td className="w-fit">Every call of the API. Whoever has it can create and remove containers and run commands in any container nobody else holds. Not required from the VPN.</td>
</tr>
<tr>
<td className="w-fit"><code>ADMIN_TOKEN</code></td>
<td className="w-fit"><code>{'X-Swarm-Admin-Token: <token>'}</code></td>
<td className="w-fit">Taking a container away from the consumer that holds it, with <code>force=true</code>. Required from the VPN too.</td>
</tr>
</tbody>
</table>

A pool is usually shared: every consumer has the API token, and a [lease](/swarm/how-to/use#leases) keeps them out of each other's containers. Breaking a lease kills somebody's work, so it takes a second secret, which you give only to operators and to the code that cleans up after crashed consumers.

You can check an admin token without doing anything with it:

```bash
curl -s http://orch.sandbox.zerops/admin/check \
-H "Authorization: Bearer $SANDBOX_API_TOKEN" \
-H "X-Swarm-Admin-Token: $SANDBOX_ADMIN_TOKEN"
# {"enforced":true,"configured":true,"admin":true}
```

### What is open and what is not

- The UI files (`/` and `/ui/*`) and the API reference (`/swagger`) are served without a token. They contain nothing about your pool.
- If you empty `API_TOKEN`, the API is open to everything that can reach it on the private network, and `force` needs no admin token either.
- If `API_TOKEN` is set and you empty `ADMIN_TOKEN`, every forced takeover is refused.
- The pool containers never see either token, as the code running in them could otherwise control the whole pool.

### Change a token

Edit the variable in the GUI and then **reload** the Swarm service. The orchestrator reads its tokens when it starts, and a reload restarts it together with the start commands of the application you deployed to the pool, if any. It does not restart the pool containers, so it is the gentle option. A **restart** of the service works too, but restarts every container in the pool.

Reservations survive both. The orchestrator keeps them on disk.

## SSH access to pool containers

The API is the intended way to run things in a pool container, and by default it is the only way available to your services. SSH access in Zerops is governed by [SSH isolation](/references/networking/ssh#ssh-access-control), and its default, `vpn`, means:

- You can SSH from the VPN to any pool container, using the `hostname` the API returns for it. The [web terminal](/references/networking/ssh#web-terminal-always-available) in the GUI works too.
- No service in the project can SSH to a pool container, and pool containers cannot SSH to each other or to your other services.

If you want a service to SSH into the pool, allow it on the Swarm service:

```yaml title="zerops-import.yaml"
services:
- hostname: sandbox
type: swarm@1
sshIsolation: "vpn service@runner"
```

:::warning
SSH does not know about leases. A service that is allowed to SSH into the pool can enter any container, including one another consumer has reserved, and the orchestrator cannot see or stop what it does there. Allow it only for services you would also trust with the admin token.
:::
132 changes: 132 additions & 0 deletions apps/docs/content/swarm/how-to/create.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
---
title: Create & import
description: Create a Swarm service in the Zerops GUI or import it with a YAML definition, choose between containers and VMs, and set the limits of the pool.
---

Create a Swarm service in the [GUI](#create-in-the-gui), or describe it in YAML and [import](#import-with-yaml) it through the GUI or zCLI.

## Create in the GUI

Go to your project dashboard, choose **Add new service** in the **Services** block and click **Swarm** (a pool of Linux containers) or **Swarm VM** (a pool of virtual machines). See [Containers or virtual machines](/swarm/overview#containers-or-virtual-machines) for the difference. The dialog asks for:

- **Hostname**: a unique service identifier, like `sandbox`, `runners` or `pool`. Maximum 25 characters, lowercase ASCII letters (a-z) and numbers (0-9) only, unique within the project. The orchestrator is reachable at `orch.<hostname>.zerops`.
- **Pool limits and resources**: the minimum and maximum number of containers, and the resources of each one. See [Size the pool](#size-the-pool).
- **Start an empty Swarm service without requiring code first**: turn it on to use the pool right away. When it is off, the service waits for your first [deploy](/swarm/how-to/deploy) before the pool can be used.

:::caution
The **hostname** and the **type** (containers or VMs) are fixed once the service is created. Pool limits and resources can be changed at any time.
:::

## Import with YAML

You can paste the YAML in the GUI (**Import services** in the left menu of your project) or import it with the [zCLI](/references/cli).

```yaml title="zerops-import.yaml"
services:
- hostname: sandbox
# swarm@1 for containers, swarm-vm@1 for virtual machines
type: swarm@1
# optional: use the pool right away, without a first deploy
startWithoutCode: true
# optional: the limits of the pool
minContainers: 0
maxContainers: 10
# optional: resources of each pool container
verticalAutoscaling:
minCpu: 1
maxCpu: 4
minRam: 0.5
maxRam: 8
minDisk: 1
maxDisk: 20
```

```sh
zcli project service-import zerops-import.yaml
```

The VM type takes fixed resource values in place of ranges, the same way the [Docker service](/docker/overview#scaling-operations) does:

```yaml title="zerops-import.yaml"
services:
- hostname: sandboxvm
type: swarm-vm@1
startWithoutCode: true
maxContainers: 5
verticalAutoscaling:
cpu: 2
ram: 4
disk: 20
```

To create a whole project with a Swarm service in it, add the `project:` section and use `zcli project project-import`. The [import reference](/references/import) describes both commands and every general parameter.

### Service parameters

<table className="w-full my-1.5">
<thead>
<tr>
<th className="w-fit" style={{ minWidth: '160px' }}>Parameter</th>
<th className="w-fit">Description</th>
</tr>
</thead>
<tbody>
<tr>
<td className="w-fit"><b>hostname</b></td>
<td className="w-fit">
The unique service identifier. Maximum 25 characters, lowercase ASCII letters (a-z) and numbers (0-9) only, unique within the project. <b>Fixed after creation.</b>
</td>
</tr>
<tr>
<td className="w-fit"><b>type</b></td>
<td className="w-fit">
<code>swarm@1</code> for a pool of containers, <code>swarm-vm@1</code> for a pool of virtual machines. <b>Fixed after creation.</b>
</td>
</tr>
<tr>
<td className="w-fit"><b>startWithoutCode</b></td>
<td className="w-fit">
<b>Optional.</b> Set to <code>true</code> to use the pool right away, with containers created from the plain base image. With the default, <code>false</code>, the service waits for your first <a href="/swarm/how-to/deploy">deploy</a> before the pool can be used.
</td>
</tr>
<tr>
<td className="w-fit"><b>minContainers</b></td>
<td className="w-fit">
<b>Optional.</b> How many containers the pool always keeps. Defaults to <code>0</code>, an empty pool. Zerops creates this many containers by itself, and the API refuses removals that would go below it.
</td>
</tr>
<tr>
<td className="w-fit"><b>maxContainers</b></td>
<td className="w-fit">
<b>Optional.</b> How many containers the pool can have at most. The API refuses to create more.
</td>
</tr>
<tr>
<td className="w-fit"><b>verticalAutoscaling</b></td>
<td className="w-fit">
<b>Optional.</b> Resources of each pool container. For <code>swarm@1</code> the usual ranges (<code>cpuMode</code>, <code>minCpu</code>/<code>maxCpu</code>, <code>minRam</code>/<code>maxRam</code>, <code>minDisk</code>/<code>maxDisk</code> and the other attributes described in <a href="/features/scaling">Scaling</a>). For <code>swarm-vm@1</code> the fixed values <code>cpu</code>, <code>ram</code> and <code>disk</code>, which default to 1 core, 1 GB and 5 GB.
</td>
</tr>
</tbody>
</table>

## Size the pool

`minContainers` and `maxContainers` mean something different here than in a runtime service. Zerops never adds or removes pool containers because of load. The two values are the limits your API calls work within:

- **Minimum**: Zerops keeps at least this many containers in the pool and creates them for you. With `0`, the default, the pool starts empty.
- **Maximum**: the most containers the pool can hold. Stopped containers count too. The limit is the same as for other runtime services, and we can raise it for your account on request.

:::note
The containers Zerops creates by itself to fill the minimum do not get the `zerops-primary` snapshot that containers created through the API have. A [reset](/swarm/how-to/use#clean-and-dirty-containers) of such a container, including acquire with `clean` and release with `reset`, fails because there is nothing to restore to. If you rely on resets, keep the minimum at `0` and create the warm containers with `POST /container`.
:::

A few things to consider when you choose them:

- Creating a container takes seconds, a VM considerably longer. If your work cannot wait for that, keep a minimum of warm containers and [acquire](/swarm/how-to/use#reserve-a-container) them, so a container is only created when all of them are taken.
- Pool containers can be stopped and started again through the API, and a stopped container keeps its disk. Acquire starts a stopped one on demand before it creates a new one.
- A [rollout](/swarm/how-to/deploy#roll-out-a-new-image) needs room to work: it cannot replace anything in a pool whose minimum equals its maximum.

Each pool container scales vertically on its own, the same way a container of any other runtime service does. VMs have fixed resources, and changing them restarts the VM.

You can read the current limits from the API with `GET /pool`.
91 changes: 91 additions & 0 deletions apps/docs/content/swarm/how-to/deploy.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
---
title: Custom image & rollout
description: Deploy a zerops.yaml to a Swarm service to prepare the image new pool containers boot from, and replace outdated containers with a rollout.
---

A Swarm service [started without code](/swarm/how-to/create#service-parameters) creates its pool containers from a plain image: Ubuntu 26.04 for `swarm@1`, the [Docker](/docker/overview) VM for `swarm-vm@1`. Usually you want more in there, like a runtime, your tools or your code. You get it by deploying to the Swarm service, the same way you deploy to any runtime service. Anything every pool container needs belongs in this image, not in snapshots you take afterwards.

## Deploy to a Swarm service

Describe the image in a `zerops.yaml` and push it with `zcli push`, from the GUI, or through the [GitHub](/references/github-integration) or [GitLab](/references/gitlab-integration) integration:

```yaml title="zerops.yaml"
zerops:
- setup: sandbox
build:
base: python@3.12
os: ubuntu
deployFiles: ./
run:
base: python@3.12
os: ubuntu
# installed once and stored in the image
prepareCommands:
- sudo apt-get update
- sudo apt-get install -y ripgrep jq
- pip install --no-cache-dir pytest ruff
```

The [build & deploy pipeline](/features/pipeline) works as usual. The build runs, `run.prepareCommands` customize the runtime image, and the result becomes the image of the service. `run.base` can be any runtime Zerops supports. The service stays a Swarm service whatever you deploy to it.

Inside a pool container everything behaves like in a normal runtime service: your deployed files are in `/var/www`, `run.envVariables` and the service's other variables are set, `run.initCommands` run when the container starts, and `run.ports` are opened. If you define `run.start`, it runs in every pool container. If you do not, nothing is started, which is what you want when the containers only wait for your `exec` calls.

:::note
For `swarm-vm@1` the base has to be a VM base, which today means `docker@26.1`. A container runtime cannot be deployed to a VM pool, and a VM base cannot be deployed to `swarm@1`. The deploy is refused with an error that names `run.base`.
:::

## What a deploy changes

This is where Swarm differs from a runtime service. A normal deploy replaces the running containers with new ones. In a Swarm pool the containers hold somebody's work, so **a deploy never touches existing containers**:

- Containers created after the deploy boot from the new image.
- Containers that already exist keep the image they were created with, together with their reservations, running commands and snapshots.
- A deploy does not create containers by itself. In a pool with a minimum of `0` the first deploy leaves the pool empty.

The API tells you which containers are behind. Every container has an `appVersionId`, the deploy it was created for, and `current`, which is `false` when a newer deploy exists:

```bash
curl -s http://orch.sandbox.zerops/container | jq '.[] | {name, current}'
```

Acquire and run hand out outdated containers like any other. If your consumers must not land on an old image, roll the new one out right after the deploy. A [fork](/swarm/how-to/use#fork-a-container) runs the image of its source.

## Roll out a new image

```bash
curl -s -X POST http://orch.sandbox.zerops/container/rollout
```

A rollout replaces every outdated container with a new one created from the current image. It works within the [pool limits](/swarm/how-to/create#size-the-pool), in rounds: it removes as many outdated containers as the pool can lose without going below its minimum, then creates as many replacements as fit under its maximum, and repeats until all are replaced. A pool that sits at its minimum starts with the creates.

What happens to a container depends on its state:

<table className="w-full my-1.5">
<thead>
<tr>
<th className="w-fit" style={{ minWidth: '190px' }}>Outdated container</th>
<th className="w-fit">What the rollout does</th>
</tr>
</thead>
<tbody>
<tr>
<td className="w-fit"><b>Free</b></td>
<td className="w-fit">Removed and replaced during the call.</td>
</tr>
<tr>
<td className="w-fit"><b>Reserved</b></td>
<td className="w-fit">Left alone and reported as skipped with <code>held-by-other</code>. With <code>force=true</code> and the <a href="/swarm/how-to/connect#tokens">admin token</a> it is replaced like a free one.</td>
</tr>
<tr>
<td className="w-fit"><b>Work in progress</b></td>
<td className="w-fit">Never interrupted. The container is marked, reported under <code>retiring</code>, and the orchestrator replaces it by itself once the command, restore, stop or start ends. No further call is needed.</td>
</tr>
</tbody>
</table>

The response lists what happened: `replaced` (ids of the removed containers), `created` (the new containers), `retiring`, `skipped` with a reason for each, and `createErrors` for replacements that could not be created. The call returns when its own removals and creates are done, which takes minutes for a VM pool, and it continues if your client disconnects.

Two limits to know about:

- A pool whose minimum equals its maximum cannot be rolled. There is no room to remove a container first or to create one first, so the call is refused with `pool-fixed-size`. Raise the maximum by one for the rollout.
- A replacement is a new container with a new id and hostname. The old container's snapshots and everything on its disk are gone.
Loading
Loading