Stellar ControlMission control · by Stellar Systems v0.1.0

Deployment

Nomad

The whole stack on HashiCorp Nomad, as processes or in containers: jobs, deployment script, TLS, reverse proxy, secrets, encryption at rest and scaling.

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#

FileContent
agent.hclNomad agent: server and client in one process, on 127.0.0.1, raw_exec enabled
stellar.yamlGlobal configuration of the MCS components (NATS URL, API address, text logs, feeders)
jobs/nats.nomad.hclJob nats: nats-server downloaded from its GitHub release, JetStream on disk
jobs/proxy.nomad.hclJob proxy: Traefik terminating TLS in front of the API of each cell
jobs/stellar.nomad.hclJob stellar: one group per component, the simulators, and a prestart task that publishes the configuration
jobs/containers/cell.nomad.hclThe same, in containers of the image stellar-mcs: the job of a cell of a production platform, routed by Traefik
jobs/containers/nats.nomad.hclJob nats in a container of the official image
jobs/containers/admin.nomad.hclPeriodic job cells-admin: stellar-admin cell tick, the life cycle of the cells
jobs/containers/onboarding.nomad.hclJob onboarding: the onboarding of the online trial (Production)
jobs/containers/upgrade.nomad.hclJob cells-upgrade: the cells run again with the released images, one at a time (Production)
policies/cells-admin.hclACL policy of stellar-admin: the jobs, services and variables of the cells
policies/traefik.hcl, policies/release.hclACL policies of the Nomad provider of Traefik and of the releases
agent-containers.hclExample of a Nomad agent of a platform in containers: Docker driver, private host network for NATS
deploy.shBuilds the release binaries and the web application, starts the agent, runs the jobs
stop.shStops the jobs and the agent; --purge also removes their state

Deploy#

Shell
deployment/nomad/deploy.sh            # cargo build --release, agent, nats, stellar
deployment/nomad/deploy.sh --no-build # binaries already built

Then:

Shell
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:8080

The 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#

VariableDefaultEffect
STELLAR_NOMAD_DATAdeployment/nomad/.dataWhere the agent and JetStream keep their state (git-ignored)
NATS_PORT, NATS_MONITORING_PORT, API_PORT4222, 8222, 8080Ports of the stack, when the defaults are taken (for instance by the NATS of compose.yaml)
NATS_TLSunset1: TLS required by NATS, with development certificates generated once in the data directory; mtls: client certificates required too
NATS_ENCRYPTunset1: JetStream encrypts its files, with a key generated once
PROXYunset1: Traefik in front of the API, HTTPS on https://localhost:8443 (HTTP on 8880 redirected)
PROXY_HTTP_PORT, PROXY_HTTPS_PORT8880, 8443Ports of the proxy
EDITORonoff: no web editor
EDITOR_REPOSITORYa bare mirror of the repositoryGit remote of the configuration repository of the editor
EDITOR_PATHexamples/config for the mirror, else .Configuration repository within that Git repository
Shell
NATS_PORT=14222 NATS_MONITORING_PORT=18222 API_PORT=18080 deployment/nomad/deploy.sh

How it works#

  • Configuration. Before the reconciler starts, the prestart task publish-config runs stellar 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.sh waits for NATS before running stellar. 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-editor listens on 127.0.0.1:8090; the API relays /v1/editor to it (api.editor_url). Its remote is by default a bare mirror of the repository, $STELLAR_NOMAD_DATA/config.git, whose main follows 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 from editor.forge and STELLAR_FORGE_TOKEN.
  • Metrics. Each component serves /metrics on its own dynamic port (STELLAR__OBSERVABILITY__METRICS_ADDR), shown by nomad 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

VariableDefaultMeaning
data_dir—JetStream storage, kept across restarts
port, monitoring_port4222, 8222Client and monitoring ports
tls_dirempty (no TLS)Directory of ca.pem, server.pem and server-key.pem
tls_verifyfalseRequire client certificates signed by the CA (mTLS)
leafnode_port0 (none)Port of the leaf nodes of the benches, with TLS
websocket_port0 (none)NATS WebSocket for leaf nodes and clients behind firewalls (443 in production), with TLS
resolver_confempty (no authentication)Operator and resolver configuration written by stellar-admin operator init
encryptfalseJetStream encryption, key in the Nomad variable nomad/jobs/nats
nats_version2.12.15Version of nats-server to download

stellar

VariableDefaultMeaning
bin_dir—Release binaries (target/release)
repository—Configuration repository published at start
config_file—Global configuration
nats_urlnats://127.0.0.1:4222NATS server
nats_ca, nats_cert, nats_keyemptyTLS of the components: CA, and client certificate for mTLS
secretsfalseCredentials, signing key and account from the Nomad variable secrets_path (items services_creds, bootstrap_creds, signing_key, account): a cell of stellar-admin
secrets_pathnomad/jobs/stellarThat variable, readable by the job only: nomad/jobs/cell-<name> for the job cell-<name> of a cell
all_in_onefalseThe core in one process (stellar-mcs, group mcs) for a small cell
counts1 per groupInstances of each group at deployment
api_port8080Port of the API, on the loopback interface, shared by its instances
web_diremptyBuilt web application
editor_repository, editor_path, editor_workdir, editor_portempty, ., /tmp/stellar-editor, 8090Web editor
editor_forgeempty (that of config_file)Forge of the editor: direct in a cell with a repository of its own

proxy

VariableDefaultMeaning
data_dir—ACME account and certificates
http_port, https_port80, 443HTTP (redirected) and HTTPS/WSS
ping_port8082Health check of Traefik, on the loopback interface
acme_emailemptyACME contact; empty, certificates of tls_dir
acme_serverLet's EncryptACME 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.

GroupInstancesHow they share the work
api0 to 3All active, sharing API_PORT (SO_REUSEPORT)
executor0 to 3All active: runs spread by identifier, targets held by leases
scheduler0 to 3All active: each run submitted once
reconciler0 to 3One leader, the others stand by
compute, alarms, transfers0 to 3One active, the others take over within leader_ttl
editor0 to 1Drafts are working copies on the local disk
simulators0 to 1Each 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#

Shell
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 certificates

The 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:

Shell
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:8443

The 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.

SecretWhereRead byRotation
Operator key of NATSoperator.nk in the directory of stellar-admin, offline between provisioningsstellar-adminA new operator: every account signed again
Signing key of the account of a cell, credentials services and bootstrapnomad/jobs/cell-<name> in the namespace of the cellsIts job cell-<name> (secrets=true)Delete and create the cell again
Forge token of the editorforge_token in the same variable (stellar-admin cell secret <cell> forge_token)The editorSet it again: the editor restarts with it
JetStream keynomad/jobs/nats (jetstream_key), encrypt=trueThe nats jobSet jetstream_prev_key to the old key and jetstream_key to the new one; drop jetstream_prev_key later
TLS keysFiles of tls_dir on the Nomad clients, or ACMENATS, TraefikA 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>-api carries 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 needs volumes { enabled = true }.
  • NATS off the loopback interface. The cells reach NATS at nats_url: with jobs/containers/nats.nomad.hcl, its ports are published on the host network host_network of the clients, a private one on a host with a public address (agent-containers.hcl).
VariableDefaultMeaning
image, tagstellar-mcs, latestImage 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
configarchive of 7 days, JSON logsGlobal configuration of the components, in YAML
nats_url, nats_cathe address of the client, port 4222; emptyNATS server, reached from the containers; its CA
secrets, secrets_pathtrue, nomad/jobs/stellarAs in the stellar job
all_in_one, countsfalse, 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_pathempty, .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_forgeempty (that of config)Forge of the editor: direct publishes a proposed draft at once
simulatorssim-1, psu-sim-1Simulated targets, one container each (simulation, target, gateway, driver)
traefik_entrypointwebsecureEntry point of Traefik
traefik_certresolveremptyCertificate resolver; empty, the default certificate of Traefik
traefik_wildcardemptyWildcard domain of one certificate for every cell (*.cells.example.org)
rootlessfalseA 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:

VariableDefaultMeaning
image, tagstellar-admin, latestImage 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
user0User of the task: the owner of the directory of the operator
selinuxfalseOn 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).

Stellar Control · v0.1.0

↑↓ to moveEnter to open