Chapter 4: Advanced Ansible: Roles, Collections, and Vault

Introduction

In previous chapters, we laid the groundwork for network automation using Ansible, covering basic playbooks, inventories, and modules. As automation initiatives mature and scale within a NetDevOps framework, the need for modularity, reusability, and robust security becomes paramount. This chapter dives into advanced Ansible concepts: Roles, Collections, and Vault.

Ansible Roles provide a structured way to organize automation content, promoting code reusability and simplifying complex workflows. Ansible Collections offer a standardized format for packaging and distributing Ansible content, including roles, modules, and plugins, enhancing collaboration and multi-vendor support. Finally, Ansible Vault addresses the critical security requirement of managing sensitive data (such as API keys, passwords, and secrets) securely within your Infrastructure as Code (IaC) pipelines.

After completing this chapter, you will be able to:

  • Structure your Ansible projects using roles for improved organization and reusability.
  • Leverage Ansible Collections to access a rich ecosystem of pre-built automation content and share your own.
  • Securely manage sensitive information in your Ansible playbooks using Ansible Vault.
  • Apply these advanced techniques to build more robust, scalable, and secure network automation solutions across Cisco and multi-vendor environments.

Technical Concepts

4.1 Ansible Roles: Structuring Your Automation

Ansible roles are the cornerstone of modular and reusable automation. A role is a self-contained unit of automation that organizes tasks, variables, handlers, templates, and files into a predefined directory structure. This structure enforces consistency and allows roles to be easily shared and reused across different projects or even entire organizations.

The core principle behind roles is separation of concerns. Instead of monolithic playbooks, you break down automation logic into smaller, manageable roles, each responsible for a specific aspect of configuration (e.g., “configure_interfaces”, “deploy_ospf”, “setup_vlans”).

Role Directory Structure:

roles/
└── my_network_role/
    ├── tasks/               # Main tasks for the role (main.yml)
    │   └── main.yml
    │   └── configure_interface.yml
    ├── handlers/            # Handlers (triggered by tasks) (main.yml)
    │   └── main.yml
    ├── defaults/            # Default variables for the role (main.yml)
    │   └── main.yml
    ├── vars/                # Other variables for the role (main.yml)
    │   └── main.yml
    ├── files/               # Static files copied to target (e.g., firmware)
    │   └── my_config_template.j2
    ├── templates/           # Jinja2 templates rendered on target
    │   └── device_banner.j2
    ├── meta/                # Role metadata (dependencies, author, license)
    │   └── main.yml
    └── README.md            # Documentation for the role

Key Components and Their Purpose:

  • tasks/: Contains the main playbook tasks executed by the role. main.yml is the entry point, but you can include other task files for better organization (e.g., tasks/configure_interfaces.yml, tasks/verify_routing.yml).
  • handlers/: Tasks that are only run when explicitly notified by another task. Handlers are typically used for service restarts or reloading configurations.
  • defaults/: Variables defined here have the lowest precedence. They provide default values that can be easily overridden by variables from other sources (inventory, group_vars, host_vars, extra-vars). This is ideal for role-specific parameters that most users will accept but can customize.
  • vars/: Variables defined here have higher precedence than defaults/. They are typically used for variables that are essential to the role’s function and less likely to be overridden by external sources.
  • files/: Contains static files that the role might need to copy to remote hosts (e.g., IOS images, license files).
  • templates/: Houses Jinja2 templates. These files are rendered dynamically by Ansible on the control node before being transferred to the target host. This is crucial for generating device configurations based on variables.
  • meta/: Defines role metadata, including dependencies on other roles, author information, and license.

Using Roles in Playbooks: You can include roles in a playbook using the roles directive:

---
- name: Deploy network configuration using roles
  hosts: network_devices
  gather_facts: false
  connection: network_cli

  roles:
    - common_device_setup
    - configure_vlans
    - deploy_ospf

For more dynamic or conditional role execution, include_role and import_role directives can be used within tasks. import_role is static (processed at playbook parsing), while include_role is dynamic (processed at runtime), allowing for loops and conditions.

Network Diagram: Ansible Role Architecture This diagram illustrates the internal structure of an Ansible Role.

@startuml
skinparam node {
    BackgroundColor LightBlue
    BorderColor DarkBlue
    FontColor DarkBlue
    RoundCorner 10
}
skinparam folder {
    BackgroundColor LightGreen
    BorderColor DarkGreen
    FontColor DarkGreen
}

cloud "Ansible Control Node" as control_node {
  folder "ansible_project/" as project {
    folder "playbooks/" as playbooks {
      file "site.yml" as site_yml
    }
    folder "roles/" as roles_folder {
      folder "configure_interfaces/" as if_role {
        folder "tasks/" as if_tasks {
          file "main.yml" as if_tasks_main
          file "ios.yml" as if_tasks_ios
          file "junos.yml" as if_tasks_junos
        }
        folder "handlers/" as if_handlers {
          file "main.yml" as if_handlers_main
        }
        folder "defaults/" as if_defaults {
          file "main.yml" as if_defaults_main
        }
        folder "vars/" as if_vars {
          file "main.yml" as if_vars_main
        }
        folder "templates/" as if_templates {
          file "interface.j2" as if_template
        }
        folder "meta/" as if_meta {
          file "main.yml" as if_meta_main
        }
      }
      folder "deploy_ospf/" as ospf_role {
        ...
      }
    }
  }
}

site_yml [label="> if_role : uses role
if_role"] if_tasks : executes
if_tasks [label="> if_handlers : notifies
if_tasks"] if_defaults : uses defaults
if_tasks [label="> if_vars : uses vars
if_tasks"] if_templates : renders templates

@enduml

4.2 Ansible Collections: Packaging and Distribution

Ansible Collections represent the future of Ansible content distribution. Introduced to provide a standardized, robust, and extensible way to package and share Ansible modules, plugins, roles, and playbooks. They solve several challenges faced by older methods (like ansible-galaxy roles):

  • Multi-vendor Support: Collections often bundle modules and roles specifically designed for particular network vendors (e.g., cisco.ios, juniper.junos, arista.eos).
  • Namespacing: Collections provide a clear namespace (namespace.collection_name) preventing naming collisions and making content discovery easier.
  • Dependencies: Collections can declare dependencies on other collections, ensuring all required components are installed.
  • Versioning: Easier management of content versions, allowing users to consume specific versions of a collection.

A collection is essentially a directory structure with a galaxy.yml metadata file, packaged into a .tar.gz archive.

Collection Naming Convention (FQCN): Every piece of content within a collection is referenced by its Fully Qualified Collection Name (FQCN), which follows the pattern namespace.collection_name.content_name. For example:

  • cisco.ios.ios_interface (module)
  • community.general.ping (module)
  • ansible.builtin.shell (module from ansible.builtin collection)

Installing Collections: Collections are typically installed using ansible-galaxy collection install:

ansible-galaxy collection install cisco.ios juniper.junos arista.eos

They can also be specified in a requirements.yml file:

# collections/requirements.yml
collections:
  - name: cisco.ios
    version: ">=2.0.0"
  - name: juniper.junos
    version: "==2.1.0"
  - name: arista.eos

Network Diagram: Ansible Collection Architecture This diagram illustrates how collections encapsulate various Ansible content and are used in playbooks.

namespace: {
  shape: cylinder
  label: "Namespace (e.g., cisco)"
  collection: {
    shape: rectangle
    label: "Collection (e.g., ios)"
    modules: {
      shape: folder
      label: "Modules"
      module1: { label: "ios_interface" }
      module2: { label: "ios_config" }
    }
    roles: {
      shape: folder
      label: "Roles"
      role1: { label: "vlan_config" }
      role2: { label: "ospf_setup" }
    }
    plugins: {
      shape: folder
      label: "Plugins"
      plugin1: { label: "cli_parse" }
    }
  }
}

playbook: {
  shape: document
  label: "Ansible Playbook"
}

inventory: {
  shape: database
  label: "Inventory"
}

playbook -> namespace.collection.modules: uses FQCN
playbook -> namespace.collection.roles: uses FQCN
playbook -> inventory: targets devices

4.3 Ansible Vault: Secure Secrets Management

In any automation endeavor, especially when dealing with network devices, sensitive data such as API keys, CLI passwords, SNMP community strings, and certificates are inevitable. Storing these credentials in plain text within your IaC repository is a major security risk. Ansible Vault provides a robust solution for encrypting these sensitive variables and files.

Ansible Vault uses AES256 encryption with a user-provided password. When a playbook needs to access vaulted data, Ansible prompts for the vault password (or reads it from a file/environment variable), decrypts the data in memory, uses it, and then discards the decrypted version. The actual vaulted files remain encrypted on disk.

Key Ansible Vault Commands:

  • ansible-vault create FILENAME: Creates a new encrypted file.
    ansible-vault create group_vars/all/vault.yml
    
  • ansible-vault edit FILENAME: Edits an existing encrypted file.
    ansible-vault edit group_vars/all/vault.yml
    
  • ansible-vault encrypt FILENAME: Encrypts an existing plain-text file.
    ansible-vault encrypt host_vars/my_router/credentials.yml
    
  • ansible-vault decrypt FILENAME: Decrypts an existing encrypted file back to plain text (use with caution).
    ansible-vault decrypt host_vars/my_router/credentials.yml
    
  • ansible-vault rekey FILENAME: Changes the encryption password for an existing vaulted file.
    ansible-vault rekey group_vars/all/vault.yml
    
  • ansible-vault encrypt_string 'my_secret_password': Encrypts a single string for embedding directly into a playbook or a non-vaulted YAML file.

Integrating Vault with Playbooks: To use vaulted data, simply reference the variable as you normally would. When running ansible-playbook, you’ll need to provide the vault password:

ansible-playbook site.yml --ask-vault-pass

Or, if storing the password in a file (ensure this file is secure and not committed to source control):

ansible-playbook site.yml --vault-password-file ~/.ansible/vault_pass.txt

Vault IDs: For projects with multiple vault files protected by different passwords, Ansible Vault IDs allow you to label and manage them distinctly. This is achieved by adding --vault-id to vault commands and --vault-password-file @prompt or @path_to_password_file to distinguish which vault password belongs to which ID.

Network Diagram: Ansible Vault Workflow This diagram illustrates the secure flow of sensitive data using Ansible Vault.

digraph G {
    rankdir=LR;
    node [shape=box, style=filled, fillcolor=lightgray];
    edge [color=gray, fontcolor=darkgray];

    user [label="Network Engineer"];
    playbook [label="Ansible Playbook"];
    vaulted_file [label="Encrypted secrets (vault.yml)\n(On Disk)", fillcolor=lightcoral];
    ansible_engine [label="Ansible Engine\n(In Memory)", fillcolor=lightblue];
    network_device [label="Network Device"];

    user -> playbook [label="Executes"];
    playbook -> vaulted_file [label="References vaulted vars"];
    user -> ansible_engine [label="Provides Vault Password"];
    vaulted_file -> ansible_engine [label="Encrypted data"];
    ansible_engine -> ansible_engine [label="Decrypts in Memory", style=dotted, dir=none];
    ansible_engine -> network_device [label="Uses decrypted credentials"];
}

RFC/Standard references: While Ansible Vault itself isn’t an RFC, its underlying encryption (AES256) adheres to NIST standards for symmetric key encryption (e.g., FIPS 197). Best practices for secrets management are often outlined by security frameworks like OWASP.

4.4 Multi-Vendor Configuration with Roles and Collections

Ansible’s network modules and collections are designed for multi-vendor environments. When building roles, you can leverage platform-specific tasks using conditionals or by structuring tasks to include files based on the device’s operating system.

Example Role Structure for Multi-Vendor:

roles/
└── configure_interface/
    ├── tasks/
    │   ├── main.yml          # Entry point, includes platform-specific tasks
    │   ├── _ios.yml          # Tasks for Cisco IOS/IOS-XE
    │   ├── _junos.yml        # Tasks for Juniper Junos
    │   └── _eos.yml          # Tasks for Arista EOS
    ├── defaults/
    │   └── main.yml
    └── vars/
        └── main.yml

Within tasks/main.yml, you might have logic like this:

# roles/configure_interface/tasks/main.yml
---
- name: Determine network OS
  set_fact:
    network_os: ""

- name: Include Cisco IOS tasks
  include_tasks: _ios.yml
  when: network_os == 'cisco.ios.ios'

- name: Include Juniper Junos tasks
  include_tasks: _junos.yml
  when: network_os == 'juniper.junos'

- name: Include Arista EOS tasks
  include_tasks: _eos.yml
  when: network_os == 'arista.eos'

Each platform-specific task file (_ios.yml, _junos.yml, _eos.yml) would then use the appropriate modules from the respective collections.

Configuration Examples

This section provides practical Ansible examples demonstrating roles, collections, and vault for multi-vendor network automation.

Scenario: Configure VLANs on Cisco IOS-XE and Juniper Junos devices, using a shared role and sensitive data stored in Ansible Vault.

4.4.1 Project Setup

First, create the project directory and ansible.cfg:

ansible_project/
├── ansible.cfg
├── inventory.yml
├── playbooks/
│   └── site.yml
├── roles/
│   └── configure_vlan/
│       ├── tasks/
│       │   ├── main.yml
│       │   ├── _cisco_ios.yml
│       │   └── _juniper_junos.yml
│       ├── defaults/
│       │   └── main.yml
│       └── vars/
│           └── main.yml
├── group_vars/
│   └── all/
│       └── vault.yml            # Encrypted file for all groups
├── host_vars/
│   ├── cisco_router/
│   │   └── vars.yml
│   └── juniper_switch/
│       └── vars.yml
└── collections/
    └── requirements.yml

ansible.cfg:

[defaults]
inventory = ./inventory.yml
remote_user = ansible_user
private_key_file = ~/.ssh/id_rsa
host_key_checking = false
gathering = smart
collections_paths = ./collections

[privilege_escalation]
become = true
become_method = enable
become_user = root
become_ask_pass = false # Set to true if privilege escalation requires a password

[paramiko_connection]
record_host_keys = false # WARNING: Disables host key checking. Use only for labs or trusted environments.

Security Warning: host_key_checking = false and record_host_keys = false are dangerous in production environments as they disable SSH host key verification, making your connections vulnerable to man-in-the-middle attacks. Always enable host key checking and manage known hosts (~/.ssh/known_hosts) properly in production.

collections/requirements.yml:

collections:
  - name: cisco.ios
  - name: juniper.junos

Install collections: ansible-galaxy collection install -r collections/requirements.yml

inventory.yml:

all:
  children:
    cisco_devices:
      hosts:
        cisco_router:
          ansible_host: 192.168.1.10
          ansible_network_os: cisco.ios.ios
    juniper_devices:
      hosts:
        juniper_switch:
          ansible_host: 192.168.1.20
          ansible_network_os: juniper.junos

group_vars/all/vault.yml (create and encrypt): First, create the file: ansible-vault create group_vars/all/vault.yml Enter a strong password when prompted. Then add the following content (which will be encrypted):

ansible_user: "devnet_user"
ansible_ssh_pass: "YourSecurePassword123!" # This will be encrypted!
ansible_become_pass: "YourEnablePassword!"  # This will be encrypted!

host_vars/cisco_router/vars.yml:

device_name: "Cisco-Router-10"
vlan_id: 100
vlan_name: "ENGINEERING"

host_vars/juniper_switch/vars.yml:

device_name: "Juniper-Switch-20"
vlan_id: 200
vlan_name: "DEVELOPMENT"

4.4.2 Ansible Role: configure_vlan

roles/configure_vlan/tasks/main.yml:

---
- name: Include Cisco IOS VLAN tasks
  ansible.builtin.include_tasks: _cisco_ios.yml
  when: ansible_network_os == 'cisco.ios.ios'

- name: Include Juniper Junos VLAN tasks
  ansible.builtin.include_tasks: _juniper_junos.yml
  when: ansible_network_os == 'juniper.junos'

roles/configure_vlan/tasks/_cisco_ios.yml:

---
- name: Configure VLAN on Cisco IOS-XE
  cisco.ios.ios_config:
    lines:
      - "name "
    parents: "vlan "
    save_when_changed: true
  delegate_to: ""
  connection: network_cli

Note: save_when_changed: true will automatically save the configuration if changes are made. Be cautious with this in production.

roles/configure_vlan/tasks/_juniper_junos.yml:

---
- name: Configure VLAN on Juniper Junos
  juniper.junos.junos_vlans:
    config:
      - name: ""
        vlan_id: ""
    state: merged
  delegate_to: ""
  connection: network_cli

roles/configure_vlan/defaults/main.yml:

---
# Default values for VLAN configuration
# These can be overridden by host_vars or group_vars
vlan_id: 1
vlan_name: "DEFAULT_VLAN"

4.4.3 Main Playbook

playbooks/site.yml:

---
- name: Deploy multi-vendor VLAN configuration
  hosts: all
  gather_facts: false

  roles:
    - configure_vlan

4.4.4 Execution and Verification

To run the playbook:

ansible-playbook playbooks/site.yml --ask-vault-pass

You will be prompted for the vault password.

Verification Commands:

For Cisco IOS-XE (cisco_router):

show vlan brief

Expected Output:

VLAN Name                             Status    Ports
---- -------------------------------- --------- -------------------------------
100  ENGINEERING                      active    

For Juniper Junos (juniper_switch):

show vlans

Expected Output:

VLAN                  Tag        Interfaces
default               1          
DEVELOPMENT           200        

Troubleshooting Note: If you encounter SSH authentication errors, double-check the ansible_user and ansible_ssh_pass in your group_vars/all/vault.yml and ensure your vault password is correct. Use ansible-playbook -vvv for detailed output.

4.5 Automation Examples (Python Integration)

While Ansible is excellent for declarative configuration, Python is often used for dynamic inventory, complex data manipulation, or integration with external systems. Here’s how you might use Python with Ansible Vault for a different kind of secrets management, such as a custom script that needs access to vaulted data.

Scenario: A Python script needs to read a credential from an Ansible Vault file.

python_scripts/read_vaulted_secret.py: This script demonstrates how to programmatically decrypt an Ansible Vault string. Note: For actual integration, you’d typically use the ansible.parsing.vault module, which is more robust. This example uses ansible-vault CLI for simplicity.

import subprocess
import json
import os

def decrypt_vaulted_string(vaulted_string, vault_password):
    """
    Decrypts an Ansible vaulted string using the ansible-vault CLI.
    This is a simplified example; for robust integration, use Ansible's API.
    """
    try:
        # Create a temporary file for the vaulted string
        temp_vaulted_file = "temp_vaulted_string.yml"
        with open(temp_vaulted_file, "w") as f:
            f.write(f"secret_data: !vault |\n  {vaulted_string}")

        # Use ansible-vault decrypt to get the plain text
        # -v is for verbose output, --output=- to print to stdout
        command = [
            "ansible-vault", "decrypt", temp_vaulted_file,
            "--output=-",
            "--vault-password-file", "-" # Read password from stdin
        ]

        process = subprocess.run(
            command,
            input=vault_password.encode('utf-8'),
            capture_output=True,
            text=True,
            check=True
        )
        
        # Parse the YAML output to extract the secret_data
        # This is a very basic YAML parse, more robust parsing needed for complex files
        output_lines = process.stdout.splitlines()
        for line in output_lines:
            if line.strip().startswith("secret_data:"):
                return line.split(':', 1)[1].strip()
        return None # Not found
    except subprocess.CalledProcessError as e:
        print(f"Error decrypting vault: {e.stderr}")
        return None
    finally:
        if os.path.exists(temp_vaulted_file):
            os.remove(temp_vaulted_file)

if __name__ == "__main__":
    # Example vaulted string (replace with your actual vaulted string)
    # You can get this by running: ansible-vault encrypt_string "MySuperSecretValue"
    # Make sure to remove the `!vault |` and leading spaces from the generated string.
    example_vaulted_string = """
$ANSIBLE_VAULT;1.1;AES256
66316263303661336465363065633630323334653334313334336166306161313330383163653139366432323062323864616261313636306663366431666465383561653136610a6237303038623766646539663738663639353931393033323030336239326162393033613637373830386665306636663965643431643433393038666135363665310a30363233393165386465393132643534346166663636306138373434643037
    """
    
    # In a real scenario, you'd get the password from an environment variable,
    # a secure prompt, or a secrets management system.
    vault_password = input("Enter vault password: ")

    decrypted_value = decrypt_vaulted_string(example_vaulted_string.strip(), vault_password)

    if decrypted_value:
        print(f"Decrypted Secret: {decrypted_value}")
    else:
        print("Failed to decrypt secret.")

To run this, first generate an encrypted string:

ansible-vault encrypt_string "MySuperSecretValue" --name 'secret_data'

Copy the full output (including !vault | and the encrypted string) into the example_vaulted_string variable in the Python script. Then run:

python python_scripts/read_vaulted_secret.py

And enter your vault password.

4.6 Security Considerations

Ansible Vault is a critical security tool, but its effectiveness depends on proper implementation and adherence to best practices.

Attack Vectors & Mitigation:

  1. Vault Password Compromise:
    • Attack Vector: Storing vault passwords in plain text, hardcoding them, or using weak passwords.
    • Mitigation:
      • Use strong, unique passwords for the vault.
      • Never hardcode vault passwords in scripts or commit them to source control.
      • Prefer prompting for passwords (--ask-vault-pass) or using a dedicated ~/.ansible/vault_pass.txt file (secured with strict permissions like chmod 600).
      • Integrate with external secrets management solutions (e.g., HashiCorp Vault, CyberArk, AWS Secrets Manager) for production environments. Ansible Vault can act as a local secrets manager, but for enterprise-scale, external systems are preferred.
      • Use ANSIBLE_VAULT_PASSWORD_FILE environment variable for CI/CD pipelines, where the file content is provided by the CI/CD system’s secret store.
  2. Access to Decrypted Data:
    • Attack Vector: Ansible decrypts vault files in memory during execution. If the control node is compromised, sensitive data could be exposed in RAM or logs.
    • Mitigation:
      • Secure your Ansible control node following hardening guides.
      • Restrict access to the control node to authorized personnel.
      • Ensure minimal logging of sensitive data. Ansible automatically redacts some sensitive variables, but custom tasks might expose data if not handled carefully (e.g., debug: var=ansible_ssh_pass).
  3. Malicious Collections/Roles:
    • Attack Vector: Using unverified collections or roles from untrusted sources, which could contain malicious code.
    • Mitigation:
      • Only install collections from trusted sources (Ansible Galaxy, Red Hat Automation Hub, or your internal private galaxy).
      • Review the code of external roles/collections before deploying them, especially for production.
      • Use checksum or signature verification if available for collections.
  4. Improper File Permissions:
    • Attack Vector: Vault files (even encrypted) or vault password files having overly permissive file permissions.
    • Mitigation:
      • Set strict file permissions: chmod 600 for vault files and vault password files.
      • Ensure the Ansible user has the minimum necessary permissions to run playbooks and access only required resources.

Compliance Requirements: Many compliance standards (PCI DSS, HIPAA, ISO 27001) mandate secure handling of sensitive data. Ansible Vault helps meet these requirements by encrypting credentials at rest and during transit (within the Ansible workflow). Always document your secrets management strategy for audits.

4.7 Verification & Troubleshooting

4.7.1 Verification

Successful execution of playbooks using roles, collections, and vault can be verified by:

  1. Playbook Output: Look for changed=X or ok=X states in the playbook summary, indicating tasks completed successfully.
    PLAY RECAP *********************************************************************
    cisco_router               : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    juniper_switch             : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    
  2. Device State: Manually or automatically verify the configuration on the target network devices using vendor-specific show commands (as demonstrated in section 4.4.4).
  3. Idempotence Check: Run the playbook again. If it’s truly idempotent, the output should show changed=0, indicating no further changes were needed.

4.7.2 Troubleshooting Common Issues

IssueDescriptionResolution Steps
Role Not FoundAnsible cannot locate the specified role.1. Ensure the role directory exists in roles/ or a path specified in ansible.cfg roles_path.
2. Check for typos in the role name in your playbook.
Collection Not FoundAnsible cannot find the module or role referenced by an FQCN (e.g., cisco.ios.ios_config).1. Verify the collection is installed: ansible-galaxy collection list.
2. Ensure collections_paths in ansible.cfg points to the correct location.
3. Check for typos in the FQCN.
4. Run ansible-galaxy collection install -r requirements.yml if missing.
Vault Decryption FailedIncorrect vault password or corrupted vault file.1. Double-check your vault password.
2. If using --vault-password-file, verify the file path and permissions (chmod 600).
3. If a vault file is corrupted, you might need to recover from a backup (another reason for version control!).
4. Ensure you’re providing the correct --vault-id if used.
Variable Precedence IssuesA variable is not taking the expected value within a role (e.g., default not overridden).1. Understand Ansible’s variable precedence order (defaults are lowest).
2. Use ansible-playbook -vvv to see variable values during execution.
3. Use ansible --syntax-check with -v to check variable definitions.
Multi-vendor Logic Not TriggeringPlatform-specific tasks are not running for a device.1. Verify ansible_network_os is correctly set in your inventory.
2. Check the when: conditions in roles/my_role/tasks/main.yml for correctness.
3. Use ansible-playbook -vvv to inspect ansible_network_os value for the target host.
Syntax Errors in Role/Collection ContentYAML formatting errors or incorrect module parameters within tasks.1. Use ansible-lint to check for common issues and best practices.
2. Use ansible-playbook --syntax-check to catch basic YAML errors.
3. Refer to module documentation for correct parameter usage (e.g., ansible-doc cisco.ios.ios_config).

Debug Commands:

  • ansible-playbook -vvv playbooks/site.yml --ask-vault-pass: Provides extremely verbose output, showing task details, variable values, and module calls. Invaluable for debugging.
  • ansible --list-tasks -i inventory.yml playbooks/site.yml: Lists all tasks that would be executed by a playbook, useful for understanding flow.
  • ansible-inventory --list -i inventory.yml: Shows the parsed inventory, including all host and group variables, which helps debug variable precedence.

4.8 Performance Optimization

While roles and collections improve organization and reusability, their impact on performance is generally positive due to better modularity. Vault has a minimal performance overhead.

  • Role Design:
    • Granularity: Create roles that are granular enough to be reusable but not so fine-grained that they introduce excessive overhead from context switching between tasks.
    • Conditional Tasks: Use when: conditions effectively to skip tasks that are not relevant to a particular device or state.
    • Idempotence: Ensure tasks are idempotent to avoid unnecessary changes and associated network calls.
  • Collection Usage:
    • ansible.builtin vs. FQCN: For commonly used modules (e.g., command, shell, copy), prefer ansible.builtin.<module_name> to avoid the overhead of collection lookup if you haven’t specified collections: in your playbook or role.
    • Avoid Redundant Collections: Only install collections that are actively used to keep the Ansible environment lean.
  • Vault Performance:
    • Ansible Vault decryption happens in memory on the control node. For typical network automation, the overhead is negligible.
    • If you have thousands of vaulted variables or very large vaulted files, there might be a slight increase in playbook startup time, but this is rarely a bottleneck.
  • Network Module Optimization:
    • _config modules: Whenever possible, use vendor-specific _config modules (e.g., cisco.ios.ios_config) that handle batching of commands more efficiently than sending individual command module calls.
    • diff: true: Use diff: true only when necessary, as generating diffs can add a small overhead.
    • Parallelism: Adjust forks in ansible.cfg to optimize parallel execution for your environment, but be mindful of rate limiting or resource constraints on network devices.

4.9 Hands-On Lab: Implementing Multi-Vendor VLAN Role with Vault

Lab Topology (nwdiag):

nwdiag {
  network core_network {
    address = "192.168.1.0/24"
    cisco_ios_xe [address = "192.168.1.10", description = "Cisco Catalyst 9K"];
    juniper_junos [address = "192.168.1.20", description = "Juniper EX Series"];
    ansible_control [address = "192.168.1.5", description = "Ubuntu VM (Ansible)"];
  }
}

Objectives:

  1. Create an Ansible project structure.
  2. Develop a multi-vendor Ansible role (configure_bgp_asn) to set a BGP Autonomous System Number (ASN) on Cisco IOS-XE and Juniper Junos devices.
  3. Encrypt the BGP ASN using Ansible Vault.
  4. Execute a playbook to apply the role and vaulted variable to both devices.
  5. Verify the BGP ASN configuration on both devices.

Pre-requisites:

  • An Ansible control node with Ansible installed.
  • Two network devices (one Cisco IOS-XE, one Juniper Junos) with SSH connectivity from the Ansible control node.
  • cisco.ios and juniper.junos collections installed.
  • Network credentials (username/password) for ansible_user with enable or root privileges.

Step-by-Step Configuration:

  1. Initialize Project: Create a new directory ansible_bgp_lab and navigate into it.

    mkdir ansible_bgp_lab
    cd ansible_bgp_lab
    mkdir -p playbooks roles/configure_bgp_asn/{tasks,defaults,vars} group_vars/all collections
    
  2. ansible.cfg: (Same as previous example, but update collections_paths)

    # ansible.cfg
    [defaults]
    inventory = ./inventory.yml
    remote_user = ansible_user
    private_key_file = ~/.ssh/id_rsa
    host_key_checking = false
    gathering = smart
    collections_paths = ./collections
    
    [privilege_escalation]
    become = true
    become_method = enable
    become_user = root
    become_ask_pass = false 
    
  3. collections/requirements.yml:

    # collections/requirements.yml
    collections:
      - name: cisco.ios
      - name: juniper.junos
    

    Install: ansible-galaxy collection install -r collections/requirements.yml

  4. inventory.yml:

    # inventory.yml
    all:
      children:
        cisco_devices:
          hosts:
            cisco_router:
              ansible_host: 192.168.1.10
              ansible_network_os: cisco.ios.ios
        juniper_devices:
          hosts:
            juniper_switch:
              ansible_host: 192.168.1.20
              ansible_network_os: juniper.junos
    
  5. group_vars/all/vault.yml (Create and Encrypt): First, create the file: ansible-vault create group_vars/all/vault.yml Enter a strong vault password. Add the following content (which will be encrypted):

    ansible_user: "your_ssh_username"
    ansible_ssh_pass: "YourSecureSSHPW!"
    ansible_become_pass: "YourSecureEnablePW!"
    bgp_asn: 65001 # This is the sensitive data we want to vault
    
  6. roles/configure_bgp_asn/tasks/main.yml:

    # roles/configure_bgp_asn/tasks/main.yml
    ---
    - name: Include Cisco IOS BGP tasks
      ansible.builtin.include_tasks: _cisco_ios.yml
      when: ansible_network_os == 'cisco.ios.ios'
    
    - name: Include Juniper Junos BGP tasks
      ansible.builtin.include_tasks: _juniper_junos.yml
      when: ansible_network_os == 'juniper.junos'
    
  7. roles/configure_bgp_asn/tasks/_cisco_ios.yml:

    # roles/configure_bgp_asn/tasks/_cisco_ios.yml
    ---
    - name: Configure BGP ASN on Cisco IOS-XE
      cisco.ios.ios_config:
        lines:
          - "router bgp "
          - "no bgp default ip-routing" # Example additional config
        save_when_changed: true
      delegate_to: ""
      connection: network_cli
    
  8. roles/configure_bgp_asn/tasks/_juniper_junos.yml:

    # roles/configure_bgp_asn/tasks/_juniper_junos.yml
    ---
    - name: Configure BGP ASN on Juniper Junos
      juniper.junos.junos_config:
        lines:
          - "set routing-options autonomous-system "
        comment: "Configured BGP ASN via Ansible"
        diff: true
      delegate_to: ""
      connection: network_cli
    
  9. playbooks/site.yml:

    # playbooks/site.yml
    ---
    - name: Deploy BGP ASN using multi-vendor role
      hosts: all
      gather_facts: false
    
      roles:
        - configure_bgp_asn
    

Verification Steps:

  1. Run the playbook:

    ansible-playbook playbooks/site.yml --ask-vault-pass
    

    Enter your vault password when prompted.

  2. Verify on Cisco IOS-XE (192.168.1.10):

    show running-config | section router bgp
    

    Expected Output:

    router bgp 65001
     no bgp default ip-routing
    
  3. Verify on Juniper Junos (192.168.1.20):

    show configuration routing-options | display set
    

    Expected Output:

    set routing-options autonomous-system 65001;
    

Challenge Exercises:

  • Modify the configure_bgp_asn role to also configure a BGP neighbor. Vault the neighbor’s IP address and remote ASN.
  • Add support for Arista EOS to the configure_bgp_asn role, including creating a _arista_eos.yml task file and updating main.yml with the appropriate when: condition.
  • Explore using ansible-vault encrypt_string to embed a single encrypted variable directly into host_vars instead of a full vault file.

4.10 Best Practices Checklist

  • Use Roles for Modularity: Always organize your automation content into roles for reusability and maintainability.
  • Leverage Collections: Utilize vendor-specific and community collections to simplify multi-vendor automation and reduce custom code.
  • Secure Secrets with Vault: Never store sensitive data in plain text. Use Ansible Vault for encryption.
  • Strong Vault Passwords: Use complex, unique passwords for your vault files.
  • Vault Password Management: Prefer --ask-vault-pass, secure password files, or external secrets managers (HashiCorp Vault, CyberArk) for production.
  • Strict File Permissions: Apply chmod 600 to vault files and vault password files.
  • Idempotent Tasks: Design tasks to be repeatable without causing unintended side effects or configuration changes if the desired state already exists.
  • Clear Variable Precedence: Understand Ansible’s variable precedence to avoid unexpected behavior in roles.
  • Multi-vendor Logic: Use when: conditions based on ansible_network_os or other facts to execute platform-specific tasks within roles.
  • Ansible Linting: Use ansible-lint to enforce coding standards and identify potential issues in roles and playbooks.
  • Version Control: Store all Ansible code (playbooks, roles, inventory, requirements.yml) in a version control system (e.g., Git). Exclude vault password files from VCS.
  • Documentation: Document your roles, playbooks, and inventory for clarity and future reference.

4.12 What’s Next

This chapter propelled your Ansible skills from basic playbooks to advanced, structured automation. You’ve learned how to organize complex tasks with roles, leverage the power of collections for multi-vendor environments, and fortify your automation with Ansible Vault for secure secrets management. These are foundational skills for any serious NetDevOps practitioner.

In the next chapter, we will shift our focus to Advanced Python for Network Automation. We’ll delve deeper into using Python libraries like NAPALM and Nornir to build more programmatic and flexible automation solutions, often integrating them with Ansible for a hybrid approach. This will include advanced data parsing with TextFSM and Genie, and leveraging modern network APIs like NETCONF, RESTCONF, and gRPC with YANG data models, further expanding your NetDevOps toolkit.