Skip to content

Validate mandatory variables and service dependencies before applying changes #215

@markuslf

Description

@markuslf

Problem

When Ansible encounters a missing mandatory variable or an unreachable service dependency (e.g. Bitwarden) mid-run, the playbook fails after some roles have already applied changes. This leads to several problems:

  • Partially configured systems in an inconsistent state, requiring manual intervention to fix.
  • Wasted time, especially on large playbooks that run for a couple of minutes before hitting the missing variable.
  • Potential downtime when infrastructure services (Apache, MariaDB, etc.) have been partially reconfigured.
  • Security risk when hardening roles are only partially applied.
  • Cascading failures when a secret store like Bitwarden is unreachable. Instead of one clear error upfront, every role that needs a password fails individually.

Currently, only 9 out of 160 roles use ansible.builtin.assert, and only for value validation (e.g. Elasticsearch watermark ranges), not for checking whether mandatory variables are defined at all.

Solution

Two approaches that serve different purposes:

pre_tasks is the only way to validate everything before any role applies changes. It runs at the very beginning of the playbook, so the system is guaranteed to be untouched when validation fails.

argument_specs validates at role entry, so earlier roles in the playbook may have already applied changes. However, it requires no manual maintenance of assert lists and protects against misuse when a role is called from a different playbook or context where pre_tasks are missing.

In practice, pre_tasks is the primary safety net. argument_specs is a secondary guardrail that comes for free once defined.

1. pre_tasks in playbooks (quick win)

Each playbook knows which roles it calls and which mandatory variables those roles expect. A single assert block in pre_tasks validates everything before the first change is applied. This is also the right place to check external service dependencies like Bitwarden or Vault.

pre_tasks:

  - name: 'Assert that mandatory variables are set'
    ansible.builtin.assert:
      that:
        - 'apache_httpd__conf_server_admin is defined'
        - 'apache_httpd__conf_server_admin | length > 0'
        - 'mariadb_server__admin_password is defined'
      quiet: true
      fail_msg: 'Mandatory variables are missing. Check your inventory.'
    tags:
      - 'always'

  - name: 'Assert that Bitwarden is reachable'
    ansible.builtin.uri:
      url: '{{ bitwarden__url }}/alive'
      timeout: 5
    when: 'bitwarden__url is defined'
    tags:
      - 'always'

Not all roles depend on Bitwarden or other external services. The reachability checks should only run when the corresponding variables are defined.

2. meta/argument_specs.yml per role (long-term)

Available since Ansible 2.11. Ansible validates argument_specs automatically at role entry, before any tasks run. This catches type mismatches and missing required variables without writing manual asserts.

# meta/argument_specs.yml
argument_specs:
  main:
    options:
      apache_httpd__conf_server_admin:
        type: 'str'
        required: true
        description: 'ServerAdmin email address.'
      apache_httpd__systemd_state:
        type: 'str'
        required: false
        default: 'started'
        choices:
          - 'reloaded'
          - 'restarted'
          - 'started'
          - 'stopped'
        description: 'Desired state of the httpd service.'

Introduce argument_specs incrementally when roles are touched anyway. No need to do all 160 at once.

Migrating existing ansible.builtin.assert usage

Some existing asserts can be replaced by argument_specs, others cannot:

Replace with argument_specs (simple "is defined" checks):

  • monitoring_plugins - checks if monitoring_plugins__version is set → required: true
  • monitoring_plugins_grafana_dashboards - checks if monitoring_plugins_grafana_dashboards__repo_version is set → required: true

Keep as assert (argument_specs does not support value ranges, regex patterns, or cross-variable validation):

  • elasticsearch - validates watermark ranges (0 ≤ low < high < flood_stage)
  • lvm - validates that size does not start with + or -

As a rule of thumb: argument_specs handles required, type, choices, and default. Anything beyond that (value ranges, regex, cross-variable dependencies) stays as assert in the tasks.

Implementation order

  1. Start with pre_tasks in the most critical playbooks (setup_basic, setup_nextcloud, setup_icinga2_master, etc.).
  2. Add meta/argument_specs.yml to roles as they are modified.
  3. Migrate simple "is defined" asserts to argument_specs.
  4. Document the approach in CONTRIBUTING.md.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions