Remote Backend Development¶
This page sets up a development shed-server that runs default_backend: remote on your machine, beside any installed shed-server. Each shed it creates is a virtual machine in your own Google Cloud project. For what the backend does and how clients see it, see Remote Backend (Cloud).
Every shed is a billed VM
A shed on this server is a Compute Engine VM with its own boot disk. It keeps running, and costing money, after you stop the dev server or close your terminal. Delete what you create and check that nothing is left.
Ports and paths¶
The remote dev server never touches an installed server's ports (8080 and 2222) or its state.
| Item | Value |
|---|---|
| HTTPS API (mTLS) | 127.0.0.1:18490 |
| SSH entry point | 127.0.0.1:12290 |
| HTTP port | 18090, declared only: an enforced auth mode starts no plain-HTTP listener |
| Rendered config | ~/.shed/dev-remote/server.yaml, from configs/server.dev-remote.yaml.tmpl |
| Log | ~/.shed/dev-remote/server.log |
| PID file | ~/.shed/dev-remote/server.pid |
| Record cache | ~/.shed/dev-remote/remote/ |
| Agent build | ~/.shed/dev-remote/agent/shed-agent-linux-amd64 |
| Server identity (host key, TLS pair, client CA) | ~/.shed/dev-remote/state/ |
| CLI entry | remote-dev |
One-time cloud setup¶
Use a Google Cloud project that runs nothing but sheds. scripts/remote/gce-setup.sh prints the gcloud commands for one environment and runs none of them:
scripts/remote/gce-setup.sh --print \
--project <project-id> \
--env <env-name> \
--region <region> \
--source-range <your-public-address>/32 \
--key-out ~/.config/shed/remote-dev/gce-key.b64
Use a documentation address such as 203.0.113.7/32 when you only want to read the output.
| Flag | Meaning |
|---|---|
--project |
The Google Cloud project ID. |
--env |
The environment name (remote.environment): 1 to 20 of a-z, 0-9 and -, starting with a letter. VMs, the network, subnet, network tag, firewall rule and service account are named shed-<env>.... |
--region |
The region of the zone the server uses. |
--source-range |
Your machine's public address as a /32: the only source the firewall rule admits on port 22. |
--subnet-range |
The subnet range (default 10.10.0.0/24). |
--role-id |
The custom role ID (default shedServer), shared by every environment in the project. |
--key-out |
Where the base64 service account key is written, mode 0600. The file must not exist yet: the printed block stops before making a key when it does. |
--firewall-auto |
Also print the two extra roles remote.firewall.mode: auto needs. Leave it out for static, the dev default. |
Read each printed block, then run it yourself. It enables the Compute Engine API and creates a custom-mode network and subnet, one firewall rule (tcp:22 from your address to the environment's tag), the custom role, a service account holding only that role, and its key. The printed text ends with the environment variables to set.
The server never creates or deletes the firewall rule, but it reads it. Unless the env file says otherwise, the dev config is rendered with firewall.mode: static and source_ranges_env: SHED_REMOTE_SOURCE_RANGES: the rule's source ranges must equal that variable, and while they do not, shed create and shed start answer 503 FIREWALL_CHECK_FAILED. When your public address changes, update both:
gcloud compute firewall-rules update shed-<env-name>-ssh --project <project-id> \
--source-ranges=<new-address>/32
# then set SHED_REMOTE_SOURCE_RANGES=<new-address>/32 in the env file and
make dev-server-restart-remote
If the address changes often, let the server follow it: run the block gce-setup.sh --print --firewall-auto adds (pass the --role-id your project's base role was made with, and check the printed service account is the one your key belongs to), then set two variables in the env file and restart:
SHED_REMOTE_FIREWALL_MODE=auto
SHED_REMOTE_ECHO_URLS=https://<first-echo-service>/,https://<second-echo-service>/
The config is then rendered with mode: auto and those echo_urls, and SHED_REMOTE_SOURCE_RANGES is no longer read. The start-up log line firewall check passed (mode auto, ...) shows the address the services agreed on. See The firewall check for every clause, the two extra roles and what each failure means.
Create the SSH key the server uses to log in to every VM:
mkdir -p ~/.config/shed/remote-dev && chmod 700 ~/.config/shed/remote-dev
ssh-keygen -t ed25519 -N '' -C shed-remote-link -f ~/.config/shed/remote-dev/link_ed25519
The private env file¶
The cloud settings live in a private env file, never in the repository or the config file. The Make targets source ~/.config/shed/remote-dev/env (override with SHED_REMOTE_ENV_FILE). It holds VAR=value lines:
SHED_REMOTE_ENV=<env-name>
SHED_GCE_PROJECT=<project-id>
SHED_GCE_ZONE=<zone>
SHED_GCE_NETWORK=shed-<env-name>
SHED_GCE_SUBNET=shed-<env-name>
SHED_GCE_TAG=shed-<env-name>
SHED_GCE_CREDENTIALS_FILE=<path to the key file>
SHED_REMOTE_SSH_KEY_FILE=<path to link_ed25519>
SHED_REMOTE_SOURCE_RANGES=<your-public-address>/32
| Variable | Read by | Meaning |
|---|---|---|
SHED_REMOTE_ENV |
dev-server-up-remote |
The environment name, rendered into remote.environment. |
SHED_GCE_PROJECT |
server, gce-image |
The project ID. |
SHED_GCE_ZONE |
server, gce-image |
The zone every VM is created in. |
SHED_GCE_NETWORK |
server, gce-image |
The environment's network. |
SHED_GCE_SUBNET |
server, gce-image |
The environment's subnet. |
SHED_GCE_TAG |
gce-image |
The network tag the firewall rule admits; the image bake gives it to its builder VM (SHED_GCE_BAKE_TAG overrides). |
SHED_GCE_CREDENTIALS_FILE |
server | Path to the service account key file: the JSON key or its base64, as --key-out writes it. GOOGLE_CREDENTIALS_BASE64 (the key itself) is the alternative; set exactly one. |
SHED_REMOTE_SSH_KEY_FILE |
server | Path to the SSH private key. SHED_REMOTE_SSH_KEY (base64 of the key) is the alternative; set exactly one. |
SHED_REMOTE_SOURCE_RANGES |
server | The firewall rule's source ranges, comma-separated IPv4 CIDRs: what you passed as --source-range. The server compares the rule with it (remote.firewall.source_ranges_env). Not read in auto mode. |
SHED_REMOTE_FIREWALL_MODE |
dev-server-up-remote |
Optional: static (the default) or auto, rendered into remote.firewall.mode. |
SHED_REMOTE_ECHO_URLS |
dev-server-up-remote |
For auto only: two or more https echo services on different hosts, comma-separated with no spaces, rendered into remote.firewall.echo_urls. |
Keep the file mode 0600. The server refuses to start, naming the variable, when one it needs is unset.
Baking an image¶
A remote shed boots from an image baked by scripts/build-gce-image.sh. The bake boots a builder VM from stock Ubuntu 24.04, provisions and verifies it, saves its disk as an image in the family shed-extensions-amd64, and deletes the builder.
set -a; . ~/.config/shed/remote-dev/env; set +a
make gce-image GCE_IMAGE_ARGS=--print # show every step, run nothing
make gce-image # bake (the builder VM is billed while it runs)
make test-gce-image # offline checks of the image tooling
The dev config boots the newest image of the family (default_image: family/shed-extensions-amd64, alias cloud). Rebake after any change to cmd/shed-agent or gce/: the server does not copy an agent into a VM (agent_install is not built), so a VM whose agent is incompatible fails its create with reason layout.
Running the dev server¶
make dev-server-up-remote # build, render the config, start
make dev-server-status-remote # running? reachable through the CLI?
make dev-server-logs-remote # follow the log
make build && make dev-server-restart-remote # pick up a rebuild
make dev-server-down-remote # stop the server; its sheds keep running
dev-server-up-remote refuses to start when the env file is missing or a dev server is already running. Like the other dev servers it runs under nohup with a PID file: no unit, no restart on crash, nothing that survives a reboot. Stopping the server does not stop or delete any VM; at its next start it lists the cloud and adopts what it finds (see Server restarts).
SHED_REMOTE_GITHUB_USER (a Make variable) names the GitHub account whose SSH keys may enrol a CLI. The rendered config also defines a nocturn service client; issue its bundle with:
Registering the CLI entry¶
Once, after the first start:
First contact is over SSH, so name the SSH port. The CLI enrols with the server's client CA and records an entry with the HTTPS API URL (port 18490), the pinned certificate fingerprint and a client certificate under ~/.shed/creds/remote-dev/, which it renews when it nears expiry. Then:
shed -s remote-dev create t1
shed -s remote-dev exec t1 -- uname -a
ssh t1@localhost -p 12290
shed -s remote-dev stop t1
shed -s remote-dev start t1
shed -s remote-dev delete t1 --force
Checking nothing is left¶
Before you stop for the day:
shed -s remote-dev list
gcloud compute instances list --project <project-id> --filter='labels.shed_env=<env-name>'
gcloud compute disks list --project <project-id> --filter='labels.shed_env=<env-name>'
Both gcloud lists must be empty once every shed is deleted. shed delete returns only when the VM is gone (about 30 s); a JSON DELETE through the API answers in about 3 s and the VM goes in the background, so it can show for a short while. A destroy that fails is retried by the server's periodic pass, which runs once a minute. A VM that should not exist can be deleted with gcloud compute instances delete; the server forgets its record at the next pass.
Every VM also carries a maximum run duration (the larger of remote.max_run_duration, 24 h in the dev config, and the owning client's max_age), after which the cloud stops it and the shed is listed stopped (run_limit). A stopped VM still bills for its disk. To see it happen, render the config with max_run_duration: 10m, the shortest the server accepts.
Integration tests¶
The integration suite's [remote] cases run only when SHED_REMOTE_SERVER names this entry:
make test-integration-remote # every [remote] case
make test-integration-remote PYTEST_ARGS="test_remote_refusals.py" # no VMs
The target is manual only and never starts the server for you. It sets every vz and fc server variable empty, so no other backend is touched. A full run creates about 25 VMs one after another, never more than remote.limits.max_running (2) at once, and takes about 42 minutes. Tests the backend cannot run (mounts, image store, guest MTU, vz/fc timing and image bakes) skip with a reason. See tests/integration/README.md for the list.
Measured timings¶
From three live runs on the dev project (e2-standard-2, 20 GB image, one zone). The cloud's own share varies from run to run: the provider phase of a create took between 12 and 35 s.
| Step | Seconds |
|---|---|
shed create (streamed) |
52 to 58 usually, 81 once: provider 12 to 35, boot 33 to 43, provision 0.1 to 0.5, ready 3 |
shed create --repo (a public repo) |
59.7: clone 1.4, provision 4.2 |
shed create --repo (a repository that does not exist) |
68: three clone attempts 11, then a warning and a running shed |
shed exec on a running shed |
0.22 |
shed stop (VM powered off) |
28 to 34 |
shed start (disk and host key kept) |
42 to 54 |
Create refused at max_running |
0.3, no VM created |
JSON create (POST /api/sheds), until 201 starting |
11 to 15 |
| Adopting a running VM after the record cache was removed | probe 0.6 |
shed delete (streamed) |
29 to 34, returns when the VM is gone |
JSON DELETE |
204 after about 3; VM gone after about 30 |
A VM stopped outside shed, until listed stopped (external) |
10 after the VM stopped (the pass runs once a minute) |
| A VM deleted outside shed, until its record is gone | 2 to 12 after the VM was gone |
make gce-image |
272 |
A create from an image with no shed agent, until error (create_failed) |
324 (the 5 minute wait for the agent); VM and disk gone 58 later |
A VM at its run limit (10 minutes), until listed stopped (run_limit) |
63 after the limit; shed start 44, with a new limit |
shed delete of a VM under deletion protection |
4, with an error; a create refused at max_running from 2 minutes on; VM gone 80 after the protection was cleared |
| Firewall rule changed by hand, until creates are refused | 29 to 53 (the next pass) |
| Auto mode: rule sources changed by hand, until patched back | 45 to 47 |