This project provides a powerful and reusable Ansible pattern called "Flexible Profiles" (FP). The core idea is to create a generic engine role (fp) that other "consumer" roles can use to manage their configurations across different environments (e.g., operating systems, deployment stages like DEV/PROD) in a clean, layered, and maintainable way.
This approach centralizes environment-specific logic and variables, keeping the main role tasks clean and free of excessive conditional statements.
For a detailed introduction to the concepts behind this pattern, please see the Flexible Profiles for Ansible Roles: Easy Management of Variables and Handlers.
- The Engine (fp role): This is the generic, reusable core. It is responsible for reading the active profile, loading variables from different layers, handling overrides, and exporting the final configuration variables for the consumer role to use.
- The Consumer Roles (myapp, myapp02, etc.): These are the roles that implement business logic. They consume the fp engine to abstract away their environment-specific configurations.
- Profiles: A profile is a collection of settings for a specific target. This could represent a RedHat-9 operating system, a Windows-2022 server, or a deployment environment like UAT or PROD.
Flexible Profiles uses a three-layered system for defining variables. This provides a clear order of operations for default values, calculated variables, and just-in-time expressions. A consumer role can choose to use any or all of these layers by providing a path to the corresponding variable directory or by skipping a layer by setting its path to "skip".
This layer provides the most basic, static, overridable default values for a profile. These are plain key-value pairs.
- Example: roles/myapp/vars/fp/fp_defaults/RedHat-9.yml
_fp_exists: true
service_name: "httpd"
config_dir: "/etc/httpd"This layer contains variables that are calculated immediately ("eagerly") when the fp role runs. They are useful for building variables that depend on values from the defaults layer.
- Example: roles/myapp03/vars/fp/fp_instant_expressions/WindowsProd.yml
_fp_exists: true
# This expression builds upon 'myapp03_api_server' from the defaults layer.
api_endpoint: "https://{{ myapp03_api_server }}/v1/data"This layer contains variables whose values are stored as unrendered Jinja2 templates. They are evaluated "lazily" every time they are accessed in a task. This is extremely powerful for values that depend on runtime facts or variables that may be defined or changed after the fp role has already run.
- Example: roles/myapp04/vars/fp/fp_deferred_expressions/PROD.yml
_fp_exists: true
# The value of 'myapp04_app_version' is not known when this is loaded.
# It will be resolved later, using the final, overridden value of the variable.
welcome_message: "Welcome to PROD environment. App version is {{ myapp04_app_version }}."To make a role a "consumer" of the FP engine, you include the fp role and provide it with a set of specific inputs.
Typically done in an init_fp.yml file which is then included by the consumer's main.yml.
# In roles/myapp/tasks/init_fp.yml
- name: MYAPP | init_fp.yml | Load Flexible Profiles for myapp
ansible.builtin.include_role:
name: fp
vars:
# ... FP inputs go here ...The fp role is configured using the following variables passed to include_role:
- fp_consumer_role_name (string, required): The name of your role (e.g., "myapp"). Used for internal namespacing.
- fp_export_with_consumer_role_prefix (boolean, optional, default: false): If true, all exported variables will be prefixed with the fp_consumer_role_name (e.g., service_name becomes myapp_service_name).
- Important: When this is set to true, any variable overrides (e.g., in playbook vars: or inventory) and any variables used within expression files must also use the prefix. For example, in an instant expression, you must use {{ myapp_api_server }} not {{ api_server }}.
- fp_active_profile (string, required): The name of the profile to activate (e.g., "PROD"). This is often determined dynamically using Ansible facts or other input variables.
- fp_profile_names (list, required): A list of all valid profile names for this role. The fp role will fail if fp_active_profile is not in this list.
- fp_defaults_path, fp_instant_expressions_path, fp_deferred_expressions_path (string, required): The absolute paths to the three variable layer directories. Set a path to "skip" to disable that layer.
- Default and Instant Variables: Access them directly in your tasks using their prefixed (or unprefixed) names (e.g., {{ myapp_service_name }}).
- Deferred Variables: These are stored inside a special dictionary named {{ fp_consumer_role_name }}_fp_deferred. To render the expression, you access the key within that dictionary (e.g., {{ myapp_fp_deferred.welcome_message }}).
This repository includes several examples, each demonstrating a different feature.
This example demonstrates fact-based profile selection, prefixed variables, and profile-specific handlers.
- Profile Selection: Automatically switches between "RedHat-9" and "Windows-2022" based on the ansible_os_family fact. It also includes a fallback profile that is used if the OS family does not match, preventing the role from failing on unknown systems.
- Features: Uses all three variable layers and shows how a notify action can trigger a handler that is specific to the active profile.
- Command:
ansible-playbook test_myapp.ymlThis example shows how to switch profiles based on a simple boolean flag and exports variables without a prefix.
- Profile Selection: Switches between "RHEL" and "RHEL_legacy" based on the rhel_legacy_mode boolean.
- Features: Skips the expression layers. It also highlights an important consideration: when not using prefixes, variables from one profile can persist in Ansible's fact cache.
- Command: These tests must be run as separate commands.
# Test the modern profile
ansible-playbook test_myapp02a.yml
# Test the legacy profile
ansible-playbook test_myapp02b.ymlThis example demonstrates a common Dev vs. Prod setup using the instant expressions layer.
- Profile Selection: Switches between "WindowsProd" and "WindowsDev" based on the non_prod boolean.
- Features: Uses instant expressions to dynamically construct an api_endpoint URL based on the environment.
- Command:
# Test the production profile
ansible-playbook test_myapp03a.yml
# Test the development profile
ansible-playbook test_myapp03b.ymlThis advanced example demonstrates the full variable override precedence chain.
- Variable Precedence Order (from lowest to highest):
- Profile Default: The base value from the active profile's fp_defaults file.
- Inventory: A value defined in an inventory file.
- Playbook vars: A value defined in the vars: section of the playbook.
- Role vars: A value passed directly to the include_role task's vars: section (highest precedence).
- Features: Shows how a variable like api_key or db_host can be defined in a profile but overridden at multiple higher levels for ultimate flexibility.
- Command: These tests must be run with the -i inventory.yml flag.
# Test UAT profile with playbook-level override
ansible-playbook -i inventory.yml test_myapp04a.yml
# Test PROD profile with playbook and role-level overrides
ansible-playbook -i inventory.yml test_myapp04b.ymlThis project uses Molecule for isolated, container-based testing of the fp role's internal logic. This ensures the core engine is robust and predictable. The test scenarios are located in roles/fp/molecule/ and cover different aspects of the role's functionality:
- defaults_and_prefixing: Tests basic variable loading, with and without prefixes.
- deferred_and_precedence: Verifies that variable override precedence works correctly and that deferred expressions use the final, highest-precedence values.
- invalid_input_validation: Ensures the role fails predictably and with clear error messages when given bad data.
- layering_and_combination: A dedicated test to ensure all three variable layers interact correctly with each other.
- test_ansible_handlers: Tests the integration with Ansible handlers, ensuring that profile-specific handlers are correctly triggered and that profile variables are available within their scope.
- unprefixed_variable_clash: An explicit test to confirm the variable caching behavior when not using prefixes.
To run a specific test scenario, navigate to the fp role directory and use the molecule command.
# Navigate to the role directory
cd roles/fp
# Run the full test sequence for the 'invalid_input_validation' scenario
molecule test -s invalid_input_validationThis project uses ansible-lint to maintain code quality and adherence to best practices.
The .ansible-lint file is used to explicitly suppress certain linting warnings that are not applicable to this project's design. For example, the var-naming[no-role-prefix] rule is ignored. This is intentional, as profile variables are designed to be generic within their files before the fp role optionally applies a prefix.
To run the linter on the entire project, execute the following command from the project's root directory:
ansible-lint