- Delete group_vars/all/20_services.yml; all service metadata now lives in each role's config/main.yml under compose.services.<name> - Rename generic 'database' service key to explicit 'mariadb'/'postgres' across all app roles; remove legacy type: field - Add utils/service_registry.py and plugins/lookup/service_registry.py as the single source of truth for service discovery - Add utils/database_service.py to centralize DB service resolution - Delete service_load_enabled filter and service_should_load lookup; replaced by service lookup plugin - Update sys-utils-service-loader to run in constructor stage and use generic load_service.yml; remove load_frontend_service.yml - Add _service_loader_preloading guard to sys-svc-compose, sys-svc-webserver-core and sys-ctl-alm-compose to prevent health-checks/alarms from firing during preload phase - Replace manual load_app.yml calls for shared DB/Mailu with assert- based preload validation in sys-svc-rdbms and sys-svc-mail - Update networks.yml.j2 templates to resolve LDAP/AI networks via service_registry instead of hardcoded role names - Migrate all tests to new service discovery API; delete obsolete tests - Update docs/contributing/code/services.md to reflect role-local model Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
7.6 KiB
Service Management
This page is the SPOT for 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
compose.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
config/main.yml under compose.services.<entity_name>.
Role-Local Service Metadata
Service providers are self-describing. The provider role owns:
enabledshared- optional
provides - optional
canonical
Example:
compose:
services:
keycloak:
enabled: false
shared: true
provides: oidc
Canonical aliases are also role-local:
compose:
services:
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
get_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
compose.services - derives deploy type and loader bucket from the role name
- resolves
provides:andcanonical: - validates and applies
run_after:frommeta/main.yml
Load Order
roles/sys-utils-service-loader/tasks/main.yml
is the single loader SPOT for all shared services.
It runs from tasks/stages/01_constructor.yml
before the normal application stage.
Global bucket order:
universalworkstationserverweb-svcweb-app
Within the same bucket, ordering is refined by run_after: in the provider role's
meta/main.yml.
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.
roles/sys-utils-service-loader/tasks/main.yml:
- queries the ordered discovered service list
- checks
lookup('service', service_key).needed - skips roles already protected by
run_once_* - loads services through
tasks/utils/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 compose.services.<feature>.enabled flags.
It does not load provider roles.
Lookup Plugins
service
File:
plugins/lookup/service.py
Examples:
lookup('service', 'matomo')
lookup('service', 'oidc')
lookup('service', 'web-svc-cdn')
Returns:
idroleenabledsharedneeded
Behavior:
- accepts either a service key or a provider role name
- resolves aliases through
canonical - resolves provider roles through discovered primary service keys
- computes
neededtransitively from enabled shared services
service_registry
File:
plugins/lookup/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:
plugins/lookup/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 now:
svc-db-mariadbprovidesmariadbsvc-db-postgresprovidespostgres
Applications express database choice directly via:
compose:
services:
mariadb:
enabled: true
shared: true
or:
compose:
services:
postgres:
enabled: true
shared: false
The lookup('database', ...) API remains as the convenience accessor for database
connection values, but it now resolves the active direct database service from those
role-local keys instead of compose.services.database.type.
Related Files
| File | Purpose |
|---|---|
utils/service_registry.py |
Service discovery, provides, canonical, bucket detection, run_after ordering |
plugins/lookup/service_registry.py |
Exposes the discovered registry and ordered provider list to Ansible |
plugins/lookup/service.py |
Resolves service flags and transitive need |
plugins/lookup/applications_current_play.py |
Builds the current play app graph with shared service deps |
roles/sys-utils-service-loader/tasks/main.yml |
Single shared-service loader SPOT |
roles/sys-utils-service-loader/tasks/load_service.yml |
Per-service load helper used by the central service loader |
tasks/stages/01_constructor.yml |
Calls the service loader during constructor |
tasks/utils/load_app.yml |
Run-once role loader |
tests/unit/utils/test_service_registry.py |
Unit tests for discovery, buckets, and run_after ordering |
tests/unit/plugins/lookup/test_service.py |
Unit tests for lookup('service', ...) |
tests/integration/test_services_resolvable.py |
Integration checks for discovered service resolution |
tests/integration/test_services_canonical.py |
Canonical alias consistency checks |
tests/integration/test_service_transitive_dependencies.py |
Integration coverage for transitive dependency resolution |