AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

Ansible Variable Dependencies: Handle Changes Without Breaking

By Luca Berton · Published 2024-01-01 · Category: troubleshooting

How to handle Ansible variable changes without breaking dependencies. Use defaults, deprecation patterns, backward compatibility, and variable validation.

Ansible Variable Dependencies: Handle Changes Without Breaking

Understanding the Issue: Breaking Dependencies with Variable Changes

Changing a primary variable in a configuration or script can unintentionally break dependent variables or components. Let’s dive into the problem and explore strategies to mitigate these issues.

Problem Analysis

Variable Dependency: If foo is the primary variable and foo.bar is derived or dependent on it, altering foo may invalidate foo.bar if it relies on a specific structure or value in foo. Dynamic Referencing: Other variables or functions referencing foo.bar without a fallback mechanism or proper error handling may fail when foo is modified. Scope or Mutability: In some languages or systems, changes to a primary variable may propagate unexpectedly, impacting dependent variables globally.

---

Solutions

1. Default Values and Fallbacks

Ensure dependent variables like foo.bar have a default value or fallback mechanism to handle changes in foo.
vars:
  foo:
    bar: ${foo.default_bar | default("fallback_value")}

2. Validation

Validate changes to foo before applying them. Ensure foo maintains the correct structure or format to prevent foo.bar from breaking.

Example in Python:

if "bar" not in foo or foo["bar"] is None:
    foo["bar"] = "fallback_value"

3. Isolate Dependencies

Refactor configurations so dependent variables don’t rely directly on mutable states in foo.
vars:
  foo:
    bar: "default_value"
  dependent_var: ${foo.bar}

4. Immutable References

If supported by your system, make foo immutable and create a new variable instead of altering foo directly.

5. Error Handling

Add robust error handling to address situations where foo.bar might not exist or becomes invalid after foo is updated.

6. Debugging

Log or trace how changes to foo propagate to other variables to identify and resolve breaking points.

---

Example: Refactoring for Resilience

Before Change

vars:
  foo:
    bar: "default_value"

After Change

vars:
  foo:
    bar: "new_value"

Problem: If bar is removed or replaced

vars:
  foo: "new_value"
# This breaks dependent variables looking for foo.bar

Refactored Solution

vars:
  foo:
    bar: ${foo.bar | default("fallback_value")}

---

By applying these strategies, you can ensure your configurations and scripts are resilient to changes in primary variables. Whether you're working in YAML, Python, or another environment, adopting these practices will save time and prevent unexpected failures.

Let me know if you need tailored advice for specific tools or systems like Terraform, Ansible, or others!

See also: Leveraging Poetry for Efficient Virtual Environment Management

The Problem

When a variable used by multiple roles or playbooks changes, it can break everything that depends on it:

# Before: variable name was db_host
# After: renamed to database_hostname
# Result: 15 roles break because they reference db_host

Safe Variable Refactoring Strategies

Strategy 1: Add alias with deprecation

# group_vars/all.yml
database_hostname: db.example.com  # New name

# In roles, support both names db_host: "{{ database_hostname | default(db_host_legacy) }}" db_host_legacy: "" # Will be removed in v3.0

Strategy 2: Default fallback chain

# In role defaults/main.yml
app_db_host: "{{ database_hostname | default(db_host, true) | default('localhost') }}"

This checks: database_hostnamedb_host'localhost'

Strategy 3: Validate required variables

- name: Validate required variables
  ansible.builtin.assert:
    that:
      - database_hostname is defined
      - database_hostname | length > 0
      - database_port is defined
    fail_msg: "Required database variables not set! Check group_vars."
    quiet: true

See also: Ansible troubleshooting - Error markupsafe

Ansible Variable Precedence (22 levels)

From lowest to highest priority:

| Priority | Source | Overrides | |----------|--------|-----------| | 1 | Command line values | — | | 2 | Role defaults (defaults/main.yml) | Lowest | | 3 | Inventory group_vars | ↑ | | 4 | Inventory host_vars | ↑ | | 5 | Playbook group_vars | ↑ | | 6 | Playbook host_vars | ↑ | | 7 | vars_files | ↑ | | 8 | Play vars | ↑ | | 9 | Role vars (vars/main.yml) | ↑ | | 10 | set_fact / register | ↑ | | 11 | include_vars | ↑ | | 12 | Extra vars (-e) | Highest |

Key rule: Use defaults/main.yml for values users should override. Use vars/main.yml for values that shouldn't be changed.

Best Practices

1. Prefix role variables

# ❌ Collision-prone
port: 8080
log_dir: /var/log

# ✅ Namespaced myapp_port: 8080 myapp_log_dir: /var/log/myapp

2. Document variable contracts

# defaults/main.yml
---
# myapp_port: (int) HTTP port for the application
# Required: no (default: 8080)
# Changed in: v2.0 (renamed from app_port)
myapp_port: 8080

3. Use assert for critical variables

- name: Pre-flight variable checks
  ansible.builtin.assert:
    that:
      - myapp_version is defined
      - myapp_version is version('2.0', '>=')
      - myapp_env in ['dev', 'staging', 'prod']
    fail_msg: "Invalid configuration. Check variables."

See also: Efficient YouTube Playlist Video Metadata Extraction

FAQ

How do I find all places a variable is used?

grep -r "db_host" roles/ group_vars/ host_vars/ *.yml

Can I set different values per environment?

Yes — use group_vars per environment:

inventory/
  production/
    group_vars/
      all.yml  # database_hostname: prod-db.example.com
  staging/
    group_vars/
      all.yml  # database_hostname: staging-db.example.com

What's the safest way to rename a variable?

Add the new name alongside the old one Update the role to check both: new_var | default(old_var) Update all consumers over time Remove the old name in a future release

The Problem

# Version 1 of your role
app_port: 8080

# Version 2 — you want to rename it app_listen_port: 8080 # Better name, but existing users use app_port!

Backward-Compatible Rename

# defaults/main.yml
app_listen_port: 8080  # New name

# tasks/main.yml - set_fact: _app_port: "{{ app_port | default(app_listen_port) }}" # Old name takes priority if set, falls back to new name

- debug: msg: "Listening on {{ _app_port }}"

# Warn about deprecated name - debug: msg: "WARNING: 'app_port' is deprecated, use 'app_listen_port'" when: app_port is defined

Deprecation Pattern

# vars/deprecation.yml
_deprecated_vars:
  - { old: app_port, new: app_listen_port }
  - { old: db_server, new: database_host }
  - { old: ssl_on, new: enable_tls }

# tasks/check-deprecated.yml - name: Warn about deprecated variables debug: msg: "DEPRECATED: '{{ item.old }}' → use '{{ item.new }}' instead" loop: "{{ _deprecated_vars }}" when: lookup('vars', item.old, default='__unset__') != '__unset__'

Variable Validation

# Validate required variables at play start
- name: Validate configuration
  assert:
    that:
      - app_listen_port is defined
      - app_listen_port | int > 0
      - app_listen_port | int < 65536
      - database_host is defined
      - database_host | length > 0
    fail_msg: |
      Invalid configuration:
      app_listen_port: {{ app_listen_port | default('NOT SET') }}
      database_host: {{ database_host | default('NOT SET') }}

Coalesce Pattern

# Check multiple variable names, use first defined
- set_fact:
    _db_host: "{{ database_host | default(db_host) | default(db_server) | default('localhost') }}"
    _db_port: "{{ database_port | default(db_port) | default(5432) }}"

Role Variable Interface

# defaults/main.yml — Document your API
---
# Required
# app_name: (must be set by user)

# Optional with defaults app_listen_port: 8080 app_workers: "{{ ansible_processor_vcpus }}" app_log_level: info app_data_dir: "/opt/{{ app_name }}/data"

# Deprecated (will be removed in v3.0) # app_port → use app_listen_port # db_server → use database_host

Computed Variables (Derived)

# vars/main.yml — computed, not user-facing
_app_config_path: "/etc/{{ app_name }}/config.yml"
_app_service_name: "{{ app_name }}.service"
_app_url: "{{ 'https' if enable_tls else 'http' }}://{{ ansible_fqdn }}:{{ _app_port }}"

Migration Playbook

# When refactoring variables across environments
- hosts: all
  tasks:
    - name: Migrate old variable format
      set_fact:
        new_config:
          listen_port: "{{ old_config.port | default(8080) }}"
          workers: "{{ old_config.num_workers | default(4) }}"
          tls: "{{ old_config.ssl | default(false) }}"
      when: old_config is defined

Variable Precedence Strategy

Role defaults    → Safest place for defaults (easily overridden)
Group vars       → Environment-specific (dev/staging/prod)
Host vars        → Per-host overrides
Extra vars       → Command-line overrides (highest priority)

FAQ

How do I remove a variable safely?

Add deprecation warning (one release cycle) Fall back to old name with default() Remove in next major version

Can I enforce variable types?

- assert:
    that:
      - app_port | int == app_port  # Must be integer
      - enable_ssl | bool == enable_ssl  # Must be boolean

Should I prefix internal variables?

Yes — use _ prefix (e.g., _computed_value) for internal role variables to distinguish from the user-facing API.

Related Articles

become directives in Ansible

Category: troubleshooting

Browse all Ansible tutorials · AnsiblePilot Home