deployment/nomad in the repository of Stellar Control runs the whole stack on Nomad: NATS with
JetStream, the services of the core, the web editor, the simulated targets of the example
configuration (sim-1 and psu-sim-1) and, optionally, a Traefik reverse proxy. Every
component is a plain process (raw_exec driver): no container runtime is needed.
As shipped, it targets a single development host. The same jobs serve a shared platform, one
cell per tenant, provisioned by stellar-admin; a production platform runs its
cells in containers instead, behind Traefik.
Files#
| File | Content |
|---|---|
agent.hcl | Nomad agent: server and client in one process, on 127.0.0.1, raw_exec enabled |
stellar.yaml | Global configuration of the MCS components (NATS URL, API address, text logs, feeders) |
jobs/nats.nomad.hcl | Job nats: nats-server downloaded from its GitHub release, JetStream on disk |
jobs/proxy.nomad.hcl | Job proxy: Traefik terminating TLS in front of the API of each cell |
jobs/stellar.nomad.hcl | Job stellar: one group per component, the simulators, and a prestart task that publishes the configuration |
jobs/containers/cell.nomad.hcl | The same, in containers of the image stellar-mcs: the job of a cell of a production platform, routed by Traefik |
jobs/containers/nats.nomad.hcl | Job nats in a container of the official image |
jobs/containers/admin.nomad.hcl | Periodic job cells-admin: stellar-admin cell tick, the life cycle of the cells |
jobs/containers/onboarding.nomad.hcl | Job onboarding: the onboarding of the online trial (Production) |
jobs/containers/upgrade.nomad.hcl | Job cells-upgrade: the cells run again with the released images, one at a time (Production) |
policies/cells-admin.hcl | ACL policy of stellar-admin: the jobs, services and variables of the cells |
policies/traefik.hcl, policies/release.hcl | ACL policies of the Nomad provider of Traefik and of the releases |
agent-containers.hcl | Example of a Nomad agent of a platform in containers: Docker driver, private host network for NATS |
deploy.sh | Builds the release binaries and the web application, starts the agent, runs the jobs |
stop.sh | Stops the jobs and the agent; --purge also removes their state |
Deploy#
deployment/nomad/deploy.sh # cargo build --release, agent, nats, stellar
deployment/nomad/deploy.sh --no-build # binaries already builtThen:
nomad job status stellar # NOMAD_ADDR=http://127.0.0.1:4646, UI at /ui
nomad alloc logs -job stellar -task executor # logs of a component
stellar watch sim-1 --api http://127.0.0.1:8080
stellar send sim-1 'tcu[TCU1].ping' --api http://127.0.0.1:8080
stellar run examples/config/runs/hot-standby.yaml --api http://127.0.0.1:8080The web console is at http://127.0.0.1:8080 when core/frontend is built.
Stop with deployment/nomad/stop.sh; add --purge to start again from an empty JetStream.
Variables of deploy.sh#
| Variable | Default | Effect |
|---|---|---|
STELLAR_NOMAD_DATA | deployment/nomad/.data | Where the agent and JetStream keep their state (git-ignored) |
NATS_PORT, NATS_MONITORING_PORT, API_PORT | 4222, 8222, 8080 | Ports of the stack, when the defaults are taken (for instance by the NATS of compose.yaml) |
NATS_TLS | unset | 1: TLS required by NATS, with development certificates generated once in the data directory; mtls: client certificates required too |
NATS_ENCRYPT | unset | 1: JetStream encrypts its files, with a key generated once |
PROXY | unset | 1: Traefik in front of the API, HTTPS on https://localhost:8443 (HTTP on 8880 redirected) |
PROXY_HTTP_PORT, PROXY_HTTPS_PORT | 8880, 8443 | Ports of the proxy |
EDITOR | on | off: no web editor |
EDITOR_REPOSITORY | a bare mirror of the repository | Git remote of the configuration repository of the editor |
EDITOR_PATH | examples/config for the mirror, else . | Configuration repository within that Git repository |
NATS_PORT=14222 NATS_MONITORING_PORT=18222 API_PORT=18080 deployment/nomad/deploy.shHow it works#
- Configuration. Before the reconciler starts, the prestart task
publish-configrunsstellar compile examples/config --publish <nats>: every restart of the reconciler group publishes the repository as it is. After a change, restart it (nomad alloc restart <reconciler alloc>) or run the job again. - Order. Nomad does not order jobs:
deploy.shwaits for NATS before runningstellar. A component that cannot reach NATS retries its connection; a failed task restarts every 5 s. - Web application. The API serves
core/frontend/dist(api.web_dir) when it is built. - Editor.
stellar-editorlistens on127.0.0.1:8090; the API relays/v1/editorto it (api.editor_url). Its remote is by default a bare mirror of the repository,$STELLAR_NOMAD_DATA/config.git, whosemainfollows the repository at each deployment and where the draft branches are pushed. Its clone and working copies live in$STELLAR_NOMAD_DATA/editor. The forge comes fromeditor.forgeandSTELLAR_FORGE_TOKEN. - Metrics. Each component serves
/metricson its own dynamic port (STELLAR__OBSERVABILITY__METRICS_ADDR), shown bynomad alloc status. - Security. As shipped, development only: NATS without authentication, no signing key (no JWT issued), declared identities accepted, everything on the loopback interface.
Variables of the jobs#
nats
| Variable | Default | Meaning |
|---|---|---|
data_dir | — | JetStream storage, kept across restarts |
port, monitoring_port | 4222, 8222 | Client and monitoring ports |
tls_dir | empty (no TLS) | Directory of ca.pem, server.pem and server-key.pem |
tls_verify | false | Require client certificates signed by the CA (mTLS) |
leafnode_port | 0 (none) | Port of the leaf nodes of the benches, with TLS |
websocket_port | 0 (none) | NATS WebSocket for leaf nodes and clients behind firewalls (443 in production), with TLS |
resolver_conf | empty (no authentication) | Operator and resolver configuration written by stellar-admin operator init |
encrypt | false | JetStream encryption, key in the Nomad variable nomad/jobs/nats |
nats_version | 2.12.15 | Version of nats-server to download |
stellar
| Variable | Default | Meaning |
|---|---|---|
bin_dir | — | Release binaries (target/release) |
repository | — | Configuration repository published at start |
config_file | — | Global configuration |
nats_url | nats://127.0.0.1:4222 | NATS server |
nats_ca, nats_cert, nats_key | empty | TLS of the components: CA, and client certificate for mTLS |
secrets | false | Credentials, signing key and account from the Nomad variable secrets_path (items services_creds, bootstrap_creds, signing_key, account): a cell of stellar-admin |
secrets_path | nomad/jobs/stellar | That variable, readable by the job only: nomad/jobs/cell-<name> for the job cell-<name> of a cell |
all_in_one | false | The core in one process (stellar-mcs, group mcs) for a small cell |
counts | 1 per group | Instances of each group at deployment |
api_port | 8080 | Port of the API, on the loopback interface, shared by its instances |
web_dir | empty | Built web application |
editor_repository, editor_path, editor_workdir, editor_port | empty, ., /tmp/stellar-editor, 8090 | Web editor |
editor_forge | empty (that of config_file) | Forge of the editor: direct in a cell with a repository of its own |
proxy
| Variable | Default | Meaning |
|---|---|---|
data_dir | — | ACME account and certificates |
http_port, https_port | 80, 443 | HTTP (redirected) and HTTPS/WSS |
ping_port | 8082 | Health check of Traefik, on the loopback interface |
acme_email | empty | ACME contact; empty, certificates of tls_dir |
acme_server | Let's Encrypt | ACME directory (its staging directory for tries) |
Scaling#
Every group scales from 0 to its maximum with nomad job scale stellar <group> <n>; the
initial counts come from the counts variable.
| Group | Instances | How they share the work |
|---|---|---|
api | 0 to 3 | All active, sharing API_PORT (SO_REUSEPORT) |
executor | 0 to 3 | All active: runs spread by identifier, targets held by leases |
scheduler | 0 to 3 | All active: each run submitted once |
reconciler | 0 to 3 | One leader, the others stand by |
compute, alarms, transfers | 0 to 3 | One active, the others take over within leader_ttl |
editor | 0 to 1 | Drafts are working copies on the local disk |
simulators | 0 to 1 | Each simulator is one target |
At 0, a component stops: api at 0 closes the web application and the CLI. See
Running the Services.
A small cell (all_in_one=true) runs stellar-mcs in the group mcs beside its simulators:
about 55 MiB at rest instead of 210 MiB for the seven separate services (debug builds).
TLS#
NATS_TLS=1 deployment/nomad/deploy.sh # clients: tls://127.0.0.1:4222 and STELLAR_NATS_CA
NATS_TLS=mtls deployment/nomad/deploy.sh # and client certificatesThe development certificates come from dev/nats/tls.sh, generated once in the data
directory. A real deployment uses the certificates of its own CA: tls_dir and tls_verify
for the nats job, nats_ca, nats_cert and nats_key for the stellar job, and
leafnode_port, websocket_port for the benches.
Reverse proxy#
With PROXY=1, Traefik serves the API, the web application and their WebSockets over HTTPS.
The CLI checks its certificate with STELLAR_API_CA:
PROXY=1 deployment/nomad/deploy.sh
STELLAR_API_CA=deployment/nomad/.data/tls/ca.pem \
stellar send sim-1 'tcu[TCU1].ping' --api https://localhost:8443The proxy routes each host to the API of its cell from the Nomad variables
nomad/jobs/proxy/cells/<cell> (host, backend): adding, changing or deleting one updates the
routes within seconds, without redeploying the proxy. In production it listens on 80 and 443 and
gets its certificates from Let's Encrypt (acme_email); with Nomad ACLs, the task needs a policy
reading nomad/jobs/proxy/cells/*.
Secrets and encryption at rest#
No secret is written in a job or in the repository: each lives in a Nomad variable (encrypted at rest by Nomad, readable by the tasks of its job only) or with the operator of the platform.
| Secret | Where | Read by | Rotation |
|---|---|---|---|
| Operator key of NATS | operator.nk in the directory of stellar-admin, offline between provisionings | stellar-admin | A new operator: every account signed again |
Signing key of the account of a cell, credentials services and bootstrap | nomad/jobs/cell-<name> in the namespace of the cells | Its job cell-<name> (secrets=true) | Delete and create the cell again |
| Forge token of the editor | forge_token in the same variable (stellar-admin cell secret <cell> forge_token) | The editor | Set it again: the editor restarts with it |
| JetStream key | nomad/jobs/nats (jetstream_key), encrypt=true | The nats job | Set jetstream_prev_key to the old key and jetstream_key to the new one; drop jetstream_prev_key later |
| TLS keys | Files of tls_dir on the Nomad clients, or ACME | NATS, Traefik | A new certificate in place and a restart; ACME renews by itself |
With NATS_ENCRYPT=1, deploy.sh generates the JetStream key once (.data/jetstream.key) and
puts it in nomad/jobs/nats; NATS then encrypts its streams, KV buckets and object stores
(ChaCha20-Poly1305). See TLS, Encryption and Secrets.
A shared platform#
A shared platform hosts one cell per tenant: the nats job includes the resolver configuration
of the operator (resolver_conf), and each cell runs its own job cell-<name>, in the namespace
of the cells, with secrets=true. Cells describes how stellar-admin provisions
them.
Containers#
On a production platform the cells run in containers (Docker driver, no raw_exec) of the image
stellar-mcs (Container images), behind a Traefik that finds
them with its Nomad provider: stellar-admin runs jobs/containers/cell.nomad.hcl for each cell
(runtime: containers in platform.yaml). Its groups and their scaling are those of the
stellar job; what changes:
- Routes from tags. The service
cell-<name>-apicarries the router of Traefik: its host, entry point, TLS and certificate resolver. Traefik watches the namespace of the cells (providers.nomad.namespaces=[cells],exposedByDefault=false); a new cell is routed within its refresh interval, a deleted one disappears with its services. The proxy of the platform (jobs/proxy.nomad.hcl) stays for the cells as processes, which it reaches on the loopback interface. - Ports of their own. Each API instance listens on 8080 in its container, published on a
port chosen by Nomad; Traefik spreads the requests among them. The editor is found by the API
through its service
cell-<name>-editor. - Hardened containers. Read-only root file system, no capabilities,
no-new-privileges, the user of the image (10001); the secrets are files of that user, mode 0400. The only host path is the configuration repository of the cell, mounted read only (read and write for the editor): the Docker plugin needsvolumes { enabled = true }. - NATS off the loopback interface. The cells reach NATS at
nats_url: withjobs/containers/nats.nomad.hcl, its ports are published on the host networkhost_networkof the clients, a private one on a host with a public address (agent-containers.hcl).
| Variable | Default | Meaning |
|---|---|---|
image, tag | stellar-mcs, latest | Image of the components; an immutable tag in production (latest is pulled at every start) |
host | — | Host name of the cell, routed by Traefik |
repository | — | Configuration repository of the cell on the clients |
config | archive of 7 days, JSON logs | Global configuration of the components, in YAML |
nats_url, nats_ca | the address of the client, port 4222; empty | NATS server, reached from the containers; its CA |
secrets, secrets_path | true, nomad/jobs/stellar | As in the stellar job |
all_in_one, counts | false, 1 per group (0 for the editor) | As in the stellar job |
memory | {default = 128, mcs = 384, editor = 256, simulators = 64} | Memory of the containers in MB, by group |
editor_repository, editor_path | empty, . | Web editor: its Git remote as seen from its container (/srv/stellar/config, the repository of the cell, mounted read and write for it) |
editor_forge | empty (that of config) | Forge of the editor: direct publishes a proposed draft at once |
simulators | sim-1, psu-sim-1 | Simulated targets, one container each (simulation, target, gateway, driver) |
traefik_entrypoint | websecure | Entry point of Traefik |
traefik_certresolver | empty | Certificate resolver; empty, the default certificate of Traefik |
traefik_wildcard | empty | Wildcard domain of one certificate for every cell (*.cells.example.org) |
rootless | false | A Nomad client not run as root with a rootless engine (Podman, a development machine): the components run as root in their containers, without capabilities, to read their secrets |
Clients of cells in containers keep the images of the cells longer than a cell sleeps
(gc { image_delay = "168h" } of the Docker plugin, agent-containers.hcl): a cell woken up
starts at once rather than pulling its image again.
cells-admin (jobs/containers/admin.nomad.hcl), the life cycle of the cells:
| Variable | Default | Meaning |
|---|---|---|
image, tag | stellar-admin, latest | Image of stellar-admin |
admin_dir | — | Directory of the operator on the clients (secrets, platform.yaml, provisioning.yaml), mounted read only |
cells_dir | — | Directory of the cells, mounted at the same path: their repositories are deleted there |
cron | */5 * * * * | When stellar-admin cell tick runs |
user | 0 | User of the task: the owner of the directory of the operator |
selinux | false | On clients enforcing SELinux, the task runs without its label (label=disable) to reach the API socket of Nomad |
It reaches Nomad through the API socket of its task with the token of its workload identity;
with ACLs, bind the job to policies/cells-admin.hcl
(nomad acl policy apply -namespace default -job cells-admin cells-admin policies/cells-admin.hcl).