DEV Community

Oleksandr Kuryzhev
Oleksandr Kuryzhev

Posted on Originally published at kuryzhev.cloud

Cisco Config Backups with Ansible: ios_config vs ios_command

Originally published on kuryzhev.cloud


Cisco config backups usually start the same way: someone replaces a failed access switch, opens a ticket for the last known-good configuration, and finds a TFTP folder that stopped updating a year ago. Ansible can fix that in an afternoon, but the first design decision is how the configuration gets from the device to disk. There are two common approaches, and they behave differently once you add Git, compliance reviews, or a few hundred switches.

When Cisco config backups need a deliberate choice

For a dozen switches and a nightly job, either approach works and the choice barely matters. It starts to matter when you want meaningful diffs, when the backup files contain secrets, or when auditors ask who changed what and when.

Three questions decide it:

  • What do you do with the file afterward? Archiving timestamped copies is a different job from feeding a Git repository that alerts on drift.
  • How noisy is the raw output? IOS and IOS XE prepend lines such as the current-configuration byte count and the last-change and NVRAM-update timestamps. These change whenever the configuration is edited or saved. Some platforms also include lines such as ntp clock-period that drift on their own, so diffs can show changes nobody made intentionally.
  • Who maintains the playbook? A single module call is easy to hand over. A task chain with text processing needs tests and an owner.

Both options below use the cisco.ios collection over ansible.netcommon.network_cli. Install it with ansible-galaxy collection install cisco.ios and check the collection's documented minimum ansible-core version, since it changes between releases. This shared inventory setup works for both:

# group_vars/cisco_switches.yml
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios
ansible_user: "{{ vault_net_user }}"        # keep credentials in Ansible Vault
ansible_password: "{{ vault_net_pass }}"
ansible_become: true
ansible_become_method: enable               # privilege level 15 via enable
ansible_become_password: "{{ vault_enable_pass }}"

Option A: ios_config with backup: true

The cisco.ios.ios_config module has a documented backup parameter that fetches the running configuration and writes it to the controller. With no lines or src given, it changes nothing on the device. That is the whole playbook:

# backup_native.yml
- name: Back up Cisco IOS running configs
  hosts: cisco_switches
  gather_facts: false            # no Python on the switch, so skip fact gathering
  tasks:
    - name: Save running-config via the module
      cisco.ios.ios_config:
        backup: true
        backup_options:
          dir_path: "/srv/net-backups/{{ inventory_hostname }}"
          filename: "running-config.cfg"   # fixed name suits Git; omit for timestamped files

Pros

  • Minimal code, maintained with the collection, and the intent is obvious to anyone reading it.
  • Without backup_options, it writes timestamped files to a backup/ directory next to the playbook, a reasonable archive with no extra work.
  • Low risk of a bad regex or templating error corrupting a backup.

Cons

  • You get the configuration as the module returns it. Volatile header lines may still show up in diffs, so verify the actual output on your platform and release.
  • It covers the running configuration. Startup-config, VLAN databases, or other show output need separate tasks.
  • Watch out for the file contents. Backups can include hashed enable secrets, type 7 passwords, and SNMP community strings. Restrict directory permissions and treat the repository as sensitive.

Option B: ios_command plus your own file handling

Here you run show running-config through cisco.ios.ios_command and write the result yourself from the controller. It is more work, but you control exactly what reaches disk. For example, you can strip lines that change without an intentional edit so Git only records real changes. Unlike the module's backup option, copy does not create missing parent directories, so the playbook creates the target directory first:

# backup_custom.yml
- name: Back up and normalize Cisco configs
  hosts: cisco_switches
  gather_facts: false
  tasks:
    - name: Ensure backup directory exists on the controller
      ansible.builtin.file:
        path: /srv/net-backups
        state: directory
        mode: "0700"
      delegate_to: localhost
      run_once: true

    - name: Collect running-config
      cisco.ios.ios_command:
        commands: show running-config
      register: run_cfg

    - name: Write normalized file on the controller
      ansible.builtin.copy:
        dest: "/srv/net-backups/{{ inventory_hostname }}.cfg"
        mode: "0600"                         # configs contain secrets
        content: |
          {{ run_cfg.stdout[0] | regex_replace('(?m)^(Building configuration|Current configuration|!\s*Last configuration change|!\s*NVRAM config last updated|ntp clock-period).*$\n?', '') }}
      delegate_to: localhost                 # otherwise copy targets the switch

Stripping ntp clock-period is a judgment call. It is a real configuration line, but the device adjusts it on its own, so keeping it mostly adds noise to the diff.

Pros

  • Full control over normalization, file mode, naming, and which commands run. Adding show startup-config or show version means one more entry in commands plus a write task that reads the matching stdout index.
  • Clean, stable output makes Git history and drift alerts far more useful.
  • The same pattern extends to other platforms using ansible.netcommon.cli_command or their own collections.

Cons

  • You own the regex. Header lines differ across IOS, IOS XE, and release trains, so verify against real output before trusting it.
  • Watch out for delegate_to: localhost. Omit it and copy runs against the device over the network connection, where it may fail or behave unpredictably.
  • Over-aggressive normalization can hide a real change. Keep the pattern narrow and review it when platforms change.

Decision matrix

Match your situation to the row that hurts most. Neither option is wrong; they trade simplicity for control.

Situation Option A: ios_config backup Option B: ios_command + copy
Small estate, archive only Best fit Overkill
Git history with drift alerts Works if output is already stable Better: normalize before commit
Need startup-config or extra show output Needs extra tasks anyway Natural fit
Mixed-skill team Easier to hand over Needs an owner and tests
Strict file permissions and naming rules Limited to module options Full control
Multi-vendor estate Similar backup options exist in other vendors' *_config modules, configured per platform One pattern reusable across platforms

A quick checklist to run before you commit to either design:

# pre-flight checklist
[ ] Backup target directory is outside the playbook repo, or encrypted
[ ] Files are mode 0600 and owned by the automation user
[ ] One sample switch per platform family diffed across two runs with no changes
[ ] Volatile header lines confirmed absent (or stripped) in the second run
[ ] Credentials come from Ansible Vault or an external secrets backend
[ ] Job scheduled (AWX/AAP, cron, or CI schedule) with failure notifications

Evidence-based recommendation

Start with Option A. It is documented behavior, a few lines long, and it removes the real problem, which is that nobody is backing up at all. Run it on a schedule, store the output somewhere access-controlled, and confirm you can actually read a restored file.

Move to Option B when a specific problem forces it. The usual triggers are noisy diffs in Git, a requirement to capture more than the running configuration, or a file-handling rule the module cannot express. Do not start there because it feels more flexible. Every regex you write is a maintenance commitment, and a typical failure is a normalization pattern that silently stops matching after an OS upgrade.

A hybrid is also legitimate. Use Option A for the nightly archive and add a separate Option B job that produces normalized files for the Git repository. Keeping the raw archive means that if the normalizer ever hides something, the original is still on disk.

Whichever you pick, check the parameter list for your installed collection version in the official cisco.ios.ios_config documentation, since defaults and backup_options behavior can change between releases. For more automation patterns, browse the rest of kuryzhev.cloud.

Related

Top comments (0)