Kromgo
A metric, a query, a badge — without opening the metrics API to the world.
Kromgo is a small Go service from the home-operations organisation that runs a fixed set of PromQL queries and renders each result as a shields-style badge, a JSON endpoint or a graph image. The queries live in its config file, not in the URL, so a public badge never lets a visitor run arbitrary queries against the metrics store. Here it sits in front of the VictoriaMetrics single node of the monitoring stack, which serves the Prometheus query API natively, and turns cluster utilisation, inventory and health into badges for READMEs and status pages.
Alternatives considered
Cloud Hosted
| Tool | Open Source | Free Tier | Monthly Cost |
|---|---|---|---|
| Shields.io endpoint badges | Yes | Yes | Free; needs a public JSON endpoint per badge, which is what kromgo provides anyway |
Self Hosted
| Tool | Open Source | Full Features | Notes |
|---|---|---|---|
| Grafana shared panels | Yes | Partly | Renders panels, not badges, and would expose a Grafana login or a public dashboard |
| Gatus badges | Yes | No | Uptime and response-time badges only, nothing from PromQL |
Installation
Architecture
- Deployment: Single
kromgodeployment in thekromgonamespace, one replica - Image:
ghcr.io/home-operations/kromgo, digest-pinned, ondistroless/static:nonroot - Data source:
KROMGO_PROMETHEUS_URLpoints atvmsingle-vm-stack.monitoring.svc:8428; no adapter is needed because VictoriaMetrics answers the Prometheus/api/v1/queryAPI - Configuration:
config/config.yamlfrom the talos overlay, generated into the hashedkromgo-configConfigMap and mounted read-only at/config; a response cache of five minutes keeps repeated badge loads off the metrics store - Networking: ClusterIP service on port 8080, HTTPRoute via the public gateway on
kromgo.web.kueber.eu; a second listener on port 8081 serves kromgo's own request and query metrics, and theprometheus.io/scrapeannotations on the Service hand it to the annotation-based vmagent job - Priority:
tier-3with requests of 25m/64Mi and a 128Mi memory limit
Security
- Runs as uid 65532,
runAsNonRoot: true, read-only root filesystem, all capabilities dropped,RuntimeDefaultseccomp, no service account token - No authentication on the badges: they are meant to be embedded publicly, and the query set is fixed by the config file, so the exposure is exactly the values the badges show
- Network contract from the app-network-policy component: ingress only from the public gateway's Envoy pods; egress only to the
vmsinglepods on 8428, which the monitoring namespace mirrors with anallow-vmsingle-kromgoingress rule, the only cross-namespace reader of the metrics store. No internet egress - The image is not attested upstream, which is why the platform's cosign policies skip it; see image supply chain
Updates
Managed by Renovate; the image is digest-pinned. Config edits change the ConfigMap hash and roll the pod, so a broken badge definition shows up as a failing rollout rather than a silently empty badge.
Configuration Management
Badges and graphs are defined in config/config.yaml next to the overlay, as PromQL against this cluster's exporters (node-exporter, kube-state-metrics, etcd). Each entry pairs a query with CEL expressions: valueExpr formats the sample or a label into the badge text (humanizeBytes, humanizeDuration, string(int(result))), colorExpr picks the colour, usually with colorScale thresholds. Instant badges cover CPU, memory, network receive rate, node and pod counts, the Kubernetes version read from a kube_node_info label, restarts over 24 hours, node uptime and etcd database size; a range badge averages CPU over seven days, and four graphs (CPU, memory, running pods, network RX) use the 30-day default window. CEL ternaries must be YAML-quoted because the : inside reads as a mapping.
Usage
Embed a badge with its id as the path on kromgo.web.kueber.eu; the same id is available as JSON for other consumers, and the graph ids render as images. Adding a badge is a new entry in config/config.yaml: pick a query that returns a single sample (or reduce a range), write the value and colour expressions, and let the ConfigMap hash roll the pod.
Cluster-specific deviations from the above live in the per-cluster README — see k8s/apps/kromgo/talos/README.md.
Cluster Deployment
Kromgo — Talos cluster
Deviations from defaults
- Cross-namespace read into vmsingle —
KROMGO_PROMETHEUS_URLpoints atvmsingle-vm-stack.monitoring.svc:8428; VictoriaMetrics serves the Prometheus query API natively, so no adapter is needed. The monitoring namespace's deny-all would drop these queries, soallow-vmsingle-kromgogrants kromgo pods ingress to the vmsingle HTTP port — the only reader among the otherwise write-only cross-namespace allowances. - Badge definitions live in the overlay —
config/config.yamlis PromQL against this cluster's exporters (node-exporter, kube-state-metrics), so it is talos-specific by nature and generated into thekromgo-configConfigMap here rather than in base.
- Image:
ghcr.io/home-operations/kromgo:0.16.1@sha256:ad39dd32da600ea261112e7a098345d474d48c997790b0373eb651c66e490844
Rendered manifests (kustomize build)
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app.kubernetes.io/name: kromgo
name: kromgo
namespace: kromgo
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app.kubernetes.io/name: kromgo
template:
metadata:
labels:
app.kubernetes.io/name: kromgo
spec:
automountServiceAccountToken: false
containers:
- env:
- name: KROMGO_PROMETHEUS_URL
value: http://vmsingle-vm-stack.monitoring.svc.cluster.local:8428
image: ghcr.io/home-operations/kromgo:0.16.1@sha256:ad39dd32da600ea261112e7a098345d474d48c997790b0373eb651c66e490844
livenessProbe:
failureThreshold: 5
httpGet:
path: /healthz
port: web
initialDelaySeconds: 10
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 3
name: kromgo
ports:
- containerPort: 8080
name: web
protocol: TCP
- containerPort: 8081
name: metrics
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /readyz
port: web
initialDelaySeconds: 5
periodSeconds: 5
successThreshold: 1
timeoutSeconds: 3
resources:
limits:
memory: 128Mi
requests:
cpu: 25m
memory: 64Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
privileged: false
readOnlyRootFilesystem: true
volumeMounts:
- mountPath: /config
name: kromgo-config
readOnly: true
priorityClassName: tier-3
securityContext:
runAsGroup: 65532
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
volumes:
- configMap:
name: kromgo-config-t47cm97kc6
name: kromgo-config