Deploy Alloy with one script; split the agent config by where it can run (#88) - #240
Conversation
…config by where it can run (#88) `oracle` was found running Alloy v1.18.1 as --privileged with a config from the day it was copied — no self-scrape, so `oracle-alloy` never existed — and no volume for its WAL. Every one of those was the runbook's `docker run` drifting from compose.yaml, and the runbook's own check could not fail: Alloy logs to stderr and the grep read stdout. scripts/deploy-agent.sh replaces the recipe. The image and version come from compose.yaml via image-for.sh, the flags are the `alloy` service written out, the config lives in a named volume so the docker group needs no sudo and a stale bind mount cannot happen, and the gate is ten seconds up followed by a sixty-second window with no unexpected level=error — read from stderr. A second runtime installs the release .deb matching the same tag, for Saruman: a Proxmox hypervisor should not run Docker, and its firewall is what ADR-0014 relies on. The native runtime is why alloy/config.alloy became a directory. Alloy loads every *.alloy in it as one config, so docker.alloy (cAdvisor, container logs) ships only where there is a socket and syslog.alloy (the listener morpheus sends to) stays on the monitoring host — on the hypervisor it would have been an open UDP port on the lab segment. The node exporter's rootfs prefix is now ALLOY_ROOTFS, `/` natively. compose.yaml mounts the directory; validate.sh, ci.yml and check_dashboards.py read every file. Saruman needed a decision before a deploy: ADR-0007 says it does not remote-write to Winterfell, ADR-0012 and this issue assumed it does. Resolved for the hypervisor's own telemetry over one unlogged pass above the ADR-0014 tripwire; ADR-0007 and ADR-0013 carry a note, network.md the rule, the runbook the placement. The rule and the run are the operator's, from Hicks — 99 → 30 is closed, as ADR-0013 records. Docs now agree on three agents, and check_docs.py guards the count against the architecture table. oracle was redeployed with the script: v1.19.2, no capabilities, cgroupns host, three jobs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The new deploy script’s Prometheus arrival-check loop can terminate early under set -euo pipefail when the query initially returns no matching jobs, preventing the intended retry behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR standardizes deployment of the Grafana Alloy agent across hosts by introducing a single convergent deployment script, and by splitting the Alloy config into multiple files that are loaded as a directory depending on where they can run (base vs Docker socket vs monitoring-host-only syslog listener). It also updates CI/validation and docs to match the new deployment and configuration model.
Changes:
- Added
scripts/deploy-agent.sh(+make deploy-agent) to deploy/redeploy Alloy on remote hosts using either a Docker container (compose-equivalent hardening) or the native.debmatching the pinned compose version. - Converted
stacks/observability/alloy/into a multi-file directory config (config.alloy,docker.alloy,syslog.alloy) and updated compose + CI/validation tooling to format-check all*.alloyfiles. - Updated runbooks and documentation consistency checks (including a new doc guard for the “Alloy agents” count).
File summaries
| File | Description |
|---|---|
| stacks/observability/README.md | Updates stack file map to describe Alloy as a directory config with per-environment files. |
| stacks/observability/prometheus/rules/stack.rules.yaml | Clarifies alert-rule commentary around job counting and historical agent job shape. |
| stacks/observability/compose.yaml | Switches Alloy to mount /etc/alloy as a directory and run Alloy against the directory path. |
| stacks/observability/alloy/syslog.alloy | Adds monitoring-host-only network syslog receiver pipeline and relabeling. |
| stacks/observability/alloy/docker.alloy | Adds Docker-socket-dependent metrics/log collection components (cAdvisor + docker logs). |
| stacks/observability/alloy/config.alloy | Refactors “common” agent config to be directory-loaded and adds ALLOY_ROOTFS support for native installs. |
| scripts/validate.sh | Extends local validation to run alloy fmt --test over all *.alloy files. |
| scripts/deploy-agent.sh | Adds new convergent remote deploy script for Docker/native runtimes with gating and arrival verification. |
| scripts/check_docs.py | Adds doc assertion for Alloy agent count derived from the architecture table. |
| scripts/check_dashboards.py | Updates level-vocabulary extraction to read all alloy/*.alloy files rather than a single file. |
| README.md | Updates repository tree description to reflect Alloy as a directory config. |
| Makefile | Adds deploy-agent target to invoke the new deployment script. |
| docs/security.md | Updates security posture description to include deploy script parity and native runtime behavior. |
| docs/runbooks/ship-firewall-logs.md | Updates syslog listener documentation to reflect syslog.alloy and directory-loading behavior. |
| docs/runbooks/schedule-maintenance.md | Updates troubleshooting note to reference ALLOY_ROOTFS for textfile collector path correctness. |
| docs/runbooks/add-monitored-device.md | Rewrites host onboarding around deploy-agent.sh (Docker and native package paths). |
| docs/roadmap.md | Moves #88 to Done and documents what was actually fixed/found. |
| docs/observability.md | Updates observability doc to reflect current pushed job counts and remote_write behavior. |
| docs/network.md | Documents Saruman’s limited cross-segment pass for its host telemetry. |
| docs/hardware.md | Updates hardware capacity statement to reflect three Alloy agents. |
| docs/architecture.md | Updates architecture diagram and host table to show agents and syslog flow. |
| docs/adr/0013-segment-access-as-implemented.md | Adds NOTE about the new cross-segment rule for Saruman’s agent. |
| docs/adr/0007-defensive-estate-and-offensive-range.md | Adds NOTE amending telemetry decision for Saruman’s host agent only. |
| .github/workflows/ci.yml | Updates CI to run alloy fmt --test across all *.alloy files. |
Review details
- Files reviewed: 24/24 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| while ((SECONDS < deadline)); do | ||
| jobs="$(curl -fsS --max-time 10 "http://${MON}:9090/api/v1/query" --data-urlencode "query=${promql}" 2>/dev/null \ | ||
| | grep -o '"job":"[^"]*"' | sort -u | wc -l | tr -d ' ')" | ||
| if curl -fsS --max-time 10 "http://${MON}:3100/loki/api/v1/label/host/values" 2>/dev/null | grep -q "\"${HOST}\""; then | ||
| in_loki=1 |
What changed
scripts/deploy-agent.sh(andmake deploy-agent): one command that converges any host on this checkout's Alloy — image and version fromcompose.yaml, thealloyservice's hardening written out asdocker run, or the release.debmatching the same tag for a host that should not run Docker. Config shipped over ssh and checksummed; gate is ten seconds up, then a sixty-second window with no unexpectedlevel=error, then Prometheus and Loki asked whether the host arrived.stacks/observability/alloy/is now a directory Alloy loads as one config:config.alloy(every host),docker.alloy(hosts with a Docker socket),syslog.alloy(monitoring host only — the listener morpheus sends to).compose.yamlmounts the directory; validate, CI andcheck_dashboards.pyread every file. Node exporter's rootfs prefix isALLOY_ROOTFS(default unchanged).docs/runbooks/add-monitored-device.mdrewritten around the script, both runtimes. Its previousdocker runwas still--privilegedand its verification grep could never fail (Alloy logs to stderr).hardware.mdthree agents, guarded by a newcheck_docs.pyclaim against the architecture table;observability.mdno longer hard-codes the pushed-job count;architecture.mddiagram and host table show the agents;network.md,security.md, stack README, two other runbooks.Why
Closes #88. The issue was stale on arrival:
oraclehas reported since 2026-08-30. What was actually wrong is in the commit message and the roadmap entry —oraclewas a version behind, privileged, on a config with no self-scrape and no WAL volume, deployed from a runbook whose check passed for the wrong reason.Sarumanneeded a decision (ADR-0007 vs ADR-0012) more than a deploy.Blast radius
Monitoring host: the
alloyservice definition changed (directory mount, directory argument), so the nextmake upfrom the main checkout recreates it andreload-config.shrestarts it once more. Seconds of gap, WAL-backed.syslog.alloymust load or morpheus's logs stop —{host="morpheus"}is the check.oracle: container replaced (done, below). Gainedalloy-configandalloy-datavolumes;/opt/alloy/config.alloyleft on disk, unused.Saruman: new package, new/etc/alloy,/etc/default/alloy,alloyuser insystemd-journalandadm. Not yet run — see below.No change to network segmentation or firewall rules — one new pass is required and is the operator's to add: on the ImaginationLAN interface, TCP
10.0.30.110 → 10.0.99.20ports 9090, 3100, not logged, above the ADR-0014 tripwire. The runbook has the table and the reason for the placement.No new port published to a VLAN that could not already reach the service
No credential added outside
secrets/*.sops.yamlVerification
make validateandmake lintpass in the worktree. The new agent-count guard was shown to fail on the stale "two" and pass on "three".The config directory was formatted with
alloy fmt -wand run for 20 s in a throwaway container from the pinned image: parsed, graph evaluated, only the expected socket/journal/textfile errors. (That run resolvedprometheusthrough the host's DNS and pushed fourinstance="smoke"series into the live Prometheus; there is no admin API to delete them, soRemoteWriteJobStale{instance="smoke"}is silenced for 25 h and the ship-firewall-logs runbook now says to point the URLs at127.0.0.1:1for such runs.)oracleredeployed from this worktree with./scripts/deploy-agent.sh atropos@10.0.99.30:Confirmed from Prometheus:
oracle-metrics,oracle-alloy,integrations/cadvisorallup,node_textfile_scrape_error0, three containers seen,alloy_build_infov1.19.2.docker inspect:CapDrop=[ALL] GroupAdd=[4 473] Cgroup=host Pids=1024 Init=true, no/var/lib/docker/containersmount, stage directory cleaned.Found on the way, not fixed here: every Docker-host agent has logged cAdvisor's
rootDiskErrevery few minutes since Reduce Alloy's privilege — it is root on the host and can read the age key #188 (the monitoring host: 83 in six hours). Partial —container_fs_usage_bytesstill exists — and nothing charted depends on that walk. The gate ignores exactly that line and node_exporter's one-time udev line, and prints anything else.After merge
git pull && make up, then confirmup{job="alloy"},up{job="prometheus-alloy"}and{host="morpheus"}still flowing../scripts/deploy-agent.sh --runtime native root@10.0.30.110. The label will be whateverhostnameprints there (probablysaruman); if the docs'Sarumanis wrong, fix the docs. Then check{app="filterlog", interface=~"vlan30.*"}shows no pushes logged on the tripwire.🤖 Generated with Claude Code