Files
computer-playbook/docs/contributing/code/services.md
T
kevinveenbirkenbachandClaude Sonnet 4.6 be6a310d59 refactor(services): implement role-local service registry (req-001)
- 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>
2026-04-13 19:33:20 +02:00

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-cdn provides the primary service key cdn
  • web-app-keycloak provides oidc
  • svc-db-mariadb provides mariadb

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:

  • enabled
  • shared
  • 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.
  • frontend vs. backend is 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: and canonical:
  • validates and applies run_after: from meta/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:

  1. universal
  2. workstation
  3. server
  4. web-svc
  5. web-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:

  • id
  • role
  • enabled
  • shared
  • needed

Behavior:

  • accepts either a service key or a provider role name
  • resolves aliases through canonical
  • resolves provider roles through discovered primary service keys
  • computes needed transitively 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-mariadb provides mariadb
  • svc-db-postgres provides postgres

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.

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