Theoretical foundations and architecture

Configuration automation for an operator's fleet is done with Ansible, an agentless configuration management system. Network modules run on the controller and send CLI commands or RPCs to the device, so the connection is selected with a connection plugin (ansible.netcommon.network_cli, netconf or httpapi). The main components: 1. Inventory: a YAML or INI file with device groups (pe_routers, p_routers, rr_servers), their addresses, the platform (ansible_network_os: cisco.ios.ios, junipernetworks.junos.junos, community.network.ce for Huawei CloudEngine, community.routeros.routeros) and group (group_vars) and host (host_vars) variables. 2. Modules (cisco.ios.ios_config, junipernetworks.junos.junos_config and others): idempotence of *_config modules is implemented by comparing configuration text: if the required lines are already present, the module returns ok. Lines must be written in normalized form, otherwise the task is forever changed. 3. Playbooks: scenarios for fact gathering, compliance checks and rolling out VRFs and interfaces with --check --diff, backup: true and staged serial. A playbook reduces manual errors, but it does not replace data validation before applying and a confirmed commit with rollback.

What Ansible gives a carrier network and what it does not

Ansible is agentless: the controller connects to the device over SSH (or NETCONF, or an HTTP API) and runs a module. For networking, modules do not run on the device: Python executes on the controller, and CLI commands or RPCs go to the hardware. That is why the connection is set not as ssh but as a connection plugin: ansible.netcommon.network_cli (CLI over SSH), ansible.netcommon.netconf (NETCONF) or ansible.netcommon.httpapi. The platform is chosen with the ansible_network_os variable.

PlatformCollection and modulesansible_network_os
Cisco IOS-XEcisco.ios: ios_config, ios_vrf, ios_commandcisco.ios.ios
Huawei CloudEngine/VRPcommunity.network: ce_config, ce_commandcommunity.network.ce
Juniper Junosjunipernetworks.junos: junos_config, junos_commandjunipernetworks.junos.junos
MikroTik RouterOScommunity.routeros: command, api, api_modifycommunity.routeros.routeros

Collection versions are pinned in requirements.yml, otherwise a controller upgrade changes module behavior. There is no official vendor collection for Huawei routers; a working alternative is ansible.netcommon.netconf_config with an XML payload based on the device YANG model.

Inventory, group_vars, host_vars

The inventory describes which devices exist and how to connect to them. The YAML format allows nested groups: pe_routers contains pe_cisco, pe_juniper and so on. Variables are taken by precedence: group_vars/all is lower than group_vars/<group>, and host_vars/<host> is higher than the group. A design rule: the inventory holds only what is needed for connection and group membership, while service data (VRF, RD, RT) lives in group_vars and host_vars or in an external source of truth. For large networks the inventory is built dynamically: netbox.netbox.nb_inventory reads devices and tags from NetBox and the manual list disappears.

yaml
# inventory/prod/hosts.yml
all:
  children:
    pe_routers:
      children:
        pe_cisco:
          hosts: {pe1: {ansible_host: 10.255.0.1, pe_id: 1}}
        pe_juniper:
          hosts: {pe2: {ansible_host: 10.255.0.2, pe_id: 2}}
# group_vars/all.yml
as_number: 65000
allowed_rt: {A: ["65000:100"], B: ["65000:200"]}

The command ansible-inventory -i inventory/prod --graph shows the resulting group tree, and --host pe1 shows the final host variables after merging. This is the first check for any surprise.

Idempotency: how it really works

Idempotency means that a repeated run produces no changes. For the *_config modules it is implemented by text comparison: Ansible loads the running-config, matches the lines under the required parents, and sends only the missing ones. This leads to typical problems:

  • A line in the playbook must match how the device stores the command. On IOS-XE route-target both 65000:100 becomes two lines, import and export, in the running-config, so such a task will always be changed. Write lines in their normalized form.
  • Commands with default values that the device does not display also produce a permanent changed.
  • ios_config defaults to match: line; use match: exact or strict for lines where order matters, and replace: block to replace a whole block.
  • RouterOS /ip/vrf/add is not idempotent: adding again returns an error, so you need either a check before adding or the community.routeros.api_modify module.

Check mode and diff

ansible-playbook --check --diff runs the modules in a "what would change" mode. For ios_config, junos_config and ce_config the module builds a diff but does not apply it; on Junos the candidate is loaded with check_commit. Limitations: *_command modules do not run change commands in check mode, and dependent tasks may fail because earlier steps were not applied. So check mode in CI is a check of syntax and diff, not proof that things work: it is complemented by a lab.

Safe rollout

  • serial: 1 or serial: [1, "25%"] rolls out one PE at a time (canary), then in batches; max_fail_percentage: 0 and any_errors_fatal: true stop the rollout at the first error.
  • --limit pe1 restricts a run, --tags selects part of the tasks.
  • Confirmation with a timeout: on Junos confirm: 5 in junos_config (commit confirmed for 5 minutes), on IOS-XE configure replace ... time or an archive/rollback wrapper. After the service is verified, a separate task confirms the commit (confirm_commit: true).
  • A backup before the change: backup: true in *_config saves the running-config on the controller; it also serves as the rollback point.
  • forks (5 by default) limits parallelism; throttle: 1 on a task limits simultaneous access to a shared resource.