Network Automation Ansible

Production engineering guide for network automation ansible covering patterns, implementation strategies, and operational best practices.

Network automation with Ansible is the practice of modeling, deploying, and maintaining network infrastructure—routers, switches, firewalls, load balancers—using declarative, idempotent playbooks. It matters when network changes must be repeatable, consistent across environments, and versioned with code: during CI/CD pipelines, during infrastructure provisioning, or in response to incidents requiring rapid, multi-device configuration rollouts.

Managing Network Devices with Ansible

Ansible uses a simple, agentless model: control nodes execute tasks over SSH or API to managed nodes. For network devices, this means no daemons or agents on routers and switches—just an SSH server and a standard OS shell.

Connecting to Network Devices

Ansible connects to network devices via network inventory plugins. Each device is defined with a host group, and the connection is configured in ansible.cfg.

[defaults]
inventory = ./inventory
remote_user = admin
become = true
become_method = enable
become_user = root
host_key_checking = false

Inventory files use the network group and define device-specific connection parameters.


[all:vars]
ansible_network_os = ios
ansible_ssh_common_args = -o StrictHostKeyChecking=no

[routers]
router1 ansible_host=192.168.1.1 ansible_user=admin
router2 ansible_host=192.168.1.2 ansible_user=admin

[switches]
switch1 ansible_host=192.168.1.10 ansible_user=admin
switch2 ansible_host=192.168.1.11 ansible_user=admin

The ansible_network_os variable determines the connection plugin. For Cisco IOS, it uses network_cli over SSH.

Writing a Basic Configuration Playbook

A playbook applies configuration to a group of network devices.

---
- name: Configure network devices
  hosts: routers
  gather_facts: false
  vars:
    ip_address: 10.0.0.1
    subnet_mask: 255.255.255.0
    default_gateway: 10.0.0.254

  tasks:
    - name: Set hostname
      ios_command:
        commands:
          - hostname {{ inventory_hostname }}
          - write memory
      register: result

    - name: Configure interface
      ios_config:
        lines:
          - interface GigabitEthernet0/0
          - ip address {{ ip_address }} {{ subnet_mask }}
          - no shutdown
        replace: line
      register: config_result

    - name: Apply configuration
      ios_command:
        commands:
          - show running-config
          - show ip interface brief
        output: json
      register: show_result

    - name: Log configuration changes
      debug:
        msg: "Applied config to {{ inventory_hostname }}"

The ios_command module sends commands directly over the CLI. ios_config applies configuration in bulk and supports replace: line, replace: full, and replace: merge.

Idempotency and State Modeling

Ansible ensures idempotency through the state parameter in modules.

- name: Ensure VLAN 10 exists
  ios_vlan:
    vlan_id: 10
    name: Marketing
    state: present

If VLAN 10 already exists, no commands are sent. If it does not, Ansible creates it and applies the name.

The state: absent removes a VLAN:

- name: Remove VLAN 10
  ios_vlan:
    vlan_id: 10
    state: absent

Handling Configuration Templates

For complex configurations, use Jinja2 templates with ios_config.

- name: Configure OSPF
  ios_config:
    lines:
      - router ospf 1
      - network 10.0.0.0 0.0.0.255 area 0
      - passive-interface default
      - no passive-interface GigabitEthernet0/0
    src: templates/ospf.j2
    backup: yes

src references a Jinja2 template file.

{# templates/ospf.j2 #}
router ospf {{ ospf_process_id }}
  network {{ network_range }} area {{ area_id }}
  passive-interface default
  no passive-interface GigabitEthernet0/0
  {% for intf in passive_interfaces %}
  passive-interface {{ intf }}
  {% endfor %}

The backup: yes parameter creates a backup of the running config before applying changes.

Using network_cli for Direct Command Execution

When the ios_config module is insufficient, use ios_command to execute arbitrary CLI commands.

- name: Enable BGP and verify
  ios_command:
    commands:
      - show running-config | section router bgp
      - show ip bgp summary
      - show ip bgp neighbors
    output: json
  register: bgp_output

- name: Parse BGP output
  debug:
    msg: "BGP neighbors: {{ bgp_output.stdout[1] | from_json }}"

The output: json flag returns structured data from the device.

Handling Multiple Network OSes

Ansible supports multiple network operating systems via the network_os variable. For Juniper JunOS:

[all:vars]
ansible_network_os = junos

[junos_routers]
junos1 ansible_host=192.168.2.1 ansible_user=admin

- name: Configure Juniper interface
  junos_config:
    lines:
      - set interfaces ge-0/0/0 unit 0 family inet address 192.168.1.1/24
      - set interfaces ge-0/0/0 unit 0 description "Upstream"
    commit: yes
    diff: yes

For Arista EOS:

[all:vars]
ansible_network_os = eos

[eos_switches]
eos1 ansible_host=192.168.3.1 ansible_user=admin

- name: Configure VLANs on EOS
  eos_config:
    lines:
      - vlan 10
      - name Marketing
      - exit
      - vlan 20
      - name Engineering
    src: configs/vlans.eos
    replace: full

The replace: full option replaces the entire configuration, while replace: merge combines new config with existing.

Managing Configuration Backups and Rollbacks

Backups are critical for network change management.

- name: Backup running configuration
  ios_config:
    save: yes
    backup: yes
  register: backup_result

- name: Verify backup was created
  assert:
    that:
      - backup_result.backup_file | basename | match("running-config")
    fail_msg: "Backup not created in expected format"

The save: yes parameter executes write memory after configuration. backup: yes copies the running config to the control node.

Rollbacks use ios_config with rollback: true.

- name: Rollback configuration
  ios_config:
    rollback: 1
  register: rollback_result

A rollback: 1 returns to the last saved configuration.

Error Handling and Debugging

Common failure modes include SSH timeouts, command mismatches, and configuration diffs.

SSH Connection Failures

When Ansible cannot reach a device:

fatal: [router1]: UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: connect to host 192.168.1.1 port 22: Connection timed out",
    "unreachable": true
}

Fix by verifying SSH access and ansible_network_os.

Command Execution Failures

A command may fail with exit status 1, and Ansible logs the output.

- name: Configure interface
  ios_command:
    commands:
      - interface GigabitEthernet0/0
      - ip address 192.168.1.100 255.255.255.0
      - no shutdown
      - ip address 192.168.1.100 255.255.255.0   # duplicate
    output: text
  register: result

- name: Check for duplicate IP error
  assert:
    that:
      - result.stdout | join('\n') | regex_search("Duplicate address")
    fail_msg: "Interface configuration failed due to duplicate IP"

The output: text returns raw CLI output. Use output: json for structured data.

Configuration Diff Errors

When ios_config detects a difference, it includes diff in the result.

- name: Apply configuration with diff
  ios_config:
    lines:
      - interface GigabitEthernet0/0
      - ip address 192.168.1.100 255.255.255.0
      - no shutdown
    diff: yes
  register: config_result

- name: Log configuration diff
  debug:
    msg: "Config diff: {{ config_result.diff }}"

The diff output shows what was added, removed, or changed.

Advanced: Using Playbooks Across Environments

For multi-environment deployments, use variable files.

# group_vars/routers.yml
ios_config:
  backup: yes
  commit: yes
  replace: full
  diff: yes

# group_vars/switches.yml
ios_vlan:
  state: present
  name: "{{ vlan_name | default('default') }}"

Playbooks can be run with ansible-playbook and --limit, --tags, and --diff.

ansible-playbook site.yml --limit routers --tags "ospf,vlan" --diff

The --diff flag shows changes in configuration files.

Managing Multiple Devices with Dynamic Inventories

For large environments, use dynamic inventory scripts.

# dynamic_inventory.py
#!/usr/bin/env python3
import json
import subprocess

def get_devices():
    result = subprocess.run(
        ["curl", "https://api.example.com/network/devices"],
        capture_output=True,
        text=True
    )
    devices = json.loads(result.stdout)
    return {
        "all": {
            "hosts": [d["hostname"] for d in devices],
            "vars": {
                "ansible_network_os": "ios",
                "ansible_user": "admin"
            }
        },
        "routers": {
            "hosts": [d["hostname"] for d in devices if d["type"] == "router"]
        },
        "switches": {
            "hosts": [d["hostname"] for d in devices if d["type"] == "switch"]
        }
    }

if __name__ == "__main__":
    print(json.dumps(get_devices()))

Run with:

ansible-inventory -i dynamic_inventory.py --list

Best Practices and Sharp Edges

- name: Apply OSPF config
  ios_config:
    lines:
      - router ospf 1
      - network 10.0.0.0 0.0.0.255 area 0
    backup: yes
  register: ospf_result

- name: Rollback on failure
  ios_config:
    rollback: 1
  when: ospf_result.failed

Common Pitfalls

Network automation with Ansible is not just about running commands—it is about modeling the network as code, ensuring consistency, versioning changes, and enabling rapid, reliable deployment across devices.

This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.