Intermediate

Getting Started with Ansible for Network Automation

Ansible philosophy for networking — install, concepts, building automation and configuring devices with roles.

Prerequisites: CCNA-level networking knowledge (OSPF, BGP, routing/switching)


Table of Contents

  1. Course Overview
  2. Ansible Philosophy & Network Automation Applications
  3. Installing Ansible
  4. Ansible Concepts & Terminology
  5. Building Basic Network Automation
  6. Configuring Network Devices with Ansible
  7. Creating & Using Ansible Roles
  8. Reference Tables
  9. Architecture Diagrams

1. Course Overview

This course covers the fundamentals of Ansible, a powerful IT infrastructure automation tool that not only automates repetitive tasks but can fundamentally change how you design, configure, and operate a network.

Learning Objectives:

  • Understand the philosophy and history of Ansible
  • Develop and execute automations: tasks, plays, playbooks, and roles
  • Automate the day-to-day operational tasks of a network engineer
  • Apply network configuration changes with Ansible

Course Use Case: The fictional company Globomantics, an industrial parts distributor suffering from configuration drift on its network devices.


2. Ansible Philosophy & Network Automation Applications

Regaining Control of the Data Center

Modern enterprise networks often suffer from configuration drift — a gradual divergence between the desired configuration and the actual configuration of devices. This occurs due to undocumented manual changes, human errors during late-night change windows, and the absence of a tool to enforce the desired configuration state.

What is Ansible?

Ansible is an open-source IT automation tool, initially developed by Michael DeHaan and acquired by Red Hat in 2015. It allows interaction with one or more devices connected to a network.

What Ansible can do:

  • Retrieve specific data from devices
  • Copy files to/from devices
  • Install and configure software
  • Apply network configuration changes idempotently

Key differentiators compared to Puppet, Chef, SaltStack:

  • Agentless: no agent to install on managed devices
  • Simple: based on SSH and YAML, easy to learn
  • Batteries included: everything is included on installation

Typical Ansible workflow:

  1. Before the change window: dry run (--check) to identify changes to be made
  2. During the change window: actual execution to apply changes
  3. After: compliance verification

Network Automation vs Systems Administration

AspectSystems Administration (Linux)Network Administration
ConfigurationText file modificationContext-sensitive CLI command sequences
Primary transportSSHSSH, REST API, NETCONF/RESTCONF, JSON-RPC
Vendor-specificNo (relatively standardized)Yes (Cisco, Juniper, Arista, etc.)
OS versionsMinor impactSignificant impact on commands
Data modelsConfig filesYANG + protocols (NETCONF, RESTCONF)

Network-specific challenges:

  • CLIs vary between vendors, network OS versions, and even between versions of the same OS
  • How to reach a desired state depends on the transport used
  • Some transports have limitations on what can be configured

Ansible Package History

Ansible uses semantic versioning since version 3.0.0 (February 2021):

  • Major (e.g., 3 → 4): breaking changes
  • Minor (e.g., 3.1 → 3.2): backward-compatible new features
  • Patch (e.g., 3.1.1 → 3.1.2): backward-compatible bug fixes

Architectural evolution:

  • Before v2.9: monorepo — everything in a single Git repository (batteries included)
  • v2.10: separation into ansible-base (core) + collections distributed via Ansible Galaxy
  • v3.0+: rename ansible-baseansible-core, semantic versioning adopted

Porting Guides & Changelogs

For each new major version of Ansible, the team publishes a porting guide explaining the changes needed for existing automation. Accessible at docs.ansible.com.

Best practice: Always review the porting guide before a major Ansible upgrade in production.

Declarative vs Imperative Programming

TypeDescriptionAnalogyExample
DeclarativeDescribes what to achieve, not howOrdering at a restaurantAnsible playbooks (desired state)
ImperativeDescribes how to achieve the goalCooking yourselfPython/Bash scripts (sequence of steps)

Ansible combines both approaches:

  • Declarative for resource state (resource modules)
  • Imperative for control logic (when conditions, loop iterations)

3. Installing Ansible

Control Node Prerequisites

An Ansible control node must satisfy three requirements:

  1. Unix-like OS: Linux (Ubuntu, CentOS, Fedora, RHEL…) or BSD. Windows is not supported (WSL possible but unofficial).
  2. Up-to-date Python: Ansible is primarily written in Python. The minimum version depends on the installed Ansible version.
  3. Low latency to managed hosts: ideally in the same data center or geographic region.

Options for creating a control node:

  • VM in a type-2 hypervisor (VirtualBox, VMware Workstation)
  • VM in a type-1 hypervisor (ESXi, Hyper-V)
  • VM in a network emulator (GNS3, EVE-NG, Cisco Modeling Labs)
  • VM in a public cloud (AWS, GCP)

Installation via Package Manager (APT/YUM/DNF)

Ubuntu/Debian (APT):

# Update package list
apt -y update

# Install dependencies
apt -y install software-properties-common

# Add Ansible PPA
add-apt-repository --yes --update ppa:ansible/ansible

# Install Ansible
apt -y install ansible

CentOS 8 / Fedora 22+ / Rocky Linux 8 (DNF):

dnf -y install ansible

CentOS 7 / Fedora 21 (YUM):

yum -y install ansible

Note: Package manager installation may not provide the latest version of Ansible. For production environments requiring a specific version, prefer installation via pip.

Installation via pip in a Virtual Environment

Recommended method — allows multiple Ansible versions on the same machine.

# Create the project folder
mkdir network-automation
cd network-automation

# Create a Python virtual environment
python3 -m virtualenv venv

# If "No module named virtualenv" error:
# Ubuntu/Debian : apt -y install python3-virtualenv
# DNF          : dnf -y install python3-virtualenv
# YUM          : python3 -m pip install virtualenv

# Activate the virtual environment
source venv/bin/activate
# Prompt changes to show (venv)

# Install Ansible in the virtual environment
pip install ansible

# Verify the installation
ansible --version

# Deactivate the virtual environment
deactivate

Installation from Source

To use fixes or features from the development branch not yet released:

# Activate the virtual environment
source venv/bin/activate

# Install from the devel branch on GitHub
pip install https://github.com/ansible/ansible/archive/devel.tar.gz

# Verify (a "development version" warning should appear)
ansible --version

Upgrading Between Major Versions

# Check installed version
ansible --version
pip freeze | grep ansible

# Uninstall the old version
pip uninstall ansible

# Install the new version
pip install ansible==4.0.0

# Verify the upgrade
ansible --version
pip freeze | grep ansible

Warning: Always review the porting guide for the new version before upgrading.


4. Ansible Concepts & Terminology

Ansible Node Types

graph TD
    CN["🖥️ Control Node\n(Ansible installed)"]
    MN1["📦 Managed Node / Host\n(Switch S1 - NX-OS)"]
    MN2["📦 Managed Node / Host\n(Switch S2 - NX-OS)"]
    MN3["🖧 Managed Node / Host\n(Router R1)"]
    CN -->|SSH / API / NETCONF| MN1
    CN -->|SSH / API / NETCONF| MN2
    CN -->|SSH / API / NETCONF| MN3

    style CN fill:#1f6feb,color:#fff
    style MN1 fill:#238636,color:#fff
    style MN2 fill:#238636,color:#fff
    style MN3 fill:#238636,color:#fff
NodeRoleNotes
Control NodeMachine where Ansible is installed and runs automationMust be Unix-like; multiple allowed
Managed Node / HostTarget device of the automationNetwork device, server, load balancer, etc.

Key points:

  • You can have multiple control nodes simultaneously (one per engineer, or in a CI/CD pipeline)
  • A node can be both a control node and a host (e.g., deploying Docker containers locally)

Ansible Configuration Files

Ansible looks for its configuration file (ansible.cfg) in this priority order:

  1. ANSIBLE_CONFIG environment variable
  2. ./ansible.cfg (current directory) ← recommended for projects
  3. ~/.ansible.cfg (user home directory)
  4. /etc/ansible/ansible.cfg (system configuration)

Important: Configuration values do not inherit from one file to another. Only the first file found is used.

Example ansible.cfg file:

[defaults]
# Disable cowsay (ASCII cow status messages)
nocows = 1

# Enable colors in output
force_color = 1

# Disable verbose mode by default
verbosity = 0

# Default inventory file
inventory = inventory.yaml

# Disable SSH host key checking (lab use only, not recommended for production)
host_key_checking = False

# Fork level (parallelism)
forks = 10

Introduction to YAML

YAML (YAML Ain’t Markup Language) is a data serialization language designed to be human-readable. Ansible uses it for inventories, playbooks, variables, etc.

Fundamental YAML structures:

---
# Start of a YAML document (three dashes)

# Dictionary (key: value)
hostname: S1
vendor: Cisco
platform: NX-OS

# List
interfaces:
  - Ethernet1/1
  - Ethernet1/2
  - Loopback0

# Nested dictionary
bgp:
  asn: 65535
  router_id: 10.1.1.1
  neighbors:
    - ip: 10.2.2.2
      remote_as: 65535

# Multi-line string (literal block |)
description: |
  WAN interface to the datacenter
  Bandwidth: 10 Gbps

# Booleans
enabled: true
shutdown: false

# Integers and floats
vlan_id: 100
bandwidth_gbps: 1.0

Recommended tools:

  • yamllint: YAML syntax checker
  • ansible-lint: Ansible-specific linter for playbooks

Ansible Inventory Files

The inventory defines which hosts should be managed by Ansible and how they are grouped.

Inventory types:

  • Static inventory: defined in a YAML or INI file
  • Dynamic inventory: fed from an external source (IPAM, cloud provider)

Default location: /etc/ansible/hosts (but recommended: in the project)

Example inventory.yaml file:

---
all:
  vars:
    ansible_connection: ansible.netcommon.network_cli
    ansible_network_os: cisco.nxos.nxos
    ansible_user: netadmin
    ansible_password: "{{ vault_password }}"  # Use ansible-vault in production

  children:
    switches:
      children:
        west:
          hosts:
            S1:
              loopback: 10.1.1.1
        east:
          hosts:
            S2:
              loopback: 10.2.2.2
graph TD
    ALL["Group: all"]
    SW["Group: switches"]
    W["Group: west"]
    E["Group: east"]
    S1["Host: S1\nloopback: 10.1.1.1"]
    S2["Host: S2\nloopback: 10.2.2.2"]

    ALL --> SW
    SW --> W
    SW --> E
    W --> S1
    E --> S2

    style ALL fill:#6e40c9,color:#fff
    style SW fill:#1f6feb,color:#fff
    style W fill:#0969da,color:#fff
    style E fill:#0969da,color:#fff
    style S1 fill:#238636,color:#fff
    style S2 fill:#238636,color:#fff

Inspect the inventory with ansible-inventory:

# Show variables for a specific host
ansible-inventory -i inventory.yaml --host S1

# Show the full inventory as JSON
ansible-inventory -i inventory.yaml --list

# Show the inventory graph
ansible-inventory -i inventory.yaml --graph

Host/Group Variables and Inheritance

Variables can be defined at multiple levels. Priority order (lowest to highest):

defaults (role) → group vars (all) → group vars (parent) → group vars (child) → host vars → extra vars (-e)

Variables in the inventory:

---
all:
  vars:
    ntp_server: ntp.global.corp.local

  children:
    west:
      vars:
        ntp_server: ntp.west.corp.local  # Override for the west group
      hosts:
        S1:
          loopback: 10.1.1.1
          # S1 inherits ntp.west.corp.local

    east:
      vars:
        ntp_server: ntp.east.corp.local  # Override for the east group
      hosts:
        S2:
          loopback: 10.2.2.2
          # S2 inherits ntp.east.corp.local

group_vars and host_vars directories:

network-automation/
├── group_vars/
│   ├── all.yaml          # Variables for all hosts
│   ├── switches.yaml     # Variables for the switches group
│   ├── west.yaml         # Variables for the west group
│   └── east.yaml         # Variables for the east group
├── host_vars/
│   ├── s1.yaml           # Variables specific to S1
│   └── s2.yaml           # Variables specific to S2
└── inventory.yaml

Example group_vars/switches.yaml:

---
bgp_asn: 65535
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.nxos.nxos

Example host_vars/s1.yaml:

---
loopback: 10.1.1.1
bgp_router_id: 10.1.1.1
interfaces:
  - name: Ethernet1/1
    ip: 192.168.12.1/30
    description: "Link to S2"
  - name: Loopback0
    ip: 10.1.1.1/32
    description: "Loopback interface"

Ansible Facts

Facts are special variables that Ansible automatically collects from hosts before executing automation. They represent the current operational state of a device.

Typical facts on a Cisco Nexus:

  • ansible_facts['net_hostname']: configured hostname
  • ansible_facts['net_version']: software version
  • ansible_facts['net_interfaces']: interface information
  • ansible_facts['net_filesystems']: available filesystems

Variables required for fact collection on NX-OS:

# In inventory.yaml or group_vars/all.yaml
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.nxos.nxos
ansible_user: netadmin
ansible_password: cisco123

Disabling fact collection to speed up execution:

---
- name: Configure BGP on switches
  hosts: switches
  gather_facts: false   # Explicit disabling
  tasks:
    - name: Enable BGP feature
      cisco.nxos.nxos_feature:
        feature: bgp
        state: enabled

Ansible Project Structure

network-automation/
├── ansible.cfg                    # Project Ansible configuration
├── site.yaml                      # Main playbook (entry point)
├── switches.yaml                  # Playbook for switches
├── inventory.yaml                 # Static inventory
├── group_vars/
│   ├── all.yaml
│   ├── switches.yaml
│   ├── west.yaml
│   └── east.yaml
├── host_vars/
│   ├── s1.yaml
│   └── s2.yaml
├── roles/
│   ├── network_devices/
│   ├── routers/
│   ├── ospf_routers/
│   └── bgp_speakers/
├── collections/
│   └── requirements.yaml          # Collection dependencies
└── venv/                          # Python virtual environment

Ansible Automation Components

graph TD
    PB["📋 Playbook\n(YAML file)"]
    P1["🎭 Play 1\n(hosts: switches)"]
    P2["🎭 Play 2\n(hosts: routers)"]
    T1["✅ Task 1\n(name + module)"]
    T2["✅ Task 2\n(name + module)"]
    T3["✅ Task 3\n(name + module)"]
    M1["🔧 Module\ncisco.nxos.nxos_feature"]
    M2["🔧 Module\ncisco.nxos.nxos_config"]

    PB --> P1
    PB --> P2
    P1 --> T1
    P1 --> T2
    P2 --> T3
    T1 --> M1
    T2 --> M2

    style PB fill:#6e40c9,color:#fff
    style P1 fill:#1f6feb,color:#fff
    style P2 fill:#1f6feb,color:#fff
    style T1 fill:#0969da,color:#fff
    style T2 fill:#0969da,color:#fff
    style T3 fill:#0969da,color:#fff
    style M1 fill:#238636,color:#fff
    style M2 fill:#238636,color:#fff
ComponentDescriptionAnalogy
ModuleReusable script performing a specific taskTool in a toolbox
TaskDefines when and how a module is executedInstruction for using the tool
PlayOrdered set of tasks targeting a group of hostsComplete procedure
PlaybookYAML file containing one or more playsProcedures manual

Complete playbook example:

---
# Playbook: copy a file to Nexus switches via SCP
- name: Copy config.txt file to Nexus switches via SCP
  hosts: switches
  gather_facts: false

  tasks:
    - name: Enable SCP feature on switches
      cisco.nxos.nxos_feature:
        feature: scp-server
        state: enabled

    - name: Copy config.txt file to switches
      cisco.nxos.nxos_file_copy:
        local_file: config.txt
        vrf: management

    - name: Disable SCP feature on switches
      cisco.nxos.nxos_feature:
        feature: scp-server
        state: disabled

Ansible Collections

A collection is a distribution format for Ansible content (modules, plugins, roles, playbooks). Collections are distributed via Ansible Galaxy.

FQCN (Fully Qualified Collection Name) naming:

<namespace>.<collection>.<module>
# Example:
cisco.nxos.nxos_config
│     │     └── Module
│     └──────── Collection
└────────────── Namespace

Managing collections with ansible-galaxy:

# List installed collections
ansible-galaxy collection list

# Check the version of a specific collection
ansible-galaxy collection list cisco.nxos

# Install the latest version
ansible-galaxy collection install cisco.nxos

# Install a specific version
ansible-galaxy collection install cisco.nxos:==2.4.0

# Force reinstall of an older version
ansible-galaxy collection install cisco.nxos:==2.4.0 --force

collections/requirements.yaml file (tracking dependencies):

---
collections:
  - name: cisco.nxos
    version: ">=2.4.0,<3.0.0"
  - name: ansible.netcommon
    version: ">=2.0.0"
  - name: ansible.utils
    version: ">=2.0.0"
# Install all required collections
ansible-galaxy collection install -r collections/requirements.yaml

5. Building Basic Network Automation

Verifying Connectivity to Network Devices

Common issues during first connection:

  1. Connection plugin not found: ansible.netcommon.network_cli connection plugin could not be found

    • Solution: verify collections_scan_sys_path = true in ansible.cfg
  2. Paramiko library missing:

    pip install paramiko
    
  3. SSH host key not in known_hosts:

    # Option 1: add manually
    ssh-keyscan -H <switch_ip> >> ~/.ssh/known_hosts
    # Option 2: disable verification (lab only!)
    # In ansible.cfg: host_key_checking = False
    

Test connectivity with the ansible.builtin.ping module:

# Test a specific host
ansible -i inventory.yaml -m ansible.builtin.ping S1

# Test a group
ansible -i inventory.yaml -m ansible.builtin.ping switches

# Test all hosts
ansible -i inventory.yaml -m ansible.builtin.ping all

Test with a network module:

ansible -i inventory.yaml -m cisco.nxos.nxos_command \
  -a "commands='show version'" S1

Ansible Ad Hoc Commands

Ad hoc commands allow executing a single module against multiple hosts quickly. Ideal for one-time tasks.

General syntax:

ansible -i <inventory> -m <module> -a "<arguments>" <hosts>

Practical examples:

# Enable the SCP server on all switches
ansible -i inventory.yaml -m cisco.nxos.nxos_feature \
  -a "feature=scp-server" all

# Copy a file to all switches
ansible -i inventory.yaml -m cisco.nxos.nxos_file_copy \
  -a "local_file=config.txt vrf=management" all

# Retrieve the hostname of all switches
ansible -i inventory.yaml -m cisco.nxos.nxos_command \
  -a "commands='show hostname'" switches

Ansible Playbooks

For repeatable and more complex tasks, write a playbook.

Execute a playbook:

ansible-playbook -i inventory.yaml playbook.yaml

Example: OSPF and BGP configuration playbook:

---
# playbook.yaml
- name: Configure OSPF on switches
  hosts: switches
  gather_facts: false

  tasks:
    - name: Enable OSPF feature
      cisco.nxos.nxos_config:
        lines:
          - feature ospf

    - name: Configure OSPF process
      cisco.nxos.nxos_config:
        lines:
          - router ospf 1

- name: Configure BGP on switches
  hosts: switches
  gather_facts: false

  tasks:
    - name: Enable BGP feature
      cisco.nxos.nxos_config:
        lines:
          - feature bgp

    - name: Configure BGP process
      cisco.nxos.nxos_config:
        lines:
          - router bgp 65535

Interpretation of Ansible output colors:

ColorMeaning
🟢 Green (ok)Task executed, no change required
🟡 Yellow (changed)Task executed, a change was made
🔴 Red (failed)Task failed
🔵 Cyan (skipped)Task skipped (when condition not met)

The —limit Parameter for Testing

Restricts playbook execution to a subset of targeted hosts without modifying the playbook.

# Target a single host
ansible-playbook -i inventory.yaml playbook.yaml --limit S1

# Target a group
ansible-playbook -i inventory.yaml playbook.yaml --limit west

# Target multiple hosts/groups (separator: :)
ansible-playbook -i inventory.yaml playbook.yaml --limit "west:east"

# Combine with --check for a targeted dry run
ansible-playbook -i inventory.yaml playbook.yaml --limit S1 --check

Important rule: The host/group specified in --limit must be a subset of the group targeted by the playbook.

Leveraging Facts on Network Devices

---
- name: Display and use Ansible facts
  hosts: switches
  gather_facts: true  # Enable fact collection

  tasks:
    - name: Display all facts
      ansible.builtin.debug:
        var: ansible_facts

    - name: Display available filesystems
      ansible.builtin.debug:
        var: ansible_facts['net_filesystems']

    - name: Copy file only if bootflash is available
      cisco.nxos.nxos_file_copy:
        local_file: running_config.txt
        file_system: bootflash:
      when: "'bootflash:' in ansible_facts['net_filesystems']"

Archiving CLI Output to Disk

The delegation concept (delegate_to) allows executing an action on the control node rather than the target host.

---
- name: Archive hostname from network devices
  hosts: switches
  gather_facts: false

  tasks:
    - name: Get configured hostname
      cisco.nxos.nxos_command:
        commands:
          - show hostname
      register: device_hostname

    - name: Display hostname output
      ansible.builtin.debug:
        var: device_hostname.stdout

    - name: Save hostname to file on control node
      ansible.builtin.copy:
        content: "{{ device_hostname.stdout[0] }}"
        dest: "/tmp/hostname_{{ inventory_hostname }}.txt"
      delegate_to: localhost   # Execute on the control node

6. Configuring Network Devices with Ansible

Configuration with Playbooks

flowchart LR
    A["📋 Playbook\nYAML"] --> B["🔍 Ansible parses\nthe playbook"]
    B --> C{"Facts\nneeded?"}
    C -->|Yes| D["📊 Gather Facts\nfrom hosts"]
    C -->|No| E["⚙️ Execute tasks\non hosts"]
    D --> E
    E --> F{"Change\nneeded?"}
    F -->|Yes - changed 🟡| G["✏️ Apply the\nchange"]
    F -->|No - ok 🟢| H["✔️ No action\nrequired"]
    G --> I["📊 Final report\n(recap)"]
    H --> I

Example: Enable OSPF and BGP with the nxos_config module:

---
- name: Configure OSPF and BGP features
  hosts: switches
  gather_facts: false

  tasks:
    - name: Enable OSPF feature
      cisco.nxos.nxos_config:
        lines:
          - feature ospf

    - name: Configure OSPF process
      cisco.nxos.nxos_config:
        lines:
          - router ospf 1

    - name: Enable BGP feature
      cisco.nxos.nxos_config:
        lines:
          - feature bgp

    - name: Configure BGP process
      cisco.nxos.nxos_config:
        lines:
          - router bgp 65535

    - name: Configure BGP address-family
      cisco.nxos.nxos_config:
        parents: router bgp 65535
        lines:
          - address-family ipv4 unicast

Idempotence

Idempotence is the property whereby an operation can be applied multiple times without changing the result beyond the first application.

Idempotence example on NX-OS:

switch# configure terminal
switch(config)# feature ospf   # First call: enables OSPF
switch(config)# feature ospf   # Second call: nothing changes
switch(config)# feature ospf   # Third call: nothing changes

Idempotence in Ansible:

  • If the desired state is already reached → status ok (green)
  • If a change is needed → status changed (yellow)
  • Ansible compares the current state with the desired state before acting

Best practice: Run playbooks regularly to automatically detect and fix configuration drift.

Check Mode (Dry Run)

Check mode performs a non-destructive dry run of the playbook. Ansible reports what it would do without actually applying changes.

# Dry run of the full playbook
ansible-playbook -i inventory.yaml playbook.yaml --check

# Targeted dry run on S1 only
ansible-playbook -i inventory.yaml playbook.yaml --check --limit S1

# Dry run with diff (shows lines that would be added/removed)
ansible-playbook -i inventory.yaml playbook.yaml --check --diff

Recommended workflow for change windows:

  1. During normal hours: --check to plan and validate
  2. During the change window: normal execution to apply

Configuration with Variables

# group_vars/switches.yaml
---
bgp_asn: 65535
dns_servers:
  - 8.8.8.8
  - 8.8.4.4
ntp_server: ntp.corp.local
# host_vars/s1.yaml
---
loopback: 10.1.1.1
bgp_router_id: 10.1.1.1
# playbook.yaml
---
- name: Configure network devices with variables
  hosts: switches
  gather_facts: false

  tasks:
    - name: Configure DNS servers
      cisco.nxos.nxos_config:
        lines:
          - "ip name-server {{ item }}"
      loop: "{{ dns_servers }}"

    - name: Configure NTP server
      cisco.nxos.nxos_config:
        lines:
          - "ntp server {{ ntp_server }}"

    - name: Configure BGP process with ASN variable
      cisco.nxos.nxos_config:
        lines:
          - "router bgp {{ bgp_asn }}"

    - name: Configure loopback interface
      cisco.nxos.nxos_config:
        parents: "interface Loopback0"
        lines:
          - "ip address {{ loopback }}/32"

Configuration with JSON/YAML Data

To load configuration data from an external file:

interface_data.json file:

{
  "S1": [
    {
      "interface": "Loopback0",
      "ip": "10.1.1.1/32",
      "description": "Loopback interface S1"
    },
    {
      "interface": "Ethernet1/1",
      "ip": "192.168.12.1/30",
      "description": "Link to S2"
    }
  ],
  "S2": [
    {
      "interface": "Loopback0",
      "ip": "10.2.2.2/32",
      "description": "Loopback interface S2"
    },
    {
      "interface": "Ethernet1/1",
      "ip": "192.168.12.2/30",
      "description": "Link to S1"
    }
  ]
}

Playbook using a lookup plugin and filters:

---
- name: Configure Layer 3 interfaces from JSON data
  hosts: switches
  gather_facts: false

  tasks:
    - name: Load interface data from JSON file
      ansible.builtin.set_fact:
        interface_data: "{{ lookup('file', 'interface_data.json') | from_json }}"

    - name: Configure layer 3 interfaces
      cisco.nxos.nxos_config:
        parents: "interface {{ item.interface }}"
        lines:
          - "description {{ item.description }}"
          - "ip address {{ item.ip }}"
          - "no shutdown"
      loop: "{{ interface_data[inventory_hostname] }}"

Debugging Playbooks

Ansible’s built-in debugger allows inspecting the state of a task during execution.

Possible values for debugger:

ValueBehavior
neverNever (default)
alwaysAlways
on_failedOnly if the task fails
on_unreachableIf the host is unreachable
on_skippedIf the task is skipped (when condition = false)
---
- name: Debug example
  hosts: switches
  gather_facts: false

  tasks:
    - name: Load interface data
      ansible.builtin.set_fact:
        interface_data: "{{ lookup('file', 'interface_data.json') | from_json }}"
      debugger: on_failed  # Enter the debugger if this task fails

Interactive debugger commands:

[1] TASK DEBUGGER> p task          # Display the task name
[1] TASK DEBUGGER> p host          # Display the target host
[1] TASK DEBUGGER> p task.args     # Display the task arguments
[1] TASK DEBUGGER> p result        # Display the task result
[1] TASK DEBUGGER> vars            # Display all available variables
[1] TASK DEBUGGER> r               # Replay (retry) the task
[1] TASK DEBUGGER> c               # Continue execution
[1] TASK DEBUGGER> q               # Quit the debugger and execution

Resource Modules

Resource modules are specialized Ansible modules that manage the configuration of a specific network feature in a declarative manner. They embody the “restaurant menu” concept from Ansible’s declarative philosophy.

Advantages of resource modules:

  • Declarative syntax (desired state, not a command sequence)
  • Guaranteed idempotence
  • No need to know the exact CLI syntax
  • Dependency management (e.g., enabling a feature before configuring it)

state parameter of resource modules:

StateDescription
mergedMerge desired config with existing config
replacedReplace the relevant config section
overriddenReplace the entire resource configuration
deletedRemove the configuration
gatheredCollect current configuration as facts
renderedDisplay the configuration that would be generated
parsedParse an existing configuration

Example: Layer 3 interface configuration with resource modules:

---
- name: Configure L3 interfaces with resource modules
  hosts: switches
  gather_facts: false

  tasks:
    - name: Configure layer 3 interfaces
      cisco.nxos.nxos_l3_interfaces:
        config:
          - name: Loopback0
            ipv4:
              - address: "{{ loopback }}/32"
          - name: Ethernet1/1
            ipv4:
              - address: "{{ eth1_ip }}/30"
        state: merged

    - name: Configure OSPF with resource module
      cisco.nxos.nxos_ospfv2:
        config:
          processes:
            - process_id: "1"
              router_id: "{{ loopback }}"
              areas:
                - area_id: "0.0.0.0"
                  default_cost: 100
        state: merged

    - name: Configure BGP with resource module
      cisco.nxos.nxos_bgp_global:
        config:
          as_number: "{{ bgp_asn }}"
          router_id: "{{ bgp_router_id }}"
          neighbors:
            - neighbor_address: "{{ bgp_peer_ip }}"
              remote_as: "{{ bgp_peer_asn }}"
        state: merged

Alternative Transport Methods (NETCONF/API)

# group_vars/all.yaml - Connect via HTTP API (NXAPI)
---
ansible_connection: ansible.netcommon.httpapi
ansible_network_os: cisco.nxos.nxos
ansible_httpapi_use_ssl: false
ansible_httpapi_validate_certs: false

“Catch-22” situation: Enable the API before using it:

---
# First play: enable NXAPI via SSH
- name: Configure NX-API feature
  hosts: switches
  gather_facts: false
  vars:
    ansible_connection: ansible.netcommon.network_cli  # Override for this play

  tasks:
    - name: Enable NX-API HTTP access
      cisco.nxos.nxos_nxapi:
        http: true
        http_port: 80
        state: present

# Subsequent plays: use the API (httpapi connection from group_vars)
- name: Configure BGP via NX-API
  hosts: switches
  gather_facts: false

  tasks:
    - name: Configure BGP process
      cisco.nxos.nxos_bgp_global:
        config:
          as_number: "{{ bgp_asn }}"
        state: merged

7. Creating & Using Ansible Roles

Ansible Roles Philosophy

Roles allow organizing automation into a reusable and shareable format. They replicate the paradigm of real network roles:

Example with the Globomantics environment:

Network roleAssociated configuration
network_devicesDNS, NTP, syslog, NXAPI (base services)
routersLayer 3 interfaces, IP addressing
ospf_routersOSPF feature, OSPF process, OSPF interfaces
bgp_speakersBGP feature, BGP process, peers, address-families

Philosophy: Separate what varies per device (variables in host_vars) from what is common to all devices of a role (tasks in the role).

Role Directory Structure

roles/
└── network_devices/          # One role = one folder
    ├── tasks/
    │   ├── main.yaml         # Task entry point (required)
    │   ├── dns.yaml          # DNS tasks (imported from main.yaml)
    │   ├── ntp.yaml          # NTP tasks
    │   └── syslog.yaml       # Syslog tasks
    ├── handlers/
    │   └── main.yaml         # Handlers (e.g., restart service after config)
    ├── templates/
    │   └── ntp.conf.j2       # Jinja2 templates
    ├── files/
    │   └── banner.txt        # Static files to deploy
    ├── vars/
    │   └── main.yaml         # Role variables (high priority)
    ├── defaults/
    │   └── main.yaml         # Default variables (low priority, easily overridden)
    ├── meta/
    │   └── main.yaml         # Metadata, role dependencies
    └── README.md             # Role documentation

defaults/main.yaml folder: Low-priority variables, easily overridden by the inventory.

---
# defaults/main.yaml for the network_devices role
dns_servers:
  - 8.8.8.8
ntp_server: pool.ntp.org
syslog_server: 192.0.2.100

Refactoring with Roles

site.yaml file (main entry point):

---
# site.yaml
- import_playbook: switches.yaml

switches.yaml file:

---
# switches.yaml
- name: Configure Nexus switches
  hosts: switches
  gather_facts: true

  roles:
    - network_devices
    - routers
    - ospf_routers
    - bgp_speakers

roles/network_devices/tasks/main.yaml file:

---
# tasks/main.yaml for the network_devices role
- name: Configure NXAPI
  cisco.nxos.nxos_nxapi:
    http: true
    http_port: 80
    state: present
  when: ansible_network_os == 'cisco.nxos.nxos'

- name: Import DNS tasks
  ansible.builtin.import_tasks: dns.yaml

- name: Import NTP tasks
  ansible.builtin.import_tasks: ntp.yaml

- name: Import syslog tasks
  ansible.builtin.import_tasks: syslog.yaml

roles/network_devices/tasks/dns.yaml file:

---
- name: Configure DNS servers
  cisco.nxos.nxos_config:
    lines:
      - "ip name-server {{ item }}"
  loop: "{{ dns_servers }}"
  tags:
    - dns

roles/network_devices/tasks/syslog.yaml file:

---
- name: Configure syslog server
  cisco.nxos.nxos_logging:
    dest: server
    dest_level: 5
    remote_server: "{{ syslog_server }}"
    state: present
  tags:
    - syslog

roles/ospf_routers/tasks/main.yaml file:

---
- name: Enable OSPF feature
  cisco.nxos.nxos_feature:
    feature: ospf
    state: enabled

- name: Configure OSPF with resource module
  cisco.nxos.nxos_ospfv2:
    config:
      processes:
        - process_id: "1"
          router_id: "{{ loopback }}"
    state: merged

- name: Activate interfaces under OSPF
  cisco.nxos.nxos_ospf_interfaces:
    config:
      - name: Ethernet1/1
        address_family:
          - afi: ipv4
            area:
              area_id: "0.0.0.0"
    state: merged

Tags for Testing Roles and Playbooks

Tags allow executing only specific parts of an Ansible project without modifying playbooks.

Adding tags to tasks:

---
- name: Configure syslog server
  cisco.nxos.nxos_logging:
    dest: server
    dest_level: 5
    remote_server: "{{ syslog_server }}"
    state: present
  tags:
    - syslog
    - logging

Adding tags to an entire role:

# switches.yaml
- name: Configure Nexus switches
  hosts: switches
  roles:
    - role: network_devices
      tags: [network_devices, base]
    - role: ospf_routers
      tags: [ospf, routing]
    - role: bgp_speakers
      tags: [bgp, routing]

Using tags on the command line:

# Run only tasks with the "syslog" tag
ansible-playbook -i inventory.yaml site.yaml --tags syslog

# Run tasks with "ospf" or "bgp" tags
ansible-playbook -i inventory.yaml site.yaml --tags "ospf,bgp"

# Skip tasks with the "syslog" tag
ansible-playbook -i inventory.yaml site.yaml --skip-tags syslog

# List all available tags in a playbook
ansible-playbook -i inventory.yaml site.yaml --list-tags

# Combine tags and limit for very targeted testing
ansible-playbook -i inventory.yaml site.yaml --tags syslog --limit S1

8. Reference Tables

Common Network Modules (cisco.nxos)

ModuleDescriptionKey Arguments
cisco.nxos.nxos_configGeneric CLI configurationlines, parents, src
cisco.nxos.nxos_commandExecute show commandscommands
cisco.nxos.nxos_featureEnable/disable featuresfeature, state
cisco.nxos.nxos_file_copyCopy fileslocal_file, vrf
cisco.nxos.nxos_l3_interfacesLayer 3 interfacesconfig, state
cisco.nxos.nxos_ospfv2OSPFv2 configurationconfig, state
cisco.nxos.nxos_bgp_globalGlobal BGP configurationconfig, state
cisco.nxos.nxos_bgp_neighbor_address_familyBGP address-familiesconfig, state
cisco.nxos.nxos_loggingSyslog configurationdest, dest_level, remote_server, state
cisco.nxos.nxos_nxapiNXAPI configurationhttp, http_port, state
cisco.nxos.nxos_vlansVLAN configurationconfig, state

Useful Ansible Built-in Modules

ModuleDescriptionKey Arguments
ansible.builtin.pingTest connectivity(none)
ansible.builtin.debugDisplay variables/messagesvar, msg
ansible.builtin.copyCopy a filecontent, dest, src
ansible.builtin.set_factDefine variables<variable_name>: <value>
ansible.builtin.templateDeploy a Jinja2 templatesrc, dest
ansible.builtin.import_tasksImport tasks<file.yaml>
ansible.builtin.include_tasksDynamically include tasks<file.yaml>
ansible.builtin.import_playbookImport a playbook<playbook.yaml>

Network Connection Variables (inventory)

VariableDescriptionCommon Values
ansible_connectionConnection pluginansible.netcommon.network_cli, ansible.netcommon.httpapi, ansible.netcommon.netconf
ansible_network_osNetwork OS of the devicecisco.nxos.nxos, cisco.ios.ios, cisco.iosxr.iosxr, juniper.junos.junos, arista.eos.eos
ansible_userSSH usernameadmin, netadmin, etc.
ansible_passwordSSH passwordUse ansible-vault
ansible_hostDevice IP address or FQDN192.168.1.1
ansible_portSSH port22 (default)
ansible_ssh_private_key_fileSSH private key~/.ssh/id_rsa
ansible_becomeEnable privilege escalationtrue/false
ansible_become_methodEscalation methodenable (Cisco)
ansible_become_passwordEnable passwordUse ansible-vault
ansible_httpapi_use_sslUse HTTPS for httpapitrue/false
ansible_httpapi_validate_certsValidate SSL certificatestrue/false

Essential Ansible CLI Commands

CommandDescription
ansible --versionCheck installed version
ansible-playbook playbook.yamlExecute a playbook
ansible-playbook playbook.yaml --checkDry run
ansible-playbook playbook.yaml --diffShow differences
ansible-playbook playbook.yaml --limit S1Target a host
ansible-playbook playbook.yaml --tags ospfExecute by tag
ansible-playbook playbook.yaml -vVerbose mode (up to -vvvv)
ansible -m ping allAd hoc command
ansible-inventory --listDisplay inventory as JSON
ansible-inventory --graphDisplay the inventory graph
ansible-galaxy collection install <collection>Install a collection
ansible-galaxy collection listList installed collections
ansible-config dumpDisplay active configuration
ansible-lint playbook.yamlLint the playbook

9. Architecture Diagrams

Ansible Architecture: Control Node & Managed Nodes

graph TB
    subgraph "Ansible Control Node"
        AN["🖥️ Ansible\n(Python virtualenv)"]
        CFG["📄 ansible.cfg"]
        PB["📋 Playbooks YAML"]
        INV["📦 Inventory"]
        ROLES["🗂️ Roles"]
        GV["📁 group_vars/\nhost_vars/"]
        COL["📦 Collections\n(cisco.nxos, etc.)"]
    end

    subgraph "Globomantics Network"
        S1["🔀 Switch S1\n(NX-OS)\n10.1.1.1"]
        S2["🔀 Switch S2\n(NX-OS)\n10.2.2.2"]
    end

    AN -->|"SSH (network_cli)\nor HTTP (httpapi)\nor NETCONF"| S1
    AN -->|"SSH (network_cli)\nor HTTP (httpapi)\nor NETCONF"| S2
    CFG -.-> AN
    PB -.-> AN
    INV -.-> AN
    ROLES -.-> AN
    GV -.-> AN
    COL -.-> AN

    style AN fill:#1f6feb,color:#fff
    style S1 fill:#238636,color:#fff
    style S2 fill:#238636,color:#fff

Playbook Execution Flow

sequenceDiagram
    participant ENG as 👤 Network Engineer
    participant CN as 🖥️ Control Node
    participant S1 as 🔀 Switch S1
    participant S2 as 🔀 Switch S2

    ENG->>CN: ansible-playbook -i inventory.yaml playbook.yaml
    CN->>CN: Load inventory + variables
    CN->>CN: Parse the YAML playbook

    Note over CN,S2: Play 1: Configure OSPF

    CN->>S1: SSH: gather_facts
    S1-->>CN: facts (version, interfaces, etc.)
    CN->>S2: SSH: gather_facts
    S2-->>CN: facts

    CN->>S1: Task: Enable OSPF feature
    S1-->>CN: ok / changed
    CN->>S2: Task: Enable OSPF feature
    S2-->>CN: ok / changed

    CN->>S1: Task: Configure OSPF process
    S1-->>CN: ok / changed
    CN->>S2: Task: Configure OSPF process
    S2-->>CN: ok / changed

    Note over CN,S2: Final summary

    CN-->>ENG: PLAY RECAP:\nS1 : ok=3 changed=1 failed=0\nS2 : ok=3 changed=0 failed=0

Inventory Structure and Variable Inheritance

graph TD
    ALL["📦 Group: all\nntp=ntp.global.corp.local\nansible_user=netadmin"]
    SW["📦 Group: switches\nbgp_asn=65535"]
    W["📦 Group: west\nntp=ntp.west.corp.local"]
    E["📦 Group: east\nntp=ntp.east.corp.local"]
    S1["🔀 Host: S1\nloopback=10.1.1.1\n← ntp inherited: ntp.west.corp.local\n← bgp_asn inherited: 65535"]
    S2["🔀 Host: S2\nloopback=10.2.2.2\n← ntp inherited: ntp.east.corp.local\n← bgp_asn inherited: 65535"]

    ALL --> SW
    SW --> W
    SW --> E
    W --> S1
    E --> S2

    style ALL fill:#6e40c9,color:#fff
    style SW fill:#1f6feb,color:#fff
    style W fill:#0969da,color:#fff
    style E fill:#0969da,color:#fff
    style S1 fill:#238636,color:#fff
    style S2 fill:#238636,color:#fff

Network Automation Pipeline with Ansible

flowchart LR
    A["📝 Define\ndesired state\n(YAML/JSON)"] --> B["✅ Validate\nwith\nansible-lint\nyamllint"]
    B --> C["🔍 Dry run\n--check\n--diff"]
    C --> D{"Changes\ncorrect?"}
    D -->|No| A
    D -->|Yes| E["🔒 Change\nWindow\n(production)"]
    E --> F["▶️ Run\nansible-playbook"]
    F --> G{"Success?"}
    G -->|failed| H["🐛 Debugger\n--debugger\n-vvvv"]
    H --> A
    G -->|ok/changed| I["📊 Verify\ncompliance\n(--check post)"]
    I --> J["💾 Archive\noutputs\n(delegate_to: localhost)"]

    style A fill:#6e40c9,color:#fff
    style E fill:#da3633,color:#fff
    style F fill:#1f6feb,color:#fff
    style I fill:#238636,color:#fff

Ansible Role Structure

graph LR
    subgraph "📋 site.yaml (entry point)"
        SITE["import_playbook:\nswitches.yaml"]
    end

    subgraph "📋 switches.yaml"
        SW["hosts: switches\nroles:\n- network_devices\n- routers\n- ospf_routers\n- bgp_speakers"]
    end

    subgraph "🗂️ roles/network_devices/"
        TD["tasks/main.yaml\n→ dns.yaml\n→ ntp.yaml\n→ syslog.yaml"]
        HD["handlers/main.yaml"]
        DD["defaults/main.yaml\n(low priority variables)"]
    end

    subgraph "🗂️ roles/ospf_routers/"
        TO["tasks/main.yaml\n(OSPF feature + process\n+ interfaces)"]
    end

    subgraph "🗂️ roles/bgp_speakers/"
        TB["tasks/main.yaml\n(BGP feature + process\n+ peers + AF)"]
    end

    SITE --> SW
    SW --> TD
    SW --> TO
    SW --> TB
    TD -.-> HD
    TD -.-> DD

Recommended next steps: Course: Automating Networks with Ansible the Right Way — to deepen advanced patterns for managing networks with Ansible in production.


Search Terms

ansible · network · automation · networking · web · servers · systems · security · configuration · playbooks · roles · control · devices · installation · inventory · node · variables · architecture · cli · commands · data · facts · inheritance · package

Interested in this course?

Contact us to book it or get a custom training plan for your team.