Implemented
2026-02
Ansible-lint implements ~100 rules in Python. We needed a strategy for rule implementation that balances expressiveness, maintainability, and extensibility.
| Option | Pros | Cons |
|---|---|---|
| All rules in Python | Full engine access, familiar language | Monolithic, hard to contribute declaratively |
| All rules in Rego | Declarative, portable, data-driven | Cannot access full engine model, limited for complex checks |
| Hybrid: Rego + Python | Best of both worlds | Two rule languages to maintain |
Hybrid approach.
- Rego rules: Rules that operate on the JSON hierarchy payload (structural checks, naming conventions, best-practice patterns) are written in Rego and evaluated by OPA.
- Python rules: Rules that require the full in-memory engine model (variable resolution, call-graph traversal, risk annotations) are written in Python as native rules.
- Rego is purpose-built for structural policy checks on JSON — concise and auditable
- OPA's
data.jsonmechanism makes rules data-driven (e.g., deprecated module lists, package module sets) - Rules needing
scandata(the fullSingleScanobject with trees, contexts, variable tracking) cannot be expressed in Rego — they stay native - Each OPA rule lives in its own
.regofile with a colocated_test.rego, matching the native rule pattern
- Declarative rules are easier to audit and contribute
- Data-driven rules update without code changes
- Clear separation: structural (Rego) vs semantic (Python)
- Two rule languages to learn and maintain
- Context switching between Rego and Python debugging
- Rego rules:
rules/opa/*.rego - Rego tests:
rules/opa/*_test.rego - Data files:
rules/opa/data.json - Native rules:
src/apme_engine/rules/
- ADR-008: Rule ID conventions (L/M/R/P)
- ADR-003: Vendored ARI engine (provides scandata)