The entity name keys a role's instance directory, its compose project and its container name, and was derived only by stripping the longest matching category prefix. Two roles under different categories whose names share a suffix therefore collapse onto one entity and overwrite each other's rendered stack, which no provides: can repair. A literal entity_name in vars/main.yml now wins over the derivation. web-app-seaweedfs-console takes its shorter name back and declares the entity seaweedfs-console, which frees the seaweedfs key in its own services map for the engine consumer and removes the provides:, covers: and alias entries that stood in for it. A new lint pairs the declaration with the primary key it has to match, because a drift between them would drop the role from the provider set in silence. Compared across all 288 roles, exactly one entity changes, and it restores what main already keeps on disk, so no instance directory moves. The function and the filter lose their get_ prefix. Two lints read that prefix as the discriminator between the variable and the filter that fills it; both discriminate on the pipe now. Ansible's templar resolves a filter and a same-named variable independently, verified against ansible-core 2.20.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
7.8 KiB
Service Management 🧱
This page describes how shared services are declared, discovered, ordered, loaded, and injected.
What Is a Service? 🧩
A service is a reusable dependency that an application enables via services.<service_key>.
Examples:
web-svc-cdnprovides the primary service keycdnweb-app-keycloakprovidesoidcsvc-db-mariadbprovidesmariadb
Each service entry lives in the provider role's own meta/services.yml, where the file root IS the services map keyed by <entity_name> (no compose: and no services: wrapper).
See layout.md for the full per-role meta layout.
Role-Local Service Metadata 🏷️
Service providers are self-describing. The provider role owns:
enabledshared- optional
provides - optional
canonical
Example (file root of meta/services.yml):
keycloak:
enabled: false
shared: true
provides: oidc
Canonical aliases are also role-local:
cdn:
enabled: false
shared: true
css:
enabled: true
shared: true
canonical: cdn
javascript:
enabled: true
shared: true
canonical: cdn
Rules:
- The primary service entry is the role entity name returned by
entity_name. provides:is only used when the public service name differs from the entity name.canonical:is only used on alias entries that resolve back to the primary service key.frontendvs.backendis derived from the role name prefix, not stored in config.
Service Discovery 🔍
Service discovery is built from role configs, not from a central registry file.
Primary implementation files:
The discovery layer:
- scans role configs from roles/
- discovers provider entries from
services - derives deploy type and loader bucket from the role name
- resolves
provides:andcanonical: - validates and applies
run_after:frommeta/services.yml.<primary_entity>
Load Order 🧮
main.yml is the single loader entry point for all shared services.
It runs from 01_constructor.yml before the normal application stage.
Global bucket order:
universalworkstationserverweb-svcweb-app
Within the same bucket, ordering is refined by run_after: declared on the provider role's primary entity in meta/services.yml (i.e. services.<primary_entity>.run_after).
Rules:
run_after:entries are role names, not service keys.- Cross-type
run_after:is invalid and fails hard. - Later-bucket dependencies are invalid and fail hard.
- Cross-type service dependencies do not need
run_after:because constructor-stage loading already brings backend services up before normal app deployment.
Loading vs Injection 🔀
Service loading and frontend injection are separate mechanisms.
Loading 📥
Loading decides whether the provider role is deployed at all.
- queries the ordered discovered service list
- checks
lookup('service', service_key).required - skips roles already protected by
run_once_* - loads services through load/app.yml
Frontend service probe/load helper:
Injection 🔌
Injection decides whether a deployed app gets extra nginx integration such as dashboard, logout, CSS, or JavaScript hooks.
This stays in:
Injection still reads the current app's services.<feature>.enabled flags.
It does not load provider roles.
Lookup Plugins 🔎
service 🧷
File: service.py
Examples:
lookup('service', 'matomo')
lookup('service', 'sso')
lookup('service', 'web-svc-cdn')
Returns:
idroleenabledsharedrequiredis True when some deployed app has the service with bothenabled: trueANDshared: true, directly or transitively through its own enabled service dependencies. This is the flag consumers should gate on when deciding whether to load, integrate with, or configure the service. A plainenabledflag is not enough on its own, because a service that is enabled but never shared with another app is not actually contractually required by anyone.
Behavior:
- accepts either a service key or a provider role name
- resolves aliases through
canonical - resolves provider roles through discovered primary service keys
- computes
requiredtransitively from enabled shared services
service_registry 📚
File: service_registry.py
Examples:
query('service_registry') | first
query('service_registry', 'ordered') | first
Modes:
- default: full discovered registry mapping
ordered: ordered primary service entries for the service loader
applications_current_play 🧭
File: applications_current_play.py
Builds the current-play application set including:
- group-selected roles
- transitive shared service dependencies
- meta dependencies
Database Services 🗄️
Relational databases are regular services:
svc-db-mariadbprovidesmariadbsvc-db-postgresprovidespostgres
Applications express database choice directly via the file root of meta/services.yml:
mariadb:
enabled: true
shared: true
or:
postgres:
enabled: true
shared: false
The lookup('database', ...) API is the convenience accessor for database connection values; it resolves the active direct database service from the role-local keys above.
Related Files 📁
| File | Purpose |
|---|---|
| service_registry.py | Service discovery, provides, canonical, bucket detection, run_after ordering |
| service_registry.py lookup | Exposes the discovered registry and ordered provider list to Ansible |
| service.py | Resolves service flags and transitive need |
| applications_current_play.py | Builds the current play app graph with shared service deps |
| main.yml | Single shared-service loader entry point |
| load_service.yml | Per-service load helper used by the central service loader |
| 01_constructor.yml | Calls the service loader during constructor |
| load/app.yml | Run-once role loader |
| test_service_registry.py | Unit tests for discovery, buckets, and run_after ordering |
| test_service.py | Unit tests for lookup('service', ...) |
| test_resolvable.py | Integration checks for discovered service resolution |
| test_canonical.py | Canonical alias consistency checks |
| test_transitive_dependencies.py | Integration coverage for transitive dependency resolution |