Add native Alloy configuration for isolated Icarus deployment
This commit is contained in:
4 files changed
+309
No files matched your search
@@ -15,3 +15,5 @@ __pycache__/
|
||||
*.env
|
||||
.env
|
||||
*.bak
|
||||
._*
|
||||
.DS_Store
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Deploy the opt-in native Alloy service using shared host/guest configs."""
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from engine.descriptor import Flag, Meta
|
||||
|
||||
METADATA = Meta(summary="Configure native Alloy from configs/alloy/config.json.",
|
||||
args=[Flag("--dry-run", "Validate the deployment plan without changes.")])
|
||||
|
||||
|
||||
def deployment(ctx):
|
||||
settings = Path(ctx.paths.configs) / "alloy/config.json"
|
||||
if not settings.exists():
|
||||
return None
|
||||
values = json.loads(settings.read_text())
|
||||
role = values.get("role")
|
||||
if role not in ("host", "guest"):
|
||||
raise ValueError("Alloy role must be host or guest")
|
||||
env_file = values.get("environment_file", "")
|
||||
if not re.fullmatch(r"/[A-Za-z0-9_./-]+", env_file):
|
||||
raise ValueError("Alloy environment_file must be an absolute path without whitespace")
|
||||
source = Path(ctx.paths.commons) / "etc/alloy" / (
|
||||
"native-host.alloy" if role == "host" else "native.alloy")
|
||||
config = source.read_text()
|
||||
extra = Path(ctx.paths.configs) / "alloy/extra.alloy"
|
||||
if extra.is_file():
|
||||
config += "\n" + extra.read_text()
|
||||
unit = ("[Service]\nEnvironmentFile=" + env_file + "\n"
|
||||
'ExecStart=\nExecStart=/usr/bin/alloy run --storage.path=/var/lib/alloy --server.http.listen-addr=127.0.0.1:12346 /etc/alloy/config.alloy\n'
|
||||
+ ("User=root\nGroup=root\n" if role == "guest" else "") +
|
||||
"SupplementaryGroups=systemd-journal" + (" docker" if role == "guest" else "") + "\n")
|
||||
return config, unit, env_file
|
||||
|
||||
|
||||
def run(ctx, args):
|
||||
plan = deployment(ctx)
|
||||
if plan is None:
|
||||
ctx.log.info("Native Alloy is not configured for this node; skipping.")
|
||||
return
|
||||
config, unit, env_file = plan
|
||||
if args.dry_run:
|
||||
ctx.log.info("Native Alloy deployment plan validated; no changes made.")
|
||||
return
|
||||
ctx.sys.sudo(["test", "-f", env_file])
|
||||
# Validate with the actual service environment without printing credentials.
|
||||
validator = '''import os, stat, subprocess, sys
|
||||
from pathlib import Path
|
||||
# Environment files deliberately use plain KEY=value, with no shell evaluation.
|
||||
secret = Path(sys.argv[1])
|
||||
info = secret.stat()
|
||||
if info.st_uid != 0 or stat.S_IMODE(info.st_mode) & 0o077:
|
||||
raise RuntimeError('Alloy environment file must be root-owned with mode 0600 or stricter')
|
||||
for line in secret.read_text().splitlines():
|
||||
if not line.strip() or line.lstrip().startswith('#'): continue
|
||||
key, value = line.split('=', 1)
|
||||
os.environ[key] = value
|
||||
subprocess.run(['alloy', 'validate', sys.argv[2]], check=True)
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix="apex-alloy-") as directory:
|
||||
config_path = Path(directory) / "config.alloy"
|
||||
unit_path = Path(directory) / "apex.conf"
|
||||
config_path.write_text(config)
|
||||
unit_path.write_text(unit)
|
||||
ctx.sys.sudo(["python3", "-c", validator, env_file, str(config_path)])
|
||||
ctx.sys.sudo(["install", "-d", "-m", "0755", "/etc/alloy",
|
||||
"/etc/systemd/system/alloy.service.d"])
|
||||
ctx.sys.sudo(["install", "-m", "0644", str(config_path), "/etc/alloy/config.alloy"])
|
||||
ctx.sys.sudo(["install", "-m", "0644", str(unit_path),
|
||||
"/etc/systemd/system/alloy.service.d/apex.conf"])
|
||||
ctx.sys.sudo(["systemctl", "unmask", "alloy.service"])
|
||||
ctx.sys.daemon_reload()
|
||||
ctx.sys.sudo(["systemctl", "enable", "alloy.service"])
|
||||
ctx.sys.sudo(["systemctl", "restart", "alloy.service"])
|
||||
ctx.sys.sudo(["systemctl", "is-active", "--quiet", "alloy.service"])
|
||||
ctx.log.success("Configured native Alloy.")
|
||||
@@ -0,0 +1,102 @@
|
||||
// Shared native Grafana Alloy configuration for the physical host.
|
||||
// Metrics and logs use the endpoints and credentials supplied through
|
||||
// APEX_PROM_PUSH_URL, APEX_LOKI_PUSH_URL, and APEX_OBS_BASIC_AUTH_USER/PASS.
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// METRICS
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Native filesystem views; physical capacity is collected on the Proxmox host.
|
||||
prometheus.exporter.unix "node" {
|
||||
enable_collectors = ["textfile"]
|
||||
textfile {
|
||||
directory = "/var/lib/alloy/textfile"
|
||||
}
|
||||
procfs_path = "/proc"
|
||||
sysfs_path = "/sys"
|
||||
rootfs_path = "/"
|
||||
}
|
||||
|
||||
// Container metrics (cadvisor-equivalent) via the docker socket.
|
||||
|
||||
|
||||
// Stamp a static `node` label onto each exporter's targets.
|
||||
discovery.relabel "node_metrics" {
|
||||
targets = prometheus.exporter.unix.node.targets
|
||||
rule {
|
||||
action = "replace"
|
||||
target_label = "node"
|
||||
replacement = sys.env("APEX_NODE_HOST")
|
||||
}
|
||||
// Stamp a stable `instance` (the node's public host) instead of the default,
|
||||
// which is the Alloy container id/address.
|
||||
rule {
|
||||
action = "replace"
|
||||
target_label = "instance"
|
||||
replacement = sys.env("APEX_NODE_HOST")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
prometheus.scrape "node" {
|
||||
targets = discovery.relabel.node_metrics.output
|
||||
forward_to = [prometheus.remote_write.default.receiver]
|
||||
scrape_interval = "30s"
|
||||
}
|
||||
|
||||
|
||||
// The configured Prometheus-compatible endpoint must accept remote writes.
|
||||
prometheus.remote_write "default" {
|
||||
endpoint {
|
||||
url = sys.env("APEX_PROM_PUSH_URL")
|
||||
basic_auth {
|
||||
username = sys.env("APEX_OBS_BASIC_AUTH_USER")
|
||||
password = sys.env("APEX_OBS_BASIC_AUTH_PASS")
|
||||
}
|
||||
queue_config {
|
||||
capacity = 5000
|
||||
max_shards = 10
|
||||
min_shards = 1
|
||||
max_samples_per_send = 1000
|
||||
batch_send_deadline = "5s"
|
||||
min_backoff = "100ms"
|
||||
max_backoff = "10s"
|
||||
}
|
||||
}
|
||||
wal {
|
||||
truncate_frequency = "30m"
|
||||
min_keepalive_time = "5m"
|
||||
max_keepalive_time = "30m"
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// LOGS
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// systemd journal (host services, ufw, sshd, crowdsec, etc.).
|
||||
loki.source.journal "systemd" {
|
||||
path = "/var/log/journal"
|
||||
max_age = "12h"
|
||||
labels = {
|
||||
node = sys.env("APEX_NODE_HOST"),
|
||||
job = "systemd-journal",
|
||||
}
|
||||
forward_to = [loki.write.default.receiver]
|
||||
}
|
||||
|
||||
// All docker containers' stdout/stderr.
|
||||
|
||||
|
||||
|
||||
|
||||
// Push logs to the central Loki.
|
||||
loki.write "default" {
|
||||
endpoint {
|
||||
url = sys.env("APEX_LOKI_PUSH_URL")
|
||||
basic_auth {
|
||||
username = sys.env("APEX_OBS_BASIC_AUTH_USER")
|
||||
password = sys.env("APEX_OBS_BASIC_AUTH_PASS")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Shared native Grafana Alloy configuration for Docker guests.
|
||||
// Metrics and logs use the endpoints and credentials supplied through
|
||||
// APEX_PROM_PUSH_URL, APEX_LOKI_PUSH_URL, and APEX_OBS_BASIC_AUTH_USER/PASS.
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// METRICS
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// Native filesystem views; physical capacity is collected on the Proxmox host.
|
||||
|
||||
|
||||
// Container metrics (cadvisor-equivalent) via the docker socket.
|
||||
prometheus.exporter.cadvisor "containers" {
|
||||
docker_host = "unix:///var/run/docker.sock"
|
||||
store_container_labels = false
|
||||
allowlisted_container_labels = [
|
||||
"com.docker.compose.project",
|
||||
"com.docker.compose.service",
|
||||
]
|
||||
docker_only = true
|
||||
enabled_metrics = [
|
||||
"cpu",
|
||||
"cpuLoad",
|
||||
"diskIO",
|
||||
"memory",
|
||||
"network",
|
||||
"oom_event",
|
||||
]
|
||||
}
|
||||
|
||||
// Stamp a static `node` label onto each exporter's targets.
|
||||
|
||||
discovery.relabel "container_metrics" {
|
||||
targets = prometheus.exporter.cadvisor.containers.targets
|
||||
rule {
|
||||
action = "replace"
|
||||
target_label = "node"
|
||||
replacement = sys.env("APEX_NODE_HOST")
|
||||
}
|
||||
rule {
|
||||
action = "replace"
|
||||
target_label = "instance"
|
||||
replacement = sys.env("APEX_NODE_HOST")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
prometheus.scrape "containers" {
|
||||
targets = discovery.relabel.container_metrics.output
|
||||
forward_to = [prometheus.remote_write.default.receiver]
|
||||
scrape_interval = "30s"
|
||||
}
|
||||
|
||||
// The configured Prometheus-compatible endpoint must accept remote writes.
|
||||
prometheus.remote_write "default" {
|
||||
endpoint {
|
||||
url = sys.env("APEX_PROM_PUSH_URL")
|
||||
basic_auth {
|
||||
username = sys.env("APEX_OBS_BASIC_AUTH_USER")
|
||||
password = sys.env("APEX_OBS_BASIC_AUTH_PASS")
|
||||
}
|
||||
queue_config {
|
||||
capacity = 5000
|
||||
max_shards = 10
|
||||
min_shards = 1
|
||||
max_samples_per_send = 1000
|
||||
batch_send_deadline = "5s"
|
||||
min_backoff = "100ms"
|
||||
max_backoff = "10s"
|
||||
}
|
||||
}
|
||||
wal {
|
||||
truncate_frequency = "30m"
|
||||
min_keepalive_time = "5m"
|
||||
max_keepalive_time = "30m"
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// LOGS
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
// systemd journal (host services, ufw, sshd, crowdsec, etc.).
|
||||
loki.source.journal "systemd" {
|
||||
path = "/var/log/journal"
|
||||
max_age = "12h"
|
||||
labels = {
|
||||
node = sys.env("APEX_NODE_HOST"),
|
||||
job = "systemd-journal",
|
||||
}
|
||||
forward_to = [loki.write.default.receiver]
|
||||
}
|
||||
|
||||
// All docker containers' stdout/stderr.
|
||||
discovery.docker "containers" {
|
||||
host = "unix:///var/run/docker.sock"
|
||||
}
|
||||
discovery.relabel "container_logs" {
|
||||
targets = discovery.docker.containers.targets
|
||||
rule {
|
||||
action = "replace"
|
||||
source_labels = ["__meta_docker_container_name"]
|
||||
regex = "/?(.*)"
|
||||
target_label = "container"
|
||||
}
|
||||
rule {
|
||||
action = "replace"
|
||||
target_label = "node"
|
||||
replacement = sys.env("APEX_NODE_HOST")
|
||||
}
|
||||
}
|
||||
loki.source.docker "containers" {
|
||||
host = "unix:///var/run/docker.sock"
|
||||
targets = discovery.relabel.container_logs.output
|
||||
relabel_rules = discovery.relabel.container_logs.rules
|
||||
forward_to = [loki.write.default.receiver]
|
||||
}
|
||||
|
||||
// Push logs to the central Loki.
|
||||
loki.write "default" {
|
||||
endpoint {
|
||||
url = sys.env("APEX_LOKI_PUSH_URL")
|
||||
basic_auth {
|
||||
username = sys.env("APEX_OBS_BASIC_AUTH_USER")
|
||||
password = sys.env("APEX_OBS_BASIC_AUTH_PASS")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user