Helvilette is a declarative GitOps continuous delivery tool for Ansible that uses your Ansible Playbooks to manage desired state on a fleet of nodes. It is designed to be a lightweight and flexible alternative to deploying Ansible configurations via GitOps pull-based model at scale, or at edge.
The current cloud-native era has created an invisible divide:
- Tier 1 (~20%): Organizations with Kubernetes, ArgoCD, Flux, and a full GitOps ecosystem. Config drifts are handled by K8s reconciliation loops.
- Tier 2 (~80%): Startups, SMBs, universities, government agencies, and homelabs running 5–50 VMs on bare metal, Proxmox, or scattered VPS providers. Config drifts happen daily, and without Kubernetes, there is no easy way to fix them.
For Tier 2 users, the current Ansible workflow — "runnable by CI/CD pipeline" — is a maze of pain:
Playbook ready
│
├─► Push to Git
├─► Configure GitHub Actions / GitLab CI
├─► Setup SSH keys in CI secrets
├─► Open port 22 (or hack a bastion host / VPN tunnel)
├─► Write CI pipeline YAML calling ansible-playbook
├─► Debug why the CI runner can't SSH into some nodes
├─► 4 hours scratching your head + 4 more for a post-mortem
│
▼
Server maybe configured???
Each step is "glue work". Nobody enjoys it, nobody excels at it, and critically, nobody should be exposing SSH port 22 just to run a playbook. The push-based model forces a fundamental security anti-pattern: all SSH root keys concentrated on a single laptop or CI server. Laptop stolen? Entire infrastructure compromised. Engineer quits? Infrastructure knowledge walks out the door.
Helvilette eliminates the push-based delivery pipeline entirely:
Playbook ready
│
├─► Push to Git repo
▼
Othela notifies and sets Job specs
│
▼
Agent polls → Agent clones → Agent runs ansible-playbook → Agent reports back
"Use SSH for Ansible to install Helvilette once. Then never need SSH for Ansible again."
Helvilette maps 1:1 to Kubernetes architecture, transposed to the OS/systemd layer:
| Kubernetes Concept | Helvilette Equivalent | Role |
|---|---|---|
kube-apiserver |
Othela (Control Plane) | Receives declarations, dispatches jobs to agents |
kubelet |
Helvilette Agent | Sits on each node, pulls + executes + reports |
| OCI Image | Ansible Playbook Repo | Artifact containing execution logic |
| Container Registry | Git Server | Artifact storage |
Dockerfile |
playbook.yml + roles/ |
Defines "what needs to be done" |
values.yaml (Helm) |
helvilette.yml |
Per-deployment declarative configuration |
Pod spec / nodeSelector |
nodeGroup / nodeSelector |
Declares what runs where |
livenessProbe |
livenessProbe |
Identical concept, applied to systemd services |
| Container runtime | Ansible engine | The actual executor |
etcd |
SQLite / Git | State storage |
Ansible installs Helvilette (the last SSH session). Helvilette delivers all future Ansible playbooks. The chicken lays the egg-making machine, then retires.
Last SSH session ever:
┌──────────────────────────────────────────┐
│ ansible-playbook install-helvilette.yml │
│ │
│ → Installs agent on N servers │
│ → Agent registers with Othela │
│ → systemd enable + start │
│ │
│ Done. Close port 22. Forever. │
└──────────────────────────────────────────┘
Helvilette operates at the OS/systemd layer — beneath Kubernetes, beneath container runtimes, beneath everything:
Layer 4: ┌─ Kubernetes ───────────────────────────┐
│ Pods, Deployments, Services │
│ ❌ Cannot self-heal │
Layer 3: ├─ Container Runtime (containerd) ───────┤
│ ❌ Cannot self-restart │
Layer 2: ├─ systemd ──────────────────────────────┤
│ kubelet.service, containerd.service │
│ etcd.service, kube-apiserver.service │
Layer 1: ├─ OS (Linux) ───────────────────────────┤
│ │
│ 🐈⬛ Helvilette Agent lives here. │
│ It is a systemd service. │
│ It can see EVERYTHING above. │
└────────────────────────────────────────┘
This gives Helvilette a unique capability: managing the managers. It can rolling-update kubelet, restart kube-apiserver, and heal what K8s cannot heal — because K8s cannot perform surgery on its own brain.
- Pull-based GitOps delivery — Agents pull playbooks from Git. No inbound SSH access required.
- Desired-state reconciliation loop — Continuous drift detection and self-healing at the OS/systemd level.
- Lightweight agent architecture — Single Go binary (~20MB RAM), runs on Raspberry Pi (ARM64). Suitable for edge computing and IoT devices.
- K8s-familiar configuration —
helvilette.ymlusesapiVersion,kind,metadata,spec,nodeSelector, and probe conventions you already know. - Label-based node targeting — Route jobs to the right nodes using
nodeSelectorlabels, just like Kubernetes pod scheduling. - Zero vendor lock-in — Remove Helvilette and you still have working Ansible playbooks + Git repos. No proprietary DSL.
- AI-ready —
helvilette.ymluses K8s + Ansible conventions massively represented in LLM training data; any AI can generate valid configs immediately. - Structured reporting — Agents capture Ansible JSON output and report execution results back to the control plane.
| Tool | Relationship to Helvilette |
|---|---|
| Puppet | Same pull-based architecture. But Puppet requires a proprietary DSL, Hiera, Facter, and PuppetDB. Helvilette uses YAML + Ansible — zero new learning. |
| Ansible AWX / Semaphore UI | Still push-based. SSH keys moved from laptop to AWX server — attack surface relocated, not eliminated. No continuous reconciliation. |
| GitHub / GitLab Runner | CI runner asks "where should this code run?". Helvilette asks "what should this server look like?". Imperative vs. convergent. |
| Kubernetes | K8s manages containers. Helvilette manages systemd services. K8s cannot self-heal its own control plane. Helvilette can do that for k8s, on a daily basic. |
| SaltStack | Replaces Ansible entirely with its own language. Helvilette preserves your existing Ansible investment. |
- Go 1.25+ (download)
- Ansible installed on every machine that will run the Helvilette Agent
- Git installed on agent machines (for playbook cloning)
- Docker (optional, for running the E2E test suite or the ephemeral lab)
# Clone the repository
git clone https://github.com/AlexanderSlokov/Helvilette.git
cd Helvilette
# Build both binaries
make build
# Binaries are output to ./bin/
ls bin/
# othela agentHelvilette uses a declarative manifest (placed inside your Ansible playbook repository) that "looks like" Kubernetes API conventions and keeps the semantic meaning & behavior as close as possible to K8s's declarative model.
For example:
apiVersion: helvilette.io/v1alpha1
kind: PlaybookDeployment
metadata:
name: "my-company-edge-proxy-fleet"
labels:
app: "nginx-collection"
version: "1.0.0"
spec:
repo: "http://git-server:3000/helvilette/nginx-collection.git"
branch: "main"
playbook: "playbook.yml"
nodeGroups:
- name: "standard-proxies"
nodeSelector:
role: "edge-proxy"
ansible:
extra_vars:
nginx_http_port: "80"
nginx_https_port: "443"Agents with matching labels (e.g., role=edge-proxy) will automatically pull and execute the configured playbook. Agents with non-matching labels will receive no work, and chillax.
The manifest is validated on load: apiVersion and kind must match exactly, and a manifest
missing metadata.name, spec.repo, spec.playbook, or a usable spec.nodeGroups entry is
rejected with an error naming the offending field. A rejected manifest is logged and its playbook
is never dispatched, rather than quietly matching no node. The helvilette.io group and the
v1alpha1 level follow Kubernetes API group conventions — see
ADR-0002.
Note that spec.vault and nodeGroups[].probes appear in the example manifests but are not yet
parsed or enforced — they are tracked in the backlog under Vault / Secret Integration and Health
Probes.
The Agent supports K8s-style configuration with the following priority: CLI flags > YAML config > Environment variables > Defaults.
Note that the config file outranks environment variables, matching k3s. The file is an explicit,
version-controlled artifact, so what you read there is what the agent runs — an ambient OTHELA_URL
inherited from a systemd unit or container environment will not silently override it. See
ADR-0001 for the reasoning.
Using CLI flags:
./bin/agent \
--othela-url=http://othela-server:8080/api/v1 \
--node-id=node-01 \
--poll-interval=5s \
--labels="role=edge-proxy,env=production"Using environment variables:
export OTHELA_URL=http://othela-server:8080/api/v1
export NODE_ID=node-01
export POLL_INTERVAL=5s
export AGENT_LABELS=role=edge-proxy,env=production
./bin/agentUsing a YAML config file ("taste" like kubelet):
# /var/lib/helvilette/agent.yaml
othelaURL: "http://othela-server:8080/api/v1"
nodeID: "node-01"
pollInterval: "5s"
workspaceDir: "/var/lib/helvilette/workspace"
labels:
role: "edge-proxy"
env: "production"Keys are matched exactly as written above. Unrecognised keys are rejected at startup rather than ignored, so a typo surfaces as an error instead of an agent silently running on defaults.
And then, start the agent with config file:
./bin/agent --config=/var/lib/helvilette/agent.yamlChecking what the agent will actually run:
Because values arrive from four sources, --print-config resolves them and reports where each
one came from, then exits without starting the agent. Use it during bring-up, or to validate a
config file in CI:
$ ./bin/agent --config=/var/lib/helvilette/agent.yaml --print-config
othelaURL = http://othela-server:8080/api/v1 source=config-file
nodeID = node-01 source=config-file
pollInterval = 5s source=default
workspaceDir = /var/lib/helvilette/workspace source=config-file
labels.owner = sre source=env(AGENT_LABELS)
labels.role = edge-proxy source=config-fileThe agent logs the same information at startup under the message effective configuration, so a
node's behaviour can be explained from its logs without reconstructing the precedence rules from
its systemd unit and environment. Set HELVILETTE_DEV=1 for human-readable console output.
Node identity:
If nodeID is not set by any source, it defaults to the machine's hostname so that agents remain
distinguishable. Setting it explicitly is still recommended — the hostname is a fallback, not an
identity you control. Should the hostname be unavailable, the agent falls back to agent-unknown
and logs a warning, since any static default makes every affected node register under one identity.
- Agent starts and registers with Othela (sends its
nodeId+labels). - Agent enters a polling loop, asking Othela for work at the configured interval.
- Othela matches the agent's labels against
nodeSelectorrules inhelvilette.ymland returns a Job with a Git repo reference +extra_vars. - Agent clones/pulls the playbook repository to its local workspace.
- Agent runs
ansible-playbookwithANSIBLE_STDOUT_CALLBACK=jsonand any configuredextra_vars. - Agent captures the structured JSON output and reports the result back to Othela.
We will open two terminals in WSL to simulate the Server and the Agent.
Start the server listening on port 8080:
cd /mnt/e/Helvilette
/usr/local/go/bin/go run ./cmd/othela \
--port=8080 \
--playbook-dir=helvilette/othela/data/playbooks \
--state-dir=./data/othela \
--log-level=infoOthela separates its two directories, and never writes into the playbook one:
| Flag | Contents | Access | Default |
|---|---|---|---|
--playbook-dir |
Playbooks and their helvilette.yml |
Read-only | helvilette/othela/data/playbooks |
--state-dir |
SQLite database and caches | Read-write | /var/lib/helvilette/othela |
The default --state-dir needs root, which is right for a systemd-managed install
but not for a development run, so the example above points it at a local path.
Without write access Othela logs a warning and falls back to in-memory storage,
losing state on restart.
These replace the single --data-dir flag, which is removed. See
ADR-0003.
Start the agent. It will poll Othela every 5 seconds. You can use environment variables, CLI flags, or a config file (Kubelet style).
cd /mnt/e/Helvilette
# Using CLI flags
/usr/local/go/bin/go run ./cmd/agent --othela-url=http://localhost:8080/api/v1 --node-id=agent-local --poll-interval=5s- Agent connects to Othela.
- Othela sends a "Job" containing an Ansible Playbook (Prints "Hello Wunjo!").
- Agent saves this to its configured workspace directory.
- Agent runs
ansible-playbook -i "localhost," -c localwithANSIBLE_STDOUT_CALLBACK=json. - Agent captures the JSON output and sends it back to Othela.
- Othela prints the JSON report to the console.
Helvilette uses a BDD testing framework (Ginkgo) combined with Testcontainers-Go to run end-to-end (E2E) tests.
When you run the E2E suite, it will:
- Automatically build a lightweight
git-daemoncontainer serving test playbooks overgit://. - Build the
othela(Control Plane) andagentcontainer images directly from the local Dockerfiles. - Assert the state and outputs of the GitOps reconciliation loop programmatically.
- Clean up all containers and networks automatically.
Prerequisites:
You need ginkgo installed on your machine:
go install github.com/onsi/ginkgo/v2/ginkgo@latestRunning the tests:
# Run the complete E2E test suite
make e2eUnit tests and end-to-end tests are deliberately separated. Unit tests need no Docker and complete in about a second; the E2E suite needs a running stack and takes minutes.
make test # Unit tests: ./cmd/... and ./pkg/... . No Docker required.
make fmt-check # Verify gofmt without rewriting files. Same check CI runs.
make e2e # End-to-end suite. Requires the stack from `make up`.make test covers ./cmd/... and ./pkg/... rather than ./..., because ./... pulls in
the Ginkgo E2E suite, which hangs when no stack is running.
The E2E stack writes runtime state to tests/e2e/data and data/. To remove it:
make clean-e2e # Tears down the stack and deletes generated state.If you ran an older version of the stack, tests/e2e/data/playbooks/server may be owned by
root and unreadable, which makes go vet ./... and go list ./... fail with
permission denied before compiling anything. make clean-e2e clears this using a throwaway
container, so no sudo is needed. Current stacks run Othela as your own UID and no longer
create root-owned files.
Our project welcomes contributions from any member of our community. To get started contributing, please see our [Contributor Guide](TODO: Link to CONTRIBUTING.md).
Helvilette is intended to provide a pull-based delivery platform for Ansible. As such, the project will implement or has implemented:
- Desired-state reconciliation loop at the OS/systemd level
- Pull-based GitOps model without inbound SSH access
- Lightweight agent architecture suitable for edge computing and IoT devices
Helvilette will be used in a cloud native environment or non cloud-native environments with other tools. Regardless, the following specific functionality will therefore not be incorporated:
- Container orchestration (like Kubernetes or Docker Swarm)
- General-purpose CI/CD pipelines (like GitHub Actions or GitLab CI)
- Core configuration management capabilities (Ansible performs this function)
- Infrastructure provisioning (e.g., should use Terraform or Pulumi)
- User Mailing List:
- Developer Mailing List:
- Slack Channel:
- Public Meeting Schedule and Links:
- Social Media:
- Other Channel(s), If Any:
[TODO: Add links to other helpful information (roadmap, docs, website, etc.)]
This project is licensed under Apache License Version 2.0.
We follow the CNCF Community Code of Conduct v1.3 stated at https://github.com/cncf/foundation/blob/main/code-of-conduct.md.