30 KiB
Operator Convention Audit: ConfigKit and Ansible
Status: audit complete; no behavior changes proposed or authorized by this document.
Scope and evidence
Reviewed ConfigKit revision: 2baf14d. Comparison date: 2026-10-19.
The configuration contract owns ConfigKit behavior; this audit
records differences, not an alternative contract or implementation plan.
The closest Ansible counterpart is usually YAML variable data loaded through
ansible.builtin.include_vars, not a playbook's task language or ansible.cfg
(which is INI). Where useful, comparisons explicitly identify fileglob, find,
variable precedence, filters, or task syntax. Those mechanisms have different
conventions even within Ansible.
Evidence consists of current official Ansible documentation, the versioned
ansible-core v2.19.0 include-vars implementation and
YAML constructor, and the
PyYAML 6.0.2 scalar resolver. Implementation observations below are
identified separately from documented interfaces; they are not promises about
every Ansible version or plugin. The /latest/ documentation links are moving
references. No Ansible executable is available in the development environment,
so this is a documentation/source comparison, not a side-by-side runtime test.
ConfigKit's parser and import-policy implementations were inspected alongside their tests and maintained consumer examples. Focused YAML, import, composition, overlay, inheritance, version, decode, secret, and consumer tests passed on the reviewed tree. No code or existing policy was changed for this audit.
Summary
| Area | Main operator surprise |
|---|---|
| YAML values | Several accepted Ansible scalar spellings are errors in ConfigKit; dates remain strings. |
| Discovery | Directory recursion, filename filtering, missing-input handling, and wildcard syntax are not drop-in equivalents. |
| Composition | Import overlap is a conflict, not permission for a later file to replace an earlier value. |
| Precedence | ConfigKit recursively combines mappings across explicit tiers; Ansible normally replaces dictionary variables unless merging is requested. |
| Reuse | ConfigKit's post-assembly sibling extends is neither YAML anchor reuse nor Ansible task/role inheritance. |
| Evaluation | YAML is configuration data, not Jinja expressions, lookups, conditionals, or a task program. |
| Secrets and inspection | Typed environment-name references and classified frozen inspection differ from Vault, lookups, and task-output controls. |
These differences are not automatically defects. Many follow accepted ConfigKit decisions, especially strict composition, predictable precedence, presence tracking, typed validation, and the complete-on-return lifecycle.
1. YAML parsing and scalar conventions
ConfigKit authority: YAML documents, scalars, and parser policy tests.
Y1 — Boolean and null spellings are narrower
ConfigKit accepts plain lowercase true, false, and null, plus ~ and an
empty mapping value for null. Unquoted yes, no, on, off, and case variants
such as True or NULL fail rather than becoming strings. Quoting makes them
strings, not typed booleans.
Ansible's YAML conventions accept additional boolean spellings; the inspected
PyYAML resolver also recognizes mixed-case null forms. An operator copying
enabled: yes must change the value to true, not merely quote it when the
application expects a boolean. Ansible YAML,
resolver source.
Y2 — Numeric spellings and timestamp interpretation differ
ConfigKit uses JSON-number syntax, rejecting forms such as 010, 0x10,
1_000, +1, .5, and .inf. Date/time-looking plain values remain strings.
The inspected PyYAML resolver recognizes several non-JSON numeric forms and
timestamps; Ansible's constructor retains timestamp construction. Do not assume
that accepting the same YAML text produces the same scalar type.
Resolver source, Ansible constructor.
ConfigKit also retains exact integer spelling for configured version checks, but ordinary typed and dynamic destinations still enforce their documented ranges. Ansible's Python-backed numeric data is not a guarantee that a value will fit a ConfigKit application's Go field.
Y3 — Duplicate keys are always errors
ConfigKit rejects duplicate decoded mapping keys at every depth. Ansible's documented default is a warning; its constructor retains the last value. Ansible also offers error and ignore settings. A file that loads with an Ansible warning can therefore fail ConfigKit outright. Duplicate-key setting, constructor source.
Y4 — Anchors, aliases, merge keys, and explicit tags are unavailable
ConfigKit rejects anchors, aliases, all explicit YAML tags, and even an ordinary
quoted key named <<. Ansible documents anchor/alias/merge reuse and recognizes
tags including !unsafe and !vault; its constructor also supports standard
tags such as !!str. These files require semantic rewriting, not just moving
them into an import directory. Advanced syntax,
constructor source.
Y5 — Empty variable files are not empty configuration mappings
ConfigKit requires one nonempty mapping document: {} is valid; an empty file,
document-only marker, null root, or sequence root is not. The inspected
include_vars implementation turns a null/empty parsed result into an empty
dictionary. Explicitly use {} for an intentionally empty ConfigKit fragment.
Include-vars implementation.
Mapping roots themselves are not a divergence from normal Ansible variable
files. Playbook sequence roots belong to a different interface. Likewise, Ansible
has an explicit from_yaml_all filter for streams; that does not mean ordinary
include_vars automatically accepts multiple documents. ConfigKit loading has
no corresponding stream mode. Variable files,
multi-document filter guidance.
Y6 — Mapping keys are data strings, not Ansible variable identifiers
ConfigKit requires string-classified keys, quoting numeric/boolean-looking keys when needed. Literal dots, slashes, spaces, and numeric-looking quoted keys are not split into paths. Ansible distinguishes restricted top-level variable names from nested dictionary keys; its YAML constructor can construct non-string hashable keys. Porting arbitrary nested Ansible dictionary keys may require quoting, while imposing Ansible variable-name rules on every ConfigKit key would be unnecessarily restrictive. Variable naming and dictionary access, constructor source.
2. Import declarations and filesystem discovery
ConfigKit authority: explicit imports, input destinations, and import internals.
Defaults below are ConfigKit's library defaults. Applications can supply
load-wide ImportDefaults; an explicit declaration field wins over that
application default. Transitive declarations use the same application defaults,
not the options authored on the declaration that selected their parent source.
An operator must therefore check the consuming application's selected defaults.
| ID | ConfigKit convention | Closest Ansible convention and difference |
|---|---|---|
| I1 | Enabled documents contain root configkit.imports; each declaration chooses path, dir, or glob. path/dir accept a string or list. |
include_vars is a task with file or dir, normally one string; fileglob is a separate lookup. Ansible variable data does not recursively execute embedded include declarations. Module, lookup. |
| I2 | Directory discovery is shallow by default. recursive: true enables descendants; depth requires effective recursion. |
include_vars: dir recurses by default. find instead defaults to recurse: false, with that setting overriding depth rather than making depth invalid. Include-vars, find. |
| I3 | pattern is one or more case-sensitive basename globs; omitted means unrestricted, [] means no matches. |
include_vars.files_matching is one regular expression. find.patterns is closer, but its documented empty/default pattern selects all rather than none. Include-vars, find. |
| I4 | exclude uses the same basename-glob grammar. |
include_vars.ignore_files is described as filenames; the inspected implementation actually uses suffix-anchored regex searches. find.excludes accepts glob/regex patterns. Do not translate fields by renaming alone. Module, implementation, find. |
| I5 | There is no extension whitelist: discovery attempts every selected filename, including .txt or extensionless files. |
Directory include_vars defaults to json, yaml, and yml; unknown extensions fail unless configured to be ignored. This restriction does not mean an explicitly named file has the same filtering. Module, implementation. |
| I6 | Missing imports are optional by default. required: true checks every named path, or requires a nonempty directory/glob result. |
Direct include_vars missing-file/directory access fails. fileglob may return no matches, and first_found(skip=True) provides a different explicit absence mechanism. Implementation, fileglob, first_found. |
| I7 | hidden and follow default to true for discovered entries; explicit roots retain normal access. |
find defaults both to false, and automatic host_group_vars ignores hidden/backup entries. The inspected include_vars walk follows directory links and has no equivalent hidden/follow switches; do not call ConfigKit's defaults universally unlike Ansible. Find, vars plugin, implementation. |
| I8 | Import paths are slash-relative to their declaring source's directory, inside its selected namespace/root. Absolute paths and URLs are invalid. | Ansible accepts absolute controller paths and uses role/play/task search contexts for relative resource lookup. Copying a role-relative name can select another file or fail in ConfigKit. Search paths, loader source. |
| I9 | A path list is a normalized set union; lexical duplicates coalesce within a declaration. Repeated admitted sources across declarations/routes fail within a strict tier. | Repeating include_vars tasks is allowed by the inspected implementation; no equivalent load-wide repeated-source rejection is applied there. first_found lists select the first hit, not all files. Implementation, first_found. |
| I10 | Source acquisition is authored/depth-first, but assembly uses the smallest dependency-ready stable source key within each strict scope. | Directory include_vars uses alphabetic file loading, not ConfigKit's import dependency schedule. Prefixing filenames cannot authorize ConfigKit replacement or force arbitrary dependency-violating item order. Module. |
| I11 | Optional whole-declaration empty results retain source-local ImportSelections evidence, not fake sources or warnings. |
include_vars returns included file paths, rather than this optional-empty declaration record. The inspection models are not interchangeable. Return values. |
Depth alignment: direct files are depth 1, and recursive depth 0 means
unlimited subject to budgets. That is consistent with the inspected
include_vars depth convention; the significant differences are recursion's
default and whether depth is independently applicable.
Implementation.
Empty-set behavior: ConfigKit path: [] succeeds even when required because
no named target is missing. A required dir: [] or pattern: [] search fails
because it selects no files. An explicit empty pattern list also overrides an
application's nonempty default patterns. These are set meanings, not automatic
fallback to unrestricted search.
I12 — Even shell-style wildcard grammars are not identical
ConfigKit uses Go path.Match, not Python fnmatch or regex: character-class
negation uses [^a], whereas Python uses [!a]; Go backslashes escape pattern
characters. For example, copying [!a]*.yml can change the selected set rather
than producing an obvious error. Match full basenames, not regex substrings.
Go pattern grammar, Python filename grammar.
ConfigKit's glob can wildcard directory components and explicitly rejects
recursive **. Ansible's fileglob contract supports filename patterns in a
single directory, not wildcard directory search as an interchangeable feature.
Python glob's ordinary wildcard treatment also excludes dot-prefixed names
unless requested by the pattern, unlike ConfigKit's default hidden: true.
Fileglob, Python glob.
I13 — Discovery errors and resource bounds are load failures
ConfigKit optionality permits absence, not malformed selected YAML, unreadable
contributions, containment failures, active traversal cycles, or exhausted
source/byte/node/entry budgets. The inspected Ansible directory walker logs
os.walk errors through its verbosity callback rather than applying the same
fail-the-load rule. Neither ignore_errors nor lookup errors='ignore' is an
equivalent of ConfigKit's narrowly scoped required: false.
Implementation, first_found behavior.
ConfigKit's fixed budgets have no shared, portable Ansible equivalence established by this audit. This is not a claim that Ansible or its parsers have no limits.
3. Combination, precedence, and inheritance
C1 — Imports compose strictly rather than overriding
Within a main tier or one underlay group, shared mappings recurse, but overlapping
non-mapping values conflict even when equal. Ansible variable precedence normally
chooses a winning definition; directory include_vars also overwrites colliding
keys in its accumulated results. 10-base.yml plus 90-local.yml is therefore
not an override scheme for ConfigKit imports. Intentional replacement needs a
higher ConfigKit tier. ConfigKit strict composition,
Ansible precedence, include-vars implementation.
C2 — Cross-tier mapping combination is recursive by default
ConfigKit retains omitted lower-tier mapping children, including when a higher
tier supplies {}; other present values replace, and [] clears a sequence.
Ansible's default dictionary-variable behavior is replacement, and its combine
filter defaults to nonrecursive merging. ConfigKit's mapping overlay is closer
to explicit combine(recursive=true) than ordinary variable reassignment.
Ansible's global hash-merge setting is configurable and discouraged upstream;
it is not a portable equivalence. ConfigKit overlays,
hash behavior, combine.
C3 — Registered sequence composition is not merge-by-identity
ConfigKit appends distinct string-ID records only at application-registered
strict-tier paths; repeated IDs conflict. Higher-tier sequences still replace
wholesale. Ansible combine offers selectable list policies, while the separate
community.general.lists_mergeby filter combines records sharing an attribute.
That optional collection filter does not describe ConfigKit's distinct-ID rule
or Ansible's universal default. ConfigKit registration,
combine, lists_mergeby.
C4 — The precedence model and lifetime are different
ConfigKit loads application defaults, ordered underlays, main, selected overlay,
and explicit CLI values into one completed result. It supplies no general
environment-variable overlay. Ansible has host/play/task scopes and runtime
variable definitions; ordinary command-line options are not generally highest
priority, while -e extra variables are. Environment also configures Ansible
itself. Do not equate every application flag with Ansible's ordinary CLI category.
ConfigKit layered loading,
Ansible precedence.
ConfigKit rejects duplicate or overlapping explicit override records rather
than providing a last-argument-wins channel. It accepts typed values supplied by
the application; it does not define an Ansible-style key=value parser.
Ansible extra-vars key=value values are strings; JSON/YAML forms preserve
other types. ConfigKit overrides,
Ansible extra vars.
C5 — Defaults depend on presence, not truthiness
ConfigKit preserves explicit false, zero, empty text/list, and allowed null;
omission retains defaults. Null is not universally legal: application Go scalar
and struct fields reject it, while supported nullable fields accept it. Ansible
can distinguish undefined from false/null too. Its ordinary Jinja default
filter handles undefined, while default(value, true) additionally treats falsey
values as needing replacement; omit is a separate task-argument mechanism.
That opt-in truthiness behavior has no ConfigKit YAML equivalent.
ConfigKit decoding,
Ansible default and omit guidance.
C6 — extends is post-assembly sibling inheritance
ConfigKit resolves an exact immediate sibling mapping after all tiers combine;
the child recursively overlays its parent. extends: null cancels a relationship,
and enclosing inheritance precedes nested destination-local resolution. All
effective relationships are resolved, not just an application-selected preset.
Ansible YAML anchors/<< reuse nodes within one document during parsing, and
YAML merge does not recursively combine an already-present nested child mapping.
Ansible play/block/task keyword inheritance is another distinct mechanism.
Neither is a drop-in equivalent of ConfigKit extends; explicit Ansible
recursive combine expressions can approximate parts of the value operation.
ConfigKit inheritance,
Ansible advanced syntax, YAML merge definition,
keyword inheritance, combine.
4. Structural controls, evaluation, and schema
S1 — Reserved data keys and destination syntax differ
ConfigKit reserves configkit for root directives and rejects it below the root;
with imports disabled it is still not ordinary application data. extends is
structural input, and quoting a key does not bypass either control. Ansible
variable dictionaries do not give these two names ConfigKit's structural meaning.
Do not confuse this with Ansible's own magic-variable and playbook-keyword rules.
ConfigKit imports,
inheritance, Ansible variable rules.
Import destination: [catalog, entries] means literal mapping segments relative
to the immediate importer; [] adds none. placement: sequence-item builds a
record in that destination's sequence, with a local helper scope. Ansible's
include_vars.name wraps data under one variable, not this relative mount/item
model; dots are not a substitute for ConfigKit segment lists.
ConfigKit destinations,
Ansible include-vars namespace.
S2 — Jinja, conditional includes, and lookups are not evaluated
In ConfigKit, "{{ value }}", "{{ lookup('env', 'TOKEN') }}", and ${TOKEN}
are ordinary strings unless application code gives them domain meaning; no
generic template pass exists. when, loop, tags, and ignore_errors are
not import-declaration controls. Ansible supports Jinja references/lookups and
task-level conditionals and loops. Configuration must be translated structurally
or handled by deliberate application resolution, not presumed executable.
ConfigKit resolution,
Ansible variables, task keywords, env lookup.
S3 — Final typed validation and version ownership are library boundaries
ConfigKit strictly decodes the assembled result against the application's Go
shape: unknown struct fields and incompatible values fail; dynamic maps leave
domain-key validation to the application. Its configured version rule accepts
string/integer declarations, can consume non-main headers, and protects the main
version path from imports/upper tiers/inheritance. None of those policies should
be inferred from the word version alone.
Ansible variable-file loading is not validation against one final application
schema; validation occurs at other boundaries, including module/role argument
specifications. A variable named version is not by itself a ConfigKit-style
protected, consumed header. ConfigKit versions,
typed decoding,
Ansible role validation, include-vars data loading.
ConfigKit's integer-version-to-string exception is specific to that configured path. It does not establish general Ansible-like filter coercion elsewhere. Ansible type conversion filters.
For a field typed as time.Duration, ConfigKit requires a duration string such
as 5s, not an integer count of seconds. Ansible duration arguments are module
specific; for example, wait_for.timeout uses integer seconds. Translate the
particular field's representation rather than assuming one shared duration type.
ConfigKit decoding,
Ansible wait_for.
S4 — Overlay selection and module objects are ConfigKit/application conventions
A selected ConfigKit overlay uses exactly overlays/<name>.yml; empty or exact
default means no overlay. There is no extension fallback or stack. Ansible's
inventory/role variable directories are different selection and precedence
mechanisms, not this named overlay convention.
ConfigKit overlays, Ansible vars plugin,
roles.
The canonical module-object guidance uses an explicit identifier and options
mapping; individual field names belong to applications. Ansible tasks instead
name an action/module or use action/args, with task keywords alongside it.
A generic ConfigKit configuration object is not an Ansible task, and ConfigKit
does not provide an Ansible module registry or execute a module-named mapping.
ConfigKit module guidance,
Ansible task syntax, keywords.
5. Secrets, resources, and inspection
O1 — A typed secret field contains an environment name
For a ConfigKit Secret slot, api_key: SERVICE_TOKEN is a reference to one
nonempty effective environment value; ordinary string slots do not gain that
behavior. Missing/unset/empty required secrets fail. Ansible uses explicit env
lookups or other secret providers; its env lookup defaults to an empty string
for undefined names unless configured otherwise. These are different missing-value
and evaluation conventions. ConfigKit secrets,
Ansible env lookup.
Vault-encrypted files or !vault values are not ConfigKit inputs to decrypt.
Ansible Vault protects stored content and supplies decryption through Ansible;
ConfigKit's typed reference deliberately keeps credential bytes out of YAML
composition/history. Its separate secrets-directory bootstrap is an application
startup operation, not another YAML include or a Vault equivalent.
ConfigKit bootstrap,
Ansible Vault.
O2 — Redaction and serialization are not task no_log
ConfigKit has application-classified paths and typed secret references, with
frozen redacted effective/explanation output; there is no YAML no_log control
or unredacted renderer option. Ansible no_log controls task output and does not
make every debug operation safe. Vault encryption and !unsafe also have
different purposes; neither is a synonym for ConfigKit classification.
ConfigKit inspection,
Ansible logging, advanced syntax.
ConfigKit effective YAML uses a fixed deterministic layout and may not be
reloadable after redaction. Ansible to_nice_yaml exposes formatting options
such as indentation and key sorting; it is serialization, not ConfigKit's frozen
safe-inspection contract. Do not use either output interchangeably without
checking the consumer's required representation. Ansible YAML filter.
O3 — Resource paths are passive data, not generic Ansible file lookup
ConfigKit retains the declaring source's resource context, including through inheritance; applications explicitly open resource-valued fields. It does not automatically resolve every string that resembles a path. Ansible file resources are interpreted by their specific module/lookup and controller/remote search context. These differ from ConfigKit import paths as well as from arbitrary application data. ConfigKit resource contexts, Ansible path contexts.
Assessment and follow-up
The most plausible convention-alignment discussion is discovery: recursion,
filter names/grammar, filename defaults, absence handling, and hidden/link
defaults. First choose the intended comparison (include_vars, fileglob, or
find); copying pieces from each would not create one coherent Ansible convention.
Preserve ConfigKit's accepted composition, presence, lifecycle, and provenance decisions unless the maintainer explicitly chooses to change them. YAML scalar and reuse differences likewise reflect an owned parsing policy, not an accidental failure to inherit upstream YAML behavior. Documenting a difference does not authorize adding a compatibility mode, a new restriction, or an implementation.
Immediate operator guidance from the audit is to review imported files for overlapping values, use explicit directory filters/requiredness when intended, translate regex/Python patterns rather than copying them, and replace Ansible tags/templates/anchors with supported application data and ConfigKit mechanisms.
Validation for this documentation pass: focused ConfigKit YAML and engine tests, maintained consumer examples, local documentation links, and whitespace checks. External sources were reviewed online; no Ansible installation or application consumer dependencies were changed. This is not an exhaustive audit of Ansible's third-party plugin ecosystem or a claim that its behavior is uniform across versions and application-specific modules.