Introduction
Validation in Ansible means checking that variables, data, and inputs meet expected conditions before tasks execute. Without validation, a typo in a variable or a missing required value can silently propagate through your playbook and break production. Ansible provides multiple validation mechanisms: assert for runtime checks, argument_specs for role/module parameter validation, ansible.utils.validate for schema-based validation, and ansible-lint for static analysis.
Validation Methods Overview
Method | When It Runs | Validates What | Best For
──────────────────────|────────────────|────────────────────────|─────────────
ansible.builtin.assert| Runtime | Variable conditions | Quick checks
argument_specs | Role call time | Role parameters | Role APIs
ansible.utils.validate| Runtime | Data against schema | Complex data
ansible-lint | Pre-run (CI) | Playbook syntax/style | Code quality
check mode (--check) | Pre-run | What would change | Dry runs
Validate with assert
The most common validation approach — check conditions and fail with a clear message.
---
- name: Deploy application with validation
hosts: app_servers
vars:
app_version: "3.2.1"
environment: "production"
max_memory_mb: 2048
database_host: "db.example.com"
database_port: 5432
tasks:
- name: Validate required variables
ansible.builtin.assert:
that:
- app_version is defined
- app_version is match('^[0-9]+\.[0-9]+\.[0-9]+$')
- environment in ['development', 'staging', 'production']
- max_memory_mb | int >= 512
- max_memory_mb | int <= 8192
- database_host | length > 0
- database_port | int > 0
- database_port | int < 65536
fail_msg: |
Variable validation failed!
app_version: {{ app_version | default('UNDEFINED') }}
environment: {{ environment | default('UNDEFINED') }}
max_memory_mb: {{ max_memory_mb | default('UNDEFINED') }}
database_host: {{ database_host | default('UNDEFINED') }}
success_msg: "All variables validated successfully"
- name: Validate host meets minimum requirements
ansible.builtin.assert:
that:
- ansible_memtotal_mb >= max_memory_mb
- ansible_processor_vcpus >= 2
- ansible_distribution in ['Ubuntu', 'Debian', 'RedHat', 'CentOS', 'Rocky']
fail_msg: >-
Host {{ inventory_hostname }} does not meet requirements:
RAM: {{ ansible_memtotal_mb }}MB (need {{ max_memory_mb }}MB),
CPUs: {{ ansible_processor_vcpus }} (need 2+),
OS: {{ ansible_distribution }}
Validate with argument_specs (Roles)
Define expected parameters for your role with types, defaults, and descriptions.
# roles/deploy_app/meta/argument_specs.yml
---
argument_specs:
main:
short_description: Deploy application to target hosts
description:
- Deploys the application with configurable version, environment, and resource limits.
options:
app_version:
type: str
required: true
description: Semantic version string (e.g., "3.2.1")
environment:
type: str
required: true
choices:
- development
- staging
- production
description: Target deployment environment
max_memory_mb:
type: int
required: false
default: 1024
description: Maximum memory allocation in MB
database_config:
type: dict
required: true
description: Database connection configuration
options:
host:
type: str
required: true
port:
type: int
default: 5432
name:
type: str
required: true
ssl:
type: bool
default: true
allowed_ips:
type: list
elements: str
required: false
default: []
description: List of IP addresses allowed to access the app
features:
type: dict
required: false
default:
enable_cache: true
enable_debug: false
description: Feature flags
# Using the role — Ansible validates parameters automatically
- name: Deploy app
hosts: app_servers
roles:
- role: deploy_app
app_version: "3.2.1"
environment: "production"
database_config:
host: "db.example.com"
name: "myapp_prod"
# max_memory_mb uses default (1024)
# allowed_ips uses default ([])
Validate with ansible.utils.validate (JSON Schema)
For complex data structures, validate against a JSON Schema.
---
- name: Validate complex configuration data
hosts: localhost
gather_facts: false
vars:
app_config:
name: "myapp"
replicas: 3
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: metrics
containerPort: 9090
protocol: TCP
resources:
limits:
cpu: "500m"
memory: "512Mi"
healthCheck:
path: "/health"
port: 8080
initialDelaySeconds: 30
tasks:
- name: Validate config against JSON Schema
ansible.utils.validate:
data: "{{ app_config }}"
criteria:
- engine: ansible.utils.jsonschema
criteria:
type: object
required:
- name
- replicas
- ports
properties:
name:
type: string
minLength: 1
replicas:
type: integer
minimum: 1
maximum: 10
ports:
type: array
minItems: 1
items:
type: object
required: [name, containerPort]
properties:
containerPort:
type: integer
minimum: 1
maximum: 65535
Validate Files Exist Before Using Them
- name: Validate all required files exist
ansible.builtin.stat:
path: "{{ item }}"
loop:
- /etc/app/config.yml
- /etc/app/ssl/cert.pem
- /etc/app/ssl/key.pem
- /opt/app/bin/server
register: file_checks
- name: Fail if any required file is missing
ansible.builtin.assert:
that: item.stat.exists
fail_msg: "Required file missing: {{ item.item }}"
loop: "{{ file_checks.results }}"
loop_control:
label: "{{ item.item }}"
Validate Network Connectivity
- name: Validate database connectivity
ansible.builtin.wait_for:
host: "{{ database_host }}"
port: "{{ database_port }}"
timeout: 10
register: db_check
- name: Validate external API reachable
ansible.builtin.uri:
url: "https://api.example.com/health"
method: GET
status_code: 200
timeout: 10
register: api_check
failed_when: api_check.status != 200
Validate with ansible-lint
# Install
pip install ansible-lint
# Run on playbook
ansible-lint deploy.yml
# Run on entire project
ansible-lint
# Common validation rules:
# - yaml[truthy]: Use true/false, not yes/no
# - no-changed-when: Commands need changed_when
# - fqcn: Use fully qualified collection names
# - name[missing]: All tasks should have names
# - risky-shell-pipe: Shell pipes need set -o pipefail
# .ansible-lint configuration
---
profile: production # strictest profile
warn_list:
- experimental
skip_list:
- role-name # Allow non-standard role names
Validate Playbook Before Running (Check Mode)
# Dry run — shows what would change without making changes
ansible-playbook deploy.yml --check --diff
# Syntax check only (fastest)
ansible-playbook deploy.yml --syntax-check
# List all tasks without running
ansible-playbook deploy.yml --list-tasks
# List all hosts that would be affected
ansible-playbook deploy.yml --list-hosts
Common Mistakes
# ❌ MISTAKE 1: No validation before destructive operations
- name: Drop and recreate database
community.postgresql.postgresql_db:
name: "{{ db_name }}"
state: absent
# FIX: Validate environment first!
- name: SAFETY CHECK — refuse to drop production DB
ansible.builtin.assert:
that: environment != 'production'
fail_msg: "REFUSING to drop database in production!"
# ❌ MISTAKE 2: Validating after the task runs (too late)
- name: Deploy config
ansible.builtin.template:
src: config.j2
dest: /etc/app/config.yml
- name: Check if config is valid # ← Too late if template was wrong!
# FIX: Validate variables BEFORE the template task
# ❌ MISTAKE 3: assert with wrong types
- name: Bad comparison
ansible.builtin.assert:
that:
- port > 0 # ← port might be a string "5432", not int
# FIX:
- port | int > 0
# ❌ MISTAKE 4: Not using fail_msg
- name: Check variables
ansible.builtin.assert:
that: app_version is defined
# Error: "Assertion failed" — useless!
# FIX: Always include fail_msg with context
Related Articles
- Ansible assert Module
- Ansible Error Handling
- Ansible ansible-lint Guide
- Ansible Playbook Best Practices
- Ansible wait_for Module
Conclusion
Validate early, validate often. Use assert for quick runtime checks on variables and host facts, argument_specs to define clear role APIs with type checking, ansible.utils.validate for complex data against JSON Schema, and ansible-lint in CI to catch issues before they reach production. The few minutes spent writing validation saves hours of debugging broken deployments.