Introduction

Welcome to Chapter 3 of “NetDevOps for Network Engineers,” where we dive into Ansible, a cornerstone tool for modern network automation. In the previous chapters, we laid the groundwork for NetDevOps principles and introduced the concept of Infrastructure as Code. Now, we shift our focus to hands-on implementation, starting with Ansible due to its simplicity, agentless architecture, and widespread adoption across both server and network automation domains.

This chapter will provide a comprehensive understanding of Ansible’s core concepts as they apply to network devices. You’ll learn how Ansible works, its key components, and how to construct robust, idempotent playbooks for managing your network infrastructure. We’ll explore practical examples for multi-vendor environments, focusing on Cisco, Juniper, and Arista devices, demonstrating how to achieve consistent configurations programmatically.

Upon completing this chapter, you will be able to:

  • Understand Ansible’s agentless architecture and its benefits for network automation.
  • Define and manage Ansible inventory for network devices.
  • Write basic Ansible playbooks with tasks, modules, and handlers.
  • Apply Ansible for configuration management on multi-vendor network devices.
  • Implement fundamental security practices within your Ansible workflows.
  • Effectively troubleshoot common Ansible network automation issues.

Let’s begin our journey into leveraging Ansible to transform manual network operations into automated, reliable processes.

Technical Concepts

3.1 Ansible Architecture and Design

Ansible is an open-source automation engine that automates provisioning, configuration management, application deployment, and intra-service orchestration. Its strength lies in its simplicity and agentless design, which means no special software or agents need to be installed on the managed network devices. This greatly reduces overhead and simplifies deployment.

3.1.1 Agentless Architecture

Unlike traditional configuration management tools that require an agent on each managed node, Ansible communicates with network devices primarily over standard protocols like SSH (for CLI-based configurations) or API-based interfaces (NETCONF, RESTCONF, gRPC, etc.). This makes it incredibly easy to get started with existing infrastructure.

The core components of Ansible include:

  • Control Node: The machine where Ansible is installed and playbooks are executed. It manages the inventory, runs playbooks, and orchestrates tasks.
  • Managed Nodes (Hosts): The network devices (routers, switches, firewalls) that Ansible manages.
  • Inventory: A list of managed nodes, often categorized into groups, along with variables associated with them.
  • Modules: Small programs that Ansible pushes to managed nodes to execute specific tasks (e.g., configure VLANs, retrieve facts, manage interfaces). Ansible provides a rich set of built-in network modules, often organized into collections by vendor.
  • Playbooks: YAML files that define a set of tasks to be executed on managed nodes. Playbooks are the heart of Ansible automation.
  • Tasks: A single unit of action within a playbook, using a specific module.
  • Handlers: Special tasks that are triggered by other tasks when a change occurs. They are typically used for actions like restarting services or saving configurations.
  • Roles: A way to organize playbooks and related files (variables, templates, handlers) into a reusable, structured format.
  • Ansible Vault: Used to encrypt sensitive data such as passwords, API keys, and private keys.
@startuml
skinparam handwritten true
skinparam style strict

cloud "The Network Cloud" as network {
    folder "Managed Devices" as devices {
        component "Cisco IOS-XE" as cisco
        component "Juniper JunOS" as juniper
        component "Arista EOS" as arista
    }
}

node "Control Node" as control {
    component "Ansible Engine" as engine
    file "Inventory (hosts.ini)" as inventory
    file "Playbooks (.yml)" as playbooks
    folder "Ansible Collections" as collections {
        file "Cisco Modules"
        file "Juniper Modules"
        file "Arista Modules"
    }
    database "Ansible Vault" as vault
}

playbooks [label="> engine : executes
inventory"] engine : provides host data
collections [label="> engine : provides modules
engine"] cisco : SSH/NETCONF/RESTCONF
engine [label="> juniper : SSH/NETCONF/RESTCONF
engine"] arista : SSH/NETCONF/RESTCONF
vault --> engine : secure credentials

@enduml

3.1.2 Idempotency

A key principle of Ansible (and Infrastructure as Code) is idempotency. This means that executing a playbook multiple times will result in the same outcome as executing it once, without causing unintended side effects. If a configuration is already in the desired state, Ansible will detect this and make no changes. This is crucial for network stability, as it prevents unnecessary reconfigurations and ensures consistent state.

3.1.3 Connection Methods

For network devices, Ansible primarily uses:

  • Network CLI (Command-Line Interface): Over SSH, using modules like ios_config, junos_config, eos_config. These modules push CLI commands to devices.
  • Network APIs (NETCONF, RESTCONF, gRPC): Using modules that interact directly with the device’s API, typically based on YANG data models. This provides a more structured and robust automation interface compared to screen scraping CLI output. Cisco, Juniper, and Arista all support these programmable interfaces.
    • NETCONF (RFC 6241): A robust, XML-based protocol for configuring network devices.
    • RESTCONF (RFC 8040): A HTTP-based protocol, often preferred for its simplicity, using YANG data models.
    • gRPC (gRPC Network Management Interface - gNMI): A high-performance, open-source universal RPC framework, gaining traction for telemetry and configuration.

3.2 Ansible Inventory

The Ansible inventory defines the hosts (network devices) that your Ansible control node will manage. It’s typically a static file (hosts.ini or inventory.yml) or can be dynamically generated.

3.2.1 Static Inventory File (hosts.ini)

A simple INI-formatted inventory file often looks like this:

[cisco_devices]
cisco-router-1.example.com
cisco-switch-1.example.com ansible_host=192.168.1.10

[juniper_devices]
juniper-router-1.example.com

[arista_devices]
arista-leaf-1.example.com

[all:vars]
ansible_user=ansible_admin
ansible_network_os=ios # Default for 'all', overridden by groups or hosts
ansible_become=yes
ansible_become_method=enable
ansible_become_pass= # Use Ansible Vault for sensitive data
ansible_connection=network_cli # Default connection type

[cisco_devices:vars]
ansible_network_os=ios
ansible_connection=network_cli
ansible_command_timeout=60

[juniper_devices:vars]
ansible_network_os=junos
ansible_connection=netconf # Using NETCONF for Junos
ansible_port=830 # Default NETCONF port over SSH

[arista_devices:vars]
ansible_network_os=eos
ansible_connection=network_cli

In this example:

  • Devices are grouped ([cisco_devices], [juniper_devices], [arista_devices]).
  • ansible_host can be specified if the hostname doesn’t resolve to the correct IP.
  • [all:vars] defines variables applicable to all hosts.
  • Group-specific variables override global variables.
  • ansible_network_os tells Ansible which network platform collection to use.
  • ansible_connection defines the connection plugin. network_cli for SSH-based CLI, netconf for NETCONF.
  • ansible_become and ansible_become_method are for privilege escalation (e.g., enable mode on Cisco).
  • ansible_become_pass uses a placeholder for a Vault-encrypted password.

3.2.2 Inventory with group_vars and host_vars

For larger, more complex environments, it’s best practice to separate variables from the main inventory file using group_vars and host_vars directories.

inventory/
├── hosts.ini
├── group_vars/
│   ├── all.yml
│   ├── cisco_devices.yml
│   ├── juniper_devices.yml
│   └── arista_devices.yml
└── host_vars/
    ├── cisco-router-1.example.com.yml
    └── juniper-router-1.example.com.yml

inventory/hosts.ini:

[cisco_devices]
cisco-router-1.example.com
cisco-switch-1.example.com

[juniper_devices]
juniper-router-1.example.com

[arista_devices]
arista-leaf-1.example.com

inventory/group_vars/all.yml:

ansible_user: ansible_admin
ansible_become: yes
ansible_become_method: enable
ansible_become_pass: ""

inventory/group_vars/cisco_devices.yml:

ansible_network_os: ios
ansible_connection: network_cli
ansible_command_timeout: 60
ntp_server: 10.0.0.1

inventory/host_vars/cisco-router-1.example.com.yml:

hostname: CR1
loopback_ip: 172.16.1.1/32

This modular approach makes inventories highly scalable and maintainable.

3.3 Playbooks: The Core of Automation

Ansible playbooks are YAML files that define automation tasks. They are declarative, meaning you describe the desired state rather than the steps to get there.

3.3.1 Playbook Structure

A typical network playbook structure includes:

  • hosts: Specifies which hosts or groups from the inventory the playbook will run against.
  • gather_facts: For network devices, this is often set to no initially as fact gathering can be resource-intensive or not directly relevant for all tasks. Specific network facts can be gathered with dedicated modules.
  • vars: Defines variables specific to the playbook.
  • tasks: A list of operations to perform. Each task calls an Ansible module.
  • handlers: Actions that are only triggered when a task reports a change.
digraph PlaybookFlow {
    rankdir=LR;
    node [shape=box, style="rounded,filled", fillcolor=lightgrey];

    start [label="Start Playbook Execution"];
    inventory [label="Load Inventory"];
    variables [label="Load Variables"];
    tasks [label="Process Tasks"];
    module_execution [label="Execute Module on Host"];
    check_change [label="Check for Change"];
    handlers [label="Trigger Handlers"];
    end [label="End Playbook"];

    start -> inventory;
    inventory -> variables;
    variables -> tasks;
    tasks -> module_execution;
    module_execution -> check_change;
    check_change -> handlers [label="If change detected"];
    check_change -> tasks [label="No change, next task"];
    handlers -> end [label="Handlers complete"];
    tasks -> end [label="No more tasks"];

    {rank=same; inventory; variables}
}

3.3.2 Common Network Modules

Ansible provides specialized modules for various network operating systems:

  • Platform-specific collections:
    • cisco.ios.ios_config: For Cisco IOS/IOS-XE
    • cisco.nxos.nxos_config: For Cisco NX-OS
    • juniper.junos.junos_config: For Juniper JunOS
    • arista.eos.eos_config: For Arista EOS
  • Generic modules for facts/commands:
    • cisco.ios.ios_command: Run arbitrary CLI commands on Cisco IOS.
    • juniper.junos.junos_command: Run arbitrary CLI commands on Juniper JunOS.
    • arista.eos.eos_command: Run arbitrary CLI commands on Arista EOS.
    • ansible.netcommon.cli_config: Generic CLI configuration.
    • ansible.netcommon.cli_command: Generic CLI command execution.
  • Modules for API-based automation (NETCONF/RESTCONF/gRPC):
    • cisco.ios.ios_netconf: NETCONF operations on IOS-XE.
    • juniper.junos.junos_netconf: NETCONF operations on JunOS.
    • These modules often interact with YANG data models to configure devices programmatically.

The choice between CLI-based and API-based modules depends on the device’s capabilities, the complexity of the task, and the desired level of abstraction. Modern devices increasingly favor API-based approaches for their structured nature and error handling.

Configuration Examples (Multi-vendor)

Let’s demonstrate how to use Ansible to configure basic settings across Cisco, Juniper, and Arista devices. We will configure NTP servers and a banner.

Assumptions:

  • Ansible control node is set up.
  • Inventory is defined as inventory/hosts.ini with group_vars/all.yml, group_vars/cisco_devices.yml, group_vars/juniper_devices.yml, group_vars/arista_devices.yml as detailed above.
  • ansible_admin user has SSH access to all devices and enable password (for Cisco/Arista) is stored securely in Ansible Vault.
  • ntp_server variable is defined in group_vars for each device type.

Playbook: configure_basics.yml

---
- name: Configure NTP and Banner on Network Devices
  hosts: all # Target all devices in the inventory
  gather_facts: no # No need to gather facts by default for network devices

  vars:
    ntp_server: "10.0.0.10" # Global NTP server, can be overridden by group_vars
    banner_text: |
      *************************************************
      * Unauthorized Access Strictly Prohibited       *
      * All activities are logged and monitored       *
      *************************************************

  tasks:
    - name: Ensure NTP server is configured on Cisco IOS-XE
      cisco.ios.ios_config:
        lines:
          - "ntp server  prefer"
        save_when: modified # Only save config if changes were made
      when: ansible_network_os == 'ios'

    - name: Configure login banner on Cisco IOS-XE
      cisco.ios.ios_banner:
        banner: login
        text: ""
        state: present
      when: ansible_network_os == 'ios'

    - name: Ensure NTP server is configured on Juniper JunOS (CLI method)
      juniper.junos.junos_config:
        lines:
          - "set system ntp server  prefer"
        # Junos typically auto-commits changes with junos_config module unless commit_empty_changes is false and there are no changes
        # Best practice for Junos is to use 'commit_changes=yes' but be aware of how your device handles this.
        # For simplicity, we assume default behavior which often implies commit.
      when: ansible_network_os == 'junos'

    - name: Configure login banner on Juniper JunOS
      juniper.junos.junos_config:
        lines:
          - "set system login banner \"\""
        comment: "Configuring login banner via Ansible"
      when: ansible_network_os == 'junos'

    - name: Ensure NTP server is configured on Arista EOS
      arista.eos.eos_config:
        lines:
          - "ntp server "
        # EOS config module has save_when similar to Cisco, but generally auto-saves to running-config.
        # For persistent config, 'write memory' might be needed depending on requirements, often managed by a handler.
      when: ansible_network_os == 'eos'

    - name: Configure login banner on Arista EOS
      arista.eos.eos_banner:
        banner: login
        text: ""
        state: present
      when: ansible_network_os == 'eos'

  handlers:
    - name: Save Cisco config
      cisco.ios.ios_command:
        commands: "write memory"
      listen: "save config" # Listen for notifications from tasks
      when: ansible_network_os == 'ios'

    - name: Save Arista config
      arista.eos.eos_command:
        commands: "write memory"
      listen: "save config"
      when: ansible_network_os == 'eos'

To run this playbook: ansible-playbook -i inventory/hosts.ini configure_basics.yml --ask-vault-pass

The save_when: modified or listen: "save config" handler approach ensures configuration is only saved if changes are actually made, adhering to idempotency.

3.4 Security Warnings for Configuration Examples

  • Sensitive Data: Never hardcode passwords, API keys, or private keys directly in playbooks or inventory files. Always use Ansible Vault for encryption. The example uses ``, which implies a variable managed by Vault.
  • Least Privilege: Ensure the ansible_user has only the necessary permissions on the network devices to perform the intended tasks. Avoid using root or highly privileged accounts unless absolutely required and carefully restricted.
  • Idempotency: While Ansible strives for idempotency, certain network device commands or modules might not be fully idempotent. Always test thoroughly in a lab environment before deploying to production.
  • Configuration Rollback: Always have a rollback strategy. Ansible can be used to revert configurations, but in critical scenarios, device-level rollback features (e.g., Juniper’s rollback) or full configuration backups are essential.
  • SSH Key Management: Prefer SSH key-based authentication over password-based. Manage SSH keys securely.

Network Diagrams

3.5.1 Lab Network Topology (NwDiag)

This diagram illustrates a basic network topology that could be used for testing the Ansible playbook.

nwdiag {
  node_width = 120
  node_height = 60

  network "Management Network" {
    address = "192.168.1.0/24"
    color = "#E0E0FF";

    control_node [label="Ansible Control Node", address="192.168.1.1"];
  }

  network "Core LAN" {
    address = "10.0.0.0/24"
    color = "#FFFFCC";

    cisco_router_1 [label="Cisco_Router_1", address="10.0.0.1"];
    juniper_router_1 [label="Juniper_Router_1", address="10.0.0.2"];
    arista_leaf_1 [label="Arista_Leaf_1", address="10.0.0.3"];
  }

  control_node -- cisco_router_1 [label="SSH/NETCONF (Mgmt)"];
  control_node -- juniper_router_1 [label="SSH/NETCONF (Mgmt)"];
  control_node -- arista_leaf_1 [label="SSH/NETCONF (Mgmt)"];

  cisco_router_1 -- juniper_router_1 [label="Data Link"];
  juniper_router_1 -- arista_leaf_1 [label="Data Link"];
}

3.5.2 Ansible Automation Workflow (GraphViz)

This diagram shows the high-level flow of an Ansible automation run on network devices.

digraph AnsibleNetworkAutomationFlow {
    rankdir=LR;
    node [shape=box, style="filled", fillcolor="#ECECEC", fontname="Arial"];
    edge [color="#666666"];

    subgraph cluster_0 {
        label="Ansible Control Node";
        style=filled;
        fillcolor="#F0F8FF";

        A [label="Ansible Playbook\n(.yml)"];
        B [label="Inventory\n(hosts.ini, group_vars, host_vars)"];
        C [label="Ansible Engine\n(Parser, Orchestrator)"];
        D [label="Ansible Collections\n(Network Modules)"];
        E [label="Ansible Vault\n(Encrypted Secrets)"];

        A -> C [label="Defines Tasks"];
        B -> C [label="Provides Targets\n& Variables"];
        D -> C [label="Executes Commands/\nAPI Calls"];
        E -> C [label="Decrypts Secrets"];
    }

    subgraph cluster_1 {
        label="Network Devices (Managed Nodes)";
        style=filled;
        fillcolor="#FFF0F5";

        F [label="Cisco IOS-XE"];
        G [label="Juniper JunOS"];
        H [label="Arista EOS"];
    }

    C -> F [label="SSH/NETCONF/RESTCONF\n(Configuration Commands)"];
    C -> G [label="SSH/NETCONF/RESTCONF\n(Configuration Commands)"];
    C -> H [label="SSH/NETCONF/RESTCONF\n(Configuration Commands)"];

    F -> C [label="CLI/API Output"];
    G -> C [label="CLI/API Output"];
    H -> C [label="CLI/API Output"];
}

3.5.3 Ansible Component Architecture (PlantUML)

This diagram offers a more detailed view of the components within the Ansible control node and their interactions.

@startuml
skinparam style strict
skinparam DefaultFontSize 12
skinparam BackgroundColor white

cloud "Network Infrastructure" as Network {
    node "Cisco Devices" as Cisco
    node "Juniper Devices" as Juniper
    node "Arista Devices" as Arista
}

package "Ansible Control Node" {
    component "Ansible Engine" as Engine {
        folder "Playbook Executor" as Executor
        folder "Connection Plugins" as Connections
        folder "Callback Plugins" as Callbacks
    }

    database "Inventory" as Inventory {
        folder "hosts.ini"
        folder "group_vars"
        folder "host_vars"
    }

    folder "Playbooks" as Playbooks {
        file "configure_vlan.yml"
        file "deploy_ospf.yml"
    }

    folder "Ansible Collections" as Collections {
        folder "cisco.ios"
        folder "juniper.junos"
        folder "arista.eos"
        folder "ansible.netcommon"
    }

    database "Ansible Vault" as Vault {
        file "vault.yml"
    }

    Playbooks [label="> Executor : Defines Automation Logic
    Inventory"] Executor : Provides Host Details
    Collections [label="> Executor : Provides Modules
    Vault"] Executor : Supplies Encrypted Secrets
    Executor [label="> Connections : Initiates Communication
    Connections"] Callbacks : Reports Status/Output
}

Connections [label="> Cisco : SSH/NETCONF/RESTCONF
Connections"] Juniper : SSH/NETCONF/RESTCONF
Connections --> Arista : SSH/NETCONF/RESTCONF

Callbacks -down-> "Reporting/Logging System" as Report
Report -right-> "CI/CD Pipeline" as CI_CD
Report -right-> "Monitoring Dashboard" as Monitoring

@enduml

Automation Examples

Building on the conceptual understanding, here are more practical Ansible automation examples.

3.6.1 Ansible Playbook for Interface Configuration

This playbook demonstrates configuring a loopback interface and a physical interface on Cisco, Juniper, and Arista devices.

Playbook: configure_interfaces.yml

---
- name: Configure Interfaces on Multi-Vendor Network Devices
  hosts: all
  gather_facts: no

  vars:
    # Interface variables - example structure.
    # In a real scenario, these would likely come from host_vars or a dedicated data model.
    cisco_interfaces:
      - name: Loopback0
        description: "Ansible Managed Loopback Interface"
        ip: "172.16.1.1/32"
      - name: GigabitEthernet0/1
        description: "Ansible Managed Uplink to Core"
        ip: "10.0.0.10/24"
    juniper_interfaces:
      - name: lo0
        description: "Ansible Managed Loopback Interface"
        ip: "172.16.2.1/32"
      - name: ge-0/0/1
        description: "Ansible Managed Uplink to Core"
        ip: "10.0.0.20/24"
    arista_interfaces:
      - name: Loopback0
        description: "Ansible Managed Loopback Interface"
        ip: "172.16.3.1/32"
      - name: Ethernet1
        description: "Ansible Managed Uplink to Core"
        ip: "10.0.0.30/24"

  tasks:
    - name: Configure Cisco Interfaces
      cisco.ios.ios_config:
        lines:
          - "interface "
          - "description "
          - "ip address "
          - "no shutdown"
        parents: "interface "
        save_when: modified
      loop: ""
      when: ansible_network_os == 'ios'

    - name: Configure Juniper Interfaces (using NETCONF for lo0)
      juniper.junos.junos_config:
        config_mode: private
        lines:
          - "set interfaces  description \"\""
          - "set interfaces  unit 0 family inet address "
        comment: "Configure Juniper interface  via Ansible"
        commit_changes: yes # Explicitly commit changes
        # Best practice: use commit_config=True and optionally rollback_on_error=True
      loop: ""
      when: ansible_network_os == 'junos' and item.name == 'lo0' # Demonstrating a conditional for a specific interface or method

    - name: Configure Juniper Interfaces (using CLI for physical interface)
      juniper.junos.junos_config:
        config_mode: private
        lines:
          - "set interfaces  description \"\""
          - "set interfaces  unit 0 family inet address "
          - "set interfaces  unit 0 family inet no-disable" # Ensure physical interface is not disabled
        comment: "Configure Juniper physical interface  via Ansible CLI"
        commit_changes: yes
      loop: ""
      when: ansible_network_os == 'junos' and item.name != 'lo0'

    - name: Configure Arista Interfaces
      arista.eos.eos_config:
        lines:
          - "interface "
          - "description "
          - "ip address "
          - "no shutdown"
        parents: "interface "
      loop: ""
      when: ansible_network_os == 'eos'

  handlers:
    - name: Save Cisco config after interface changes
      cisco.ios.ios_command:
        commands: "write memory"
      listen: "save config"
      when: ansible_network_os == 'ios'

    - name: Save Arista config after interface changes
      arista.eos.eos_command:
        commands: "write memory"
      listen: "save config"
      when: ansible_network_os == 'eos'

Running this playbook requires the same command as before: ansible-playbook -i inventory/hosts.ini configure_interfaces.yml --ask-vault-pass

Important Notes:

  • Data Models: For robust production automation, interface details and other configuration parameters should come from a centralized Source of Truth (e.g., NetBox, a YAML/JSON file, or a custom data model) rather than being hardcoded in vars.
  • Error Handling: For production, tasks should include ignore_errors: yes for non-critical tasks or failed_when conditions for more precise error handling.
  • Idempotency & commit_changes (Juniper): The junos_config module will commit changes by default if commit_changes: yes. If no changes are needed, it will still interact with the device to check the state, but commit_changes won’t execute if the device reports no pending changes (unless commit_empty_changes=True).

Security Considerations

Network automation, while powerful, introduces new security concerns. It’s paramount to integrate security best practices into your NetDevOps workflows.

3.7.1 Attack Vectors

  • Compromised Control Node: If your Ansible control node is breached, an attacker could gain control over your entire automated network.
  • Stolen Credentials: Hardcoded or poorly protected credentials in playbooks or inventory are a major risk.
  • Malicious Playbooks: Unauthorized or poorly reviewed playbooks can introduce vulnerabilities, misconfigurations, or backdoors.
  • Privilege Escalation Vulnerabilities: Weak ansible_become configurations could allow lower-privileged users to execute tasks with elevated privileges.
  • Insecure Communication: Using unencrypted protocols or weak ciphers for communication with network devices (e.g., Telnet instead of SSH) exposes data and credentials.

3.7.2 Mitigation Strategies and Best Practices

  • Ansible Vault (Mandatory):
    • Encrypt all sensitive data (passwords, API keys, private keys, SSH keys) using Ansible Vault.
    • Store Vault passwords securely (e.g., in a secret management system, not in source control).
    • Use --ask-vault-pass or a vault-password-file (secured with restrictive file permissions).
  • Least Privilege:
    • Configure ansible_user accounts on network devices with the minimum necessary permissions.
    • Grant only specific commands or configuration modes rather than full administrative access if possible.
    • For ansible_become, restrict which commands can be run with sudo or enable.
  • Control Node Hardening:
    • Treat your Ansible control node as a critical infrastructure component.
    • Implement strong access controls (MFA, limited SSH access, jump boxes).
    • Keep the OS and Ansible software patched and up-to-date.
    • Segregate the control node from general user access.
  • Version Control and Code Review:
    • Store all playbooks, inventories, and roles in a version control system (e.g., Git).
    • Implement strict code review processes for all changes to automation scripts. This prevents unauthorized or malicious code and catches potential errors.
  • Secure Connection Protocols:
    • Always use SSH (RFC 4251, 4252, etc.) for CLI connections.
    • Prioritize secure API protocols like NETCONF over SSH (RFC 6242), RESTCONF over HTTPS, or gRPC with TLS.
    • Disable weak SSH ciphers and protocols on network devices.
  • Logging and Auditing:
    • Enable comprehensive logging on network devices for configuration changes made via automation.
    • Ensure Ansible’s own verbose output (-vvv) is logged and reviewed, especially in CI/CD pipelines.
    • Integrate with SIEM systems for anomaly detection.
  • Separation of Duties:
    • Separate who can write playbooks, who can approve them, and who can execute them in production.
    • Use Ansible Automation Platform (AAP) or similar orchestration tools to enforce role-based access control (RBAC).

3.7.3 Security Configuration Examples (Ansible Vault)

To use Ansible Vault, first encrypt your sensitive variables:

ansible-vault create group_vars/all_vault.yml

This will open a text editor. Enter your sensitive variables:

vault_enable_pass: "YourStrongEnablePassword123!"
vault_ssh_pass: "YourStrongSSHPassword!"

Save and exit. Ansible Vault will encrypt the file.

Then, update group_vars/all.yml to reference these vaulted variables:

group_vars/all.yml:

ansible_user: ansible_admin
ansible_become: yes
ansible_become_method: enable
ansible_become_pass: "" # Reference the vaulted variable
# ansible_password: "" # Use if SSH key-based auth is not possible

When running the playbook, you’ll provide the Vault password: ansible-playbook -i inventory/hosts.ini configure_basics.yml --ask-vault-pass

Verification & Troubleshooting

Effective verification and troubleshooting are essential for reliable network automation.

3.8.1 Verification Commands

After running an Ansible playbook, always verify the changes on the network device.

For Cisco IOS-XE:

! Verify NTP
show ntp status
show ntp associations
! Verify Banner
show banner login
! Verify Interface
show interface Loopback0
show ip interface brief

For Juniper JunOS:

# Verify NTP
show ntp status
show ntp associations
# Verify Banner
show system login
# Verify Interface
show interfaces lo0
show interfaces ge-0/0/1

For Arista EOS:

! Verify NTP
show ntp status
show ntp associations
! Verify Banner
show banner login
! Verify Interface
show interface Loopback0
show ip interface brief

Expected Output (Example for Cisco Loopback0):

cisco-router-1#show interface Loopback0
Loopback0 is up, line protocol is up
  Hardware is Loopback
  Internet address is 172.16.1.1/32
  MTU 1514 bytes, BW 8000000 Kbit/sec, DLY 5000 usec,
     reliability 255/255, txload 1/255, rxload 1/255
  Encapsulation LOOPBACK, loopback not set
  Keepalive set (10 sec)
  Full-duplex, 1000Mb/s, link type is force-up
  Last input never, output never, output hang never
  Last clearing of "show interface" counters never
  Input queue: 0/375/0/0 (size/max/drops/flushes); Total output drops: 0
  Queueing strategy: fifo
  Output queue: 0/40 (size/max)
  5 minute input rate 0 bits/sec, 0 packets/sec
  5 minute output rate 0 bits/sec, 0 packets/sec
     0 packets input, 0 bytes, 0 no buffer
     Received 0 broadcasts (0 IP multicasts)
     0 runts, 0 giants, 0 throttles
     0 input errors, 0 CRC, 0 frame, 0 overrun, 0 ignored
     0 watchdog, 0 multicast, 0 pause input
     0 packets output, 0 bytes, 0 underruns
     0 output errors, 0 collisions, 0 interface resets
     0 unknown protocol drops
     0 babbles, 0 late collision, 0 deferred
     0 lost carrier, 0 no carrier, 0 PKT too big
     0 output buffer failures, 0 output buffers swapped out

3.8.2 Troubleshooting Common Issues

IssueRoot CauseResolution StepsDebug Commands
Connectivity ErrorsSSH not enabled, incorrect ansible_host, firewall blocking, incorrect ansible_port.Verify SSH connectivity manually (ssh ansible_admin@device_ip). Check firewall rules, device IP in inventory. Ensure correct ansible_port (e.g., 22 for SSH, 830 for NETCONF).ansible-playbook -i inventory.ini playbook.yml -vvv
ssh -v ansible_admin@device_ip
Authentication FailuresIncorrect ansible_user, ansible_password, ansible_ssh_private_key_file, or ansible_become_pass.Double-check credentials. Ensure correct ansible_user on device. Verify Ansible Vault password. For SSH keys, confirm permissions (chmod 600 key.pem).ansible-playbook -i inventory.ini playbook.yml -vvv
Check device logs (show logging for Cisco/Arista, show log messages for Juniper).
Syntax Errors in Playbook/InventoryYAML indentation issues, incorrect module parameters, missing required arguments.Use a YAML linter. Run ansible-playbook --syntax-check playbook.yml. Carefully review module documentation for required parameters.ansible-playbook --syntax-check playbook.yml
ansible-playbook -i inventory.ini playbook.yml -vvv
Privilege Escalation Issuesansible_become not enabled, incorrect ansible_become_method or ansible_become_pass, user not authorized for enable/sudo.Ensure ansible_become: yes and correct ansible_become_method (e.g., enable). Verify ansible_become_pass is correct via Vault. Check device user roles/permissions.ansible-playbook -i inventory.ini playbook.yml -vvv
On device: show privilege, `show run
Module-Specific ErrorsInvalid configuration lines, unsupported commands, device-specific quirks.Consult Ansible module documentation for the specific network OS. Check device output for error messages related to the configuration.ansible-playbook -i inventory.ini playbook.yml -vvv
Copy/paste failing config lines to CLI manually.
Idempotency Not WorkingA module might not correctly detect current state, or save_when is misconfigured.Review the module’s behavior. Manually check the device state before and after. Ensure handlers for saving config are correctly triggered. Some modules might need specific diff parameters.ansible-playbook -i inventory.ini playbook.yml --diff (to show changes)
ansible-playbook -i inventory.ini playbook.yml -vvv

Root Cause Analysis:

  • Use ansible-playbook -vvv: This provides very verbose output, showing SSH commands, module arguments, and device responses, which is invaluable for debugging.
  • Manual CLI verification: Before assuming Ansible is the culprit, try to apply the configuration manually via CLI. If it fails manually, the issue is with the configuration itself or the device, not Ansible.
  • Check device logs: Network devices provide valuable log messages that can indicate why a command failed or why an operation was denied.

Performance Optimization

Optimizing Ansible playbook performance for network automation is crucial, especially when managing a large number of devices or performing complex operations.

3.9.1 Tuning Parameters

  • Forks: The -f or --forks parameter controls how many parallel processes Ansible will use. For network automation, a higher number of forks can significantly speed up execution. Start with 10-20 and adjust based on your control node’s resources and network latency. ansible-playbook -i inventory.ini playbook.yml --forks 20
  • Strategy Plugins: Ansible’s default strategy is linear, which executes tasks on all hosts before moving to the next task. The free strategy allows hosts to progress through tasks as soon as they complete the previous one, which can be faster if tasks have varying execution times.
    ---
    - name: My Playbook
      hosts: all
      strategy: free # Apply the free strategy
      tasks:
        # ... tasks ...
    
  • SSH Pipelining: This feature reduces the number of SSH connections required by executing multiple commands over a single SSH connection. It’s generally enabled by default for network connections but can be explicitly enabled in ansible.cfg or per play. [ssh_connection] pipelining = True
  • Connection Optimization:
    • Persistent Connections: Network modules often use persistent SSH connections to avoid the overhead of establishing a new connection for each task. Ensure network_cli or netconf connections are correctly configured to leverage this.
    • ControlPersist (SSH): Configured in your ~/.ssh/config or ansible.cfg, this keeps SSH connections open for a period, speeding up subsequent connections.
  • Fact Gathering (gather_facts: no): As seen in our examples, explicitly set gather_facts: no for network plays unless you specifically need host facts. Gathering facts on many network devices can be very slow.
  • Specific Fact Gathering: If you need network device facts, use dedicated modules like cisco.ios.ios_facts or juniper.junos.junos_facts, which can be more efficient and gather only relevant information.

3.9.2 Capacity Planning

  • Control Node Resources: Ensure your Ansible control node has sufficient CPU, RAM, and network bandwidth to handle the desired number of concurrent connections and module executions.
  • Network Device Load: Be mindful of the load you place on network devices. Too many concurrent SSH/NETCONF sessions or heavy configuration pushes can impact device performance, especially on older or less powerful hardware. Monitor device CPU/memory during automation runs.

3.9.3 Monitoring Recommendations

Integrate Ansible runs with your existing monitoring solutions.

  • Callback Plugins: Ansible can use callback plugins to send results to external systems (e.g., Splunk, ELK stack, Prometheus).
  • Ansible Automation Platform (AAP): If using AAP, its dashboard provides extensive analytics and historical data on automation job performance and success rates.
  • Device Monitoring: Continuously monitor the health and performance of your network devices, especially after automation deployments, to detect any unintended side effects.

Hands-On Lab

This lab will guide you through configuring basic network settings on a multi-vendor environment using Ansible.

3.10.1 Lab Topology

nwdiag {
  node_width = 150
  node_height = 70
  fontsize = 12

  network "Lab Management Segment" {
    address = "192.168.122.0/24"
    color = "#DDF8FF";
    description = "Virtual Management Network";

    ansible_cn [label="Ansible Control Node\n(Ubuntu VM)", address="192.168.122.10"];
  }

  network "Device Backplane" {
    address = "10.0.0.0/24"
    color = "#FFF8DD";
    description = "Inter-Device Connectivity";

    cisco_rtr [label="Cisco IOS-XE (vCSR1000V)\nMgmt IP: 192.168.122.101", address="10.0.0.1"];
    juniper_rtr [label="Juniper JunOS (vSRX/vMX)\nMgmt IP: 192.168.122.102", address="10.0.0.2"];
    arista_sw [label="Arista EOS (vEOS)\nMgmt IP: 192.168.122.103", address="10.0.0.3"];
  }

  ansible_cn -- cisco_rtr [label="SSH/NETCONF"];
  ansible_cn -- juniper_rtr [label="SSH/NETCONF"];
  ansible_cn -- arista_sw [label="SSH/NETCONF"];

  cisco_rtr -- juniper_rtr [label="Eth1/0 to ge-0/0/0"];
  juniper_rtr -- arista_sw [label="ge-0/0/1 to Eth2"];
}

3.10.2 Objectives

  1. Set up Ansible inventory for the lab devices.
  2. Create an Ansible Vault for sensitive credentials.
  3. Write a playbook to configure a standard login banner across all devices.
  4. Write a playbook to configure NTP servers and a Loopback interface on each device.
  5. Verify the configurations.

3.10.3 Step-by-Step Configuration

Pre-requisites:

  • Three virtual network devices (Cisco IOS-XE, Juniper JunOS, Arista EOS) reachable from your Ansible control node via SSH.
  • ansible_admin user configured on all devices with necessary privileges.
  • Ansible installed on your control node.

Step 1: Create Lab Directory Structure

mkdir -p ansible_lab/inventory/group_vars
cd ansible_lab

Step 2: Create inventory/hosts.ini

# ansible_lab/inventory/hosts.ini
[cisco_devices]
cisco-router-1 ansible_host=192.168.122.101

[juniper_devices]
juniper-router-1 ansible_host=192.168.122.102

[arista_devices]
arista-switch-1 ansible_host=192.168.122.103

Step 3: Create group_vars/all.yml (non-sensitive)

# ansible_lab/inventory/group_vars/all.yml
ansible_user: ansible_admin
ansible_become: yes
ansible_become_method: enable

Step 4: Create group_vars/vault.yml (sensitive, using Ansible Vault)

ansible-vault create inventory/group_vars/vault.yml

Enter a strong vault password. In the editor, add:

ansible_become_pass: "YOUR_CISCO_ENABLE_PASSWORD" # Replace with actual password
ansible_password: "YOUR_JUNIPER_SSH_PASSWORD" # Replace with actual password
# If Juniper needs an 'enable' pass (e.g. for su), use similar to Cisco

Save and exit the editor.

Now update inventory/group_vars/all.yml to reference the vaulted variable:

# ansible_lab/inventory/group_vars/all.yml
ansible_user: ansible_admin
ansible_become: yes
ansible_become_method: enable
ansible_become_pass: "" # Will load from vault.yml

Note: You might need to adjust the variable name, or place ansible_password directly in vault.yml if your Ansible user and enable password are the same. For Juniper, ansible_password often suffices for SSH.

Step 5: Create group_vars for each vendor

inventory/group_vars/cisco_devices.yml:

ansible_network_os: ios
ansible_connection: network_cli
ntp_server: 192.168.122.50
loopback_ip: 172.16.10.1/32

inventory/group_vars/juniper_devices.yml:

ansible_network_os: junos
ansible_connection: netconf # Using NETCONF for Junos
ansible_port: 830 # Default NETCONF port over SSH
ntp_server: 192.168.122.50
loopback_ip: 172.16.10.2/32

inventory/group_vars/arista_devices.yml:

ansible_network_os: eos
ansible_connection: network_cli
ntp_server: 192.168.122.50
loopback_ip: 172.16.10.3/32

Step 6: Create Playbook 01_configure_banner.yml

# ansible_lab/01_configure_banner.yml
---
- name: Configure Login Banner on All Devices
  hosts: all
  gather_facts: no

  vars:
    login_banner: |
      *************************************************
      * This is a Lab Environment - Access Restricted *
      * Unauthorized Access is Prohibited             *
      *************************************************

  tasks:
    - name: Configure Cisco IOS-XE banner
      cisco.ios.ios_banner:
        banner: login
        text: ""
        state: present
      when: ansible_network_os == 'ios'

    - name: Configure Juniper JunOS banner
      juniper.junos.junos_config:
        lines:
          - "set system login banner \"\""
        comment: "Configured via Ansible"
        commit_changes: yes
      when: ansible_network_os == 'junos'

    - name: Configure Arista EOS banner
      arista.eos.eos_banner:
        banner: login
        text: ""
        state: present
      when: ansible_network_os == 'eos'

Step 7: Create Playbook 02_configure_ntp_loopback.yml

# ansible_lab/02_configure_ntp_loopback.yml
---
- name: Configure NTP and Loopback Interface
  hosts: all
  gather_facts: no

  tasks:
    - name: Configure NTP and Loopback on Cisco IOS-XE
      cisco.ios.ios_config:
        lines:
          - "ntp server  prefer"
          - "interface Loopback0"
          - "description Ansible Managed Lab Loopback"
          - "ip address "
          - "no shutdown"
        save_when: modified
      when: ansible_network_os == 'ios'

    - name: Configure NTP and Loopback on Juniper JunOS
      juniper.junos.junos_config:
        lines:
          - "set system ntp server  prefer"
          - "set interfaces lo0 unit 0 description \"Ansible Managed Lab Loopback\""
          - "set interfaces lo0 unit 0 family inet address "
        comment: "Configure NTP and Loopback via Ansible"
        commit_changes: yes
      when: ansible_network_os == 'junos'

    - name: Configure NTP and Loopback on Arista EOS
      arista.eos.eos_config:
        lines:
          - "ntp server "
          - "interface Loopback0"
          - "description Ansible Managed Lab Loopback"
          - "ip address "
          - "no shutdown"
        # EOS config module often saves to running-config implicitly.
        # Add a handler for 'write memory' if persistent saving is desired.
      when: ansible_network_os == 'eos'

  handlers:
    - name: Save running config on Cisco IOS-XE
      cisco.ios.ios_command:
        commands: "write memory"
      listen: "save config"
      when: ansible_network_os == 'ios'

    - name: Save running config on Arista EOS
      arista.eos.eos_command:
        commands: "write memory"
      listen: "save config"
      when: ansible_network_os == 'eos'

Step 8: Execute the Playbooks

ansible-playbook -i inventory/hosts.ini 01_configure_banner.yml --ask-vault-pass
ansible-playbook -i inventory/hosts.ini 02_configure_ntp_loopback.yml --ask-vault-pass

3.10.4 Verification Steps

After each playbook run, log in to each device and verify the configuration:

  • Cisco IOS-XE:
    show banner login
    show ntp status
    show ntp associations
    show ip interface brief
    show interface Loopback0
    
  • Juniper JunOS:
    show system login
    show ntp status
    show ntp associations
    show interfaces lo0
    
  • Arista EOS:
    show banner login
    show ntp status
    show ntp associations
    show ip interface brief
    show interface Loopback0
    

You should see the configured banner, the NTP server configured, and the Loopback interface with its IP address.

3.10.5 Challenge Exercises

  1. Add a physical interface: Modify 02_configure_ntp_loopback.yml to also configure one of the physical interfaces (e.g., GigabitEthernet0/0 on Cisco, ge-0/0/0 on Juniper, Ethernet1 on Arista) with an IP address (e.g., from the 10.0.0.0/24 range) and a description.
  2. Gather facts: Create a new playbook (03_gather_facts.yml) that gathers network facts from all devices using their respective fact modules (e.g., cisco.ios.ios_facts, juniper.junos.junos_facts). Print some of the gathered facts (e.g., device serial number, OS version) using the debug module.
  3. Implement a handler for saving: For Arista, explicitly modify 02_configure_ntp_loopback.yml to use a handler to issue write memory only if changes were made. (Hint: the arista.eos.eos_config module has a notify parameter).

Best Practices Checklist

By adhering to these best practices, you can build more robust, secure, and maintainable Ansible network automation solutions.

  • Use Ansible Vault: Encrypt all sensitive data (passwords, API keys, etc.).
  • Version Control: Store all Ansible content (playbooks, inventory, roles) in Git.
  • Idempotency: Design playbooks to be idempotent, ensuring consistent state on repeated runs.
  • Modular Inventory: Use group_vars and host_vars for scalable inventory management.
  • Structured Playbooks (Roles): Organize complex automation into reusable Ansible Roles.
  • Gather Facts Judiciously: Set gather_facts: no for network plays by default and gather specific facts only when needed using dedicated modules.
  • Explicit ansible_network_os: Always define ansible_network_os in inventory.
  • Test Thoroughly: Use a lab environment for all new or modified playbooks.
  • Code Review: Implement a peer review process for all Ansible code changes.
  • Meaningful Naming: Use clear and descriptive names for playbooks, tasks, and variables.
  • Documentation: Document your playbooks, roles, and inventory.
  • Error Handling: Implement ignore_errors or failed_when where appropriate.
  • Rollback Strategy: Always have a plan to revert changes if automation introduces issues.
  • Least Privilege: Ensure Ansible users have minimal necessary permissions on managed devices.
  • Secure Communications: Use SSH, NETCONF over SSH, or RESTCONF over HTTPS.
  • Pre-Checks & Post-Checks: Include tasks to verify current state before changes and desired state after changes.

What’s Next

This chapter has equipped you with the fundamental knowledge and practical skills to start automating your network using Ansible. You’ve learned about its agentless architecture, how to manage network inventory, create idempotent playbooks, and apply them across Cisco, Juniper, and Arista devices. We also covered critical security considerations and troubleshooting techniques.

In the next chapter, we will build upon this foundation by exploring Ansible Roles and Advanced Playbook Design. You’ll learn how to structure your automation for maximum reusability, manage complex configurations, and integrate more sophisticated control flows, preparing you for enterprise-grade NetDevOps deployments. We will also touch upon integrating dynamic inventory sources and setting up CI/CD for Ansible. Get ready to elevate your Ansible skills to the next level!