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

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.