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
- Start with
pre_tasks in the most critical playbooks (setup_basic, setup_nextcloud, setup_icinga2_master, etc.).
- Add
meta/argument_specs.yml to roles as they are modified.
- Migrate simple "is defined" asserts to
argument_specs.
- Document the approach in CONTRIBUTING.md.
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:
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_tasksis 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_specsvalidates 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 wherepre_tasksare missing.In practice,
pre_tasksis the primary safety net.argument_specsis a secondary guardrail that comes for free once defined.1.
pre_tasksin playbooks (quick win)Each playbook knows which roles it calls and which mandatory variables those roles expect. A single
assertblock inpre_tasksvalidates everything before the first change is applied. This is also the right place to check external service dependencies like Bitwarden or Vault.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.ymlper role (long-term)Available since Ansible 2.11. Ansible validates
argument_specsautomatically at role entry, before any tasks run. This catches type mismatches and missing required variables without writing manual asserts.Introduce
argument_specsincrementally when roles are touched anyway. No need to do all 160 at once.Migrating existing
ansible.builtin.assertusageSome existing asserts can be replaced by
argument_specs, others cannot:Replace with
argument_specs(simple "is defined" checks):monitoring_plugins- checks ifmonitoring_plugins__versionis set →required: truemonitoring_plugins_grafana_dashboards- checks ifmonitoring_plugins_grafana_dashboards__repo_versionis set →required: trueKeep as
assert(argument_specsdoes not support value ranges, regex patterns, or cross-variable validation):elasticsearch- validates watermark ranges (0 ≤ low < high < flood_stage)lvm- validates thatsizedoes not start with+or-As a rule of thumb:
argument_specshandlesrequired,type,choices, anddefault. Anything beyond that (value ranges, regex, cross-variable dependencies) stays asassertin the tasks.Implementation order
pre_tasksin the most critical playbooks (setup_basic,setup_nextcloud,setup_icinga2_master, etc.).meta/argument_specs.ymlto roles as they are modified.argument_specs.