Ansible for Network Automation: Complete Guide - 夜莺博客

Ansible for Network Automation: Complete Guide

Ansible 是网络自动化的事实标准,本指南面向想系统上手的人:从最小 playbook 快速开始,到带 block/rescue 错误处理的生产级模式,再到 check mode、幂等性、变量化与超时处理等最佳实践,并附常见问题速查表,覆盖路由器、交换机与防火墙的自动化。

This guide is a working reference for using Ansible against network devices — routers, switches and firewalls — rather than servers. Network automation is different from server automation in ways that break the habits you build on Linux hosts: there is no persistent SSH session you can leave open and run commands in, no Python interpreter on the remote side, no package manager and no filesystem to write to. Ansible solves this with persistent connections and network-specific modules, but those differences drive almost every design decision in the playbooks below.

What Ansible Brings to Network Automation

Ansible is agentless: it connects over the management plane you already use — SSH or, on some platforms, NETCONF over SSH — and runs against device APIs through modules. There is nothing to install on the switch, no agent to keep alive, and no minimum firmware beyond what a given module requires. Playbooks are YAML, the inventory is a plain file or a dynamic source, and roles let you package reusable configuration logic. The practical result is that a task which used to be "log into 40 switches and paste the same block" becomes one command that reports exactly which devices changed and which were already correct.

The three properties worth internalising are idempotency (running the same playbook twice should not change anything the second time), declarative intent (you describe the state you want, and the module works out the commands), and readability (YAML reviewable by a network engineer who does not write Python). Keep those three in mind and the rest of the guide follows naturally.

Prerequisites and Inventory

On the control node you need a recent Ansible release and the network collections for your platforms. The classic modules live in ansible.netcommon, cisco.ios, cisco.nxos, arista.eos, juniper.junos and similar collections, and they are installed with the collection manager rather than with the core package:

# control node setup
python3 -m pip install --user ansible
ansible-galaxy collection install cisco.ios arista.eos juniper.junos ansible.netcommon

# confirm the modules are visible
ansible-doc -t module cisco.ios.ios_config | head -40

The inventory describes your devices. Group them by role (core, distribution, access), by vendor (ios, eos, junos) and by site, because almost every variable you set — credentials, NTP servers, SNMP community — will be scoped to one of those groups. A minimal inventory with connection details looks like this:

# inventory.ini
[switches:children]
access
core

[access]
access01 ansible_host=10.10.1.11
access02 ansible_host=10.10.1.12

[core]
core01 ansible_host=10.10.0.1

[switches:vars]
ansible_network_os=cisco.ios.ios
ansible_connection=network_cli
ansible_user=automation
ansible_password={{ vault_password }}
ansible_become=yes
ansible_become_method=enable

Two settings do most of the work here. ansible_connection=network_cli tells Ansible this is a network device rather than a Linux host, so it uses the persistent connection and the network module path. ansible_network_os tells the platform-specific modules which CLI dialect and prompt patterns to expect. Getting either wrong produces the classic error where Ansible tries to run Python over SSH and reports a module-not-found or JSON parse failure.

Quick Start

---
- name: Ansible for Network Automation
  hosts: all
  become: true
  tasks:
    - name: Execute task
      ansible.builtin.debug:
        msg: "Implementing ansible for network automation"

This is the smallest valid playbook and it is worth running first, because a successful "ok" here proves the inventory parses, the connection works and the becoming/privilege method is accepted. Check the inventory with a targeted ping before touching configuration:

ansible all -i inventory.ini -m ansible.netcommon.net_ping
ansible all -i inventory.ini -m cisco.ios.ios_command -a "commands=show version"

Use --limit to restrict the run to one device while you are learning (--limit access01), and add -vvv when something does not connect — the verbose output shows the exact SSH exchange and prompt matching, which is where most first-run problems live.

Connecting to Network Devices: network_cli and Connection Options

Under the hood, network_cli keeps a single SSH session open for the duration of the task and speaks the CLI through it. There is no shell, so modules are "local" in Ansible's model: the collection runs on the control node and only the resulting CLI commands travel over the wire. That has three consequences you should design around. First, the device does not need Python or any agent; only the control node does. Second, you cannot use ordinary ansible.builtin modules that assume a remote filesystem — use network modules instead. Third, because the session is shared, a task that leaves the device in a sub-shell or a paging prompt can break the following task; network modules reset the session at the end of each task to avoid this.

For platforms that support it, you can move from CLI scraping to structured data by switching to a NETCONF or REST connection. The configuration syntax stays similar while the payload becomes XML or JSON, which makes the output far more reliable to parse than a wall of terminal text. Use ansible.netcommon.netconf or the platform REST modules when the vendor supports them well, and fall back to CLI modules where they do not.

Production Pattern: Error Handling

Wrap any task that changes a device in block/rescue so a failure on one device does not leave the rest of the run in an unknown state, and use always for cleanup that must happen regardless of outcome:

- name: Advanced implementation with error handling
  block:
    - name: Main task
      ansible.builtin.debug:
        msg: "Ansible for Network Automation - advanced method"
    - name: Push intended configuration
      cisco.ios.ios_config:
        src: templates/access.j2
  rescue:
    - name: Handle failure
      ansible.builtin.debug:
        msg: "Task failed, executing recovery"
    - name: Record the failure
      ansible.builtin.debug:
        msg: "Roll back or escalate: device {{ inventory_hostname }} failed"
  always:
    - name: Save running-config after any outcome
      cisco.ios.ios_config:
        save_when: modified

Three layers of handling matter in practice. The block/rescue pattern above handles per-device failures inside a play. The serial keyword handles them across the fleet by limiting how many devices change at once, so a bad template takes down one batch rather than everything:

- name: Push to access layer one batch at a time
  hosts: access
  serial: 5
  max_fail_percentage: 20
  tasks:
    - name: Apply intended configuration
      cisco.ios.ios_config:
        src: templates/access.j2

And any_errors_fatal handles the case where a failure should stop the whole run immediately rather than leave you with a half-migrated network. Choose deliberately: for a read-only fact gathering you want to continue past failures; for a routing change you usually want to stop the world at the first failure and investigate.

Idempotency for Network Devices

Network modules are idempotent by design, but only when you give them the state you actually want. ios_config compares the lines you supply against the running configuration and only issues the missing commands, reporting changed: false when the device already matches. That behaviour falls apart the moment you use an inline command that always runs, or a *_command module that sends whatever you give it without comparison. The rules that keep idempotency intact:

  • Prefer *_config and *_resource modules over *_command modules for anything that changes state.
  • Use match: line or match: strict in ios_config so the comparison is deliberate rather than the loose default.
  • Avoid timestamps, sequential IDs and free-text banners that change on every run.
  • Let modules manage the parent/child hierarchy instead of pasting a full block as an escaped string.

A useful check is to run a playbook twice in a row and confirm the second run reports zero changes on every host. If it does not, something in the playbook is non-deterministic, and the second-run diff is the fastest way to find it.

Check Mode, Diff and Dry Runs

Always test before applying. Check mode makes supported modules report what they would change without changing it, and diff mode shows the before/after:

ansible-playbook -i inventory.ini site.yml --check --diff
ansible-playbook -i inventory.ini site.yml --limit access01 --check --diff

Check mode support varies by module, and network modules are honest about it: a module that cannot predict its changes skips in check mode with a clear message rather than pretending. That means check mode is a filter, not a guarantee — treat it as the strongest signal available short of a lab, and pair it with the serial batching above so that the first real change happens on the smallest possible blast radius.

Variables, Group Vars and Templating

Keep the playbook logic-free and push everything environment-specific into variables: group_vars for values shared by a group, host_vars for the exceptions, and a role under roles/ for the reusable logic.

# group_vars/access.yml
ntp_servers:
  - 10.0.0.10
  - 10.0.0.11
snmp_location: "DC1 rack 12"
vlan_list:
  - id: 10
    name: users
  - id: 20
    name: voice

Then a Jinja2 template turns that data into device configuration, so the same template serves every device and the differences live in data rather than in duplicated YAML:

# templates/access.j2
{% for srv in ntp_servers %}
ntp server {{ srv }}
{% endfor %}
{% for vlan in vlan_list %}
vlan {{ vlan.id }}
 name {{ vlan.name }}
{% endfor %}

See the dedicated Ansible Jinja2 templates for network device configs guide for loops, conditionals and filters in depth.

Timeouts, Retries and Persistent Connections

Network devices are slower to respond than Linux hosts, and a command that takes ten seconds on a switch can exceed a default timeout designed for servers. Tune it deliberately rather than raising it everywhere:

ansible_command_timeout: 60
ansible_connect_timeout: 30
ansible_persistent_command_timeout: 60
ansible_persistent_connect_timeout: 30

For transient conditions — a device mid-reboot, a management interface that flaps — wrap the task in retries and until so it fails only after a real attempt window:

- name: Wait for the device to come back
  cisco.ios.ios_command:
    commands: show version
  register: result
  retries: 10
  delay: 15
  until: result is not failed

Two other settings save a lot of pain. Set ansible_become_method: enable for platforms that need an enable prompt, and reduce the number of simultaneous forks when the control node or the AAA server struggles to service many sessions at once (-f 10). Persistent connection files left in ~/.ansible/pc can go stale after a control-node crash; deleting them clears the "unable to connect to socket" class of errors.

Production Example

---
- name: Production ansible for network automation
  hosts: all
  become: true
  vars:
    app_name: myapp
    environment: production
  tasks:
    - name: Validate prerequisites
      ansible.builtin.assert:
        that:
          - app_name is defined
          - environment in ['dev', 'staging', 'production']
    - name: Execute ansible for network automation
      ansible.builtin.debug:
        msg: "Running in {{ environment }} for {{ app_name }}"
      register: result
    - name: Verify success
      ansible.builtin.assert:
        that: result is not failed
        fail_msg: "Ansible for Network Automation failed"

The pattern worth copying from this example is the guard clauses at the top. Validating that required variables are defined, and that a string variable is one of a known set of values, turns a subtle misconfiguration three tasks later into an immediate, obvious failure at the start of the run. Do the same for anything destructive: assert that the device is in the expected group, that the current firmware is the supported version, and that a backup exists before you change a route or a VLAN.

Backing Up and Restoring Configurations

Before automating change, automate the safety net. Gathering facts, backing up the running configuration and saving it with a timestamp gives you a rollback path that costs one task per device and has saved more outages than any clever playbook:

- name: Back up running configuration
  cisco.ios.ios_config:
    backup: yes
    backup_options:
      dir_path: backups/{{ inventory_hostname }}
      filename: "{{ ansible_date_time.date }}_running.cfg"

Store the backups outside the playbook directory and version or archive them, because the failure mode you are protecting against is exactly the one where the playbook that broke the device is the playbook you are looking at. The dedicated guide to Ansible network modules: ios_config, facts and config backup walks through the full workflow including restore.

Secrets and Credential Handling

Never put a plaintext password in an inventory file that lives in git. Use Ansible Vault to encrypt the secret and reference it as a variable, or delegate to a secrets manager and inject the value at run time:

ansible-vault create group_vars/switches/vault.yml
ansible-playbook -i inventory.ini site.yml --ask-vault-pass
ansible-playbook -i inventory.ini site.yml --vault-password-file ~/.vaultpass

Better still, give the automation platform its own device account with a scoped role rather than reusing a personal admin login, and rotate the credential on a schedule. When a playbook runs unattended from CI, a short-lived credential issued by a secrets manager is the least-bad way to keep secrets out of the repository and out of shell history.

Testing and Continuous Integration

A playbook that has never run in a lab is a hypothesis. Run syntax and lint checks in CI on every commit, and, where you can, run the playbook against virtual devices first:

ansible-playbook -i inventory.ini site.yml --syntax-check
ansible-lint site.yml
molecule test        # where a containerlab or virtual-topology driver is configured

The minimum viable pipeline is three stages: syntax check and lint, a --check --diff run against production, and a real run against a lab or a single canary device. Anything less leaves the blast radius unbounded, and anything more is only worth building once the first three are reliable.

Best Practices

  1. 上线前先跑 ansible-playbook --check --diff;
  2. 用变量参数化,跨环境复用;
  3. block/rescue/always 优雅处理失败;
  4. 保证幂等性——重复执行结果一致;
  5. 注释说明"为什么"而不是"是什么";
  6. 用 serial 分批下发,把一次改动的爆炸半径限制在一小批设备内;
  7. 改动前先备份 running-config,把回滚路径当成默认能力;
  8. 凭据交给 Vault 或密钥管理服务,绝不写进仓库。

Common Problems Cheat Sheet

问题 原因 解决
任务失败 前置依赖缺失 检查依赖
不幂等 未做状态检查 加条件判断
权限拒绝 权限不足 become: true
超时 网络或资源问题 调大 timeout
Module not found / JSON parse error 连接被当成 Linux 主机 设置 ansible_connection=network_cli 与 ansible_network_os
Unable to connect to socket 持久连接文件陈旧 删除 ~/.ansible/pc 后重试
命令输出被分页截断 设备分页未关闭 模块会自动处理,或调整终端长度设置
同一命令每次都显示 changed 使用了 *_command 模块 改用 *_config 模块并设置 match

相关阅读

网工向完整教程见本站 Ansible for Network Engineers: Complete Guide;101 入门见 Ansible Network Automation 101;标准+定制配置实战见 Cisco IOS Ansible Playbook: Standard + Unique Configs;模板化配置见 Ansible Jinja2 Templates for Network Device Configs;配置备份与 facts 见 Ansible Network Modules: ios_config, Facts and Config Backup。

原文链接:https://www.ansiblebyexample.com/articles/ansible-ansible-for-network-automation-complete-guide