Conditional Default Variables in Ansible: Dynamically Setting composer_opts Based on env Value


3 views

When working with Ansible roles, we often encounter situations where default variables need to adapt based on other variables' values. A common scenario involves dependent configurations like environment settings (env) and their corresponding command options (composer_opts).

# Default values that work together
env: prod
composer_opts: "--no-dev --optimize-autoloader --no-interaction"

The challenge arises when users change the env to dev but forget to adjust composer_opts. The production-optimized defaults then break the development environment setup.

# Problem scenario
env: dev  # Changed from default
composer_opts: "--no-dev"  # Still using production default - breaks dev setup!

Ansible provides several ways to implement smart defaults that adjust based on other variables:

Solution 1: Using the default Filter with Conditionals

# defaults/main.yml
env: prod
composer_opts: "{{ '--no-dev --optimize-autoloader --no-interaction' if env == 'prod' else '' }}"

Solution 2: Jinja2 Conditional Expressions in vars/

# vars/main.yml
composer_opts: >-
  {% if env == 'prod' %}
    --no-dev --optimize-autoloader --no-interaction
  {% else %}
    # Empty string for dev
  {% endif %}

Solution 3: Combining defaults with set_fact

# tasks/main.yml
- name: Set smart composer_opts default
  ansible.builtin.set_fact:
    composer_opts: "{{ composer_opts | default(env_opts_mapping[env]) }}"
  vars:
    env_opts_mapping:
      prod: "--no-dev --optimize-autoloader --no-interaction"
      dev: ""
      test: "--no-dev"

All these solutions respect existing variable assignments while providing smart defaults:

# Example playbook that overrides defaults
vars:
  env: dev
  composer_opts: "--profile"  # User's specific setting takes precedence

For complex scenarios, consider creating a custom filter plugin:

# filter_plugins/smart_defaults.py
def smart_composer_opts(env, current_value=None):
    if current_value:
        return current_value
    return "--no-dev --optimize-autoloader --no-interaction" if env == "prod" else ""

class FilterModule(object):
    def filters(self):
        return {'smart_composer_opts': smart_composer_opts}

Usage in playbook:

composer_opts: "{{ env | smart_composer_opts(composer_opts) }}"
  • Document your conditional defaults clearly in role documentation
  • Consider adding input validation for the env variable
  • Test all possible combinations of explicit and default values

In Ansible role development, we often encounter situations where default variables need to adapt based on other variable values. A common scenario involves interdependent configuration parameters where changing one should intelligently affect another - but only when the second variable isn't explicitly set.

Consider these related variables for a Composer deployment:

# defaults/main.yml
env: prod
composer_opts: "--no-dev --optimize-autoloader --no-interaction"

When env: prod, the default composer_opts works perfectly. But if we switch to env: dev while keeping the default options, we get broken behavior since development dependencies should be installed.

Ansible provides several ways to implement conditional defaults:

Method 1: Using the default filter with conditions

# defaults/main.yml
env: prod
composer_opts: "{{ '' if env == 'dev' else '--no-dev --optimize-autoloader --no-interaction' }}"

Method 2: Separate variable files with includes

# defaults/main.yml
env: prod
composer_opts: "{{ composer_opts_default }}"

# vars/dev.yml
composer_opts_default: ""

# vars/prod.yml  
composer_opts_default: "--no-dev --optimize-autoloader --no-interaction"

To ensure explicit variable settings always take precedence:

# tasks/main.yml
- name: Set dynamic composer options
  set_fact:
    final_composer_opts: >-
      {% if composer_opts is defined and composer_opts != omit %}
        {{ composer_opts }}
      {% else %}
        {{ '' if env == 'dev' else '--no-dev --optimize-autoloader --no-interaction' }}
      {% endif %}
  • Document all dynamic default behaviors in role documentation
  • Use clear variable naming (like _default suffix)
  • Test all conditional branches in molecule tests
  • Consider using omit for truly optional parameters

For complex logic, create a custom filter plugin:

# filter_plugins/dynamic_defaults.py
def dynamic_composer_opts(env, current_value=None):
    if current_value:
        return current_value
    return '' if env == 'dev' else '--no-dev --optimize-autoloader --no-interaction'

class FilterModule(object):
    def filters(self):
        return {'dynamic_composer_opts': dynamic_composer_opts}

Usage in playbook:

composer_opts: "{{ env | dynamic_composer_opts(composer_opts) }}"