Runnable network-automation playbooks: ad-hoc facts, config push with backup/check/diff, Jinja2 templating, declarative resource modules, safe rolling changes (serial), dynamic inventory from NetBox, and a CI/CD pipeline.
Theory sticks once you've seen the playbooks. Here are the network-automation cases that come up most — each with a runnable snippet and the point an interviewer is checking. Companion to Ansible — Theory.
Network hosts need a connection plugin and ansible_network_os. Put shared creds in
group_vars (vault the password).
# inventory.yml
all:
children:
ios:
hosts:
r1: { ansible_host: 10.0.0.1 }
r2: { ansible_host: 10.0.0.2 }
vars:
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.ios.ios
ansible_user: admin
ansible_password: "{{ vault_ios_password }}" # from ansible-vault
ansible ios -m cisco.ios.ios_command -a "commands='show version'"
# gather structured facts:
ansible ios -m cisco.ios.ios_facts -a "gather_subset=hardware"
Point: ad-hoc is for one-off reads/checks; no playbook needed.
- name: NTP + logging, safely
hosts: ios
gather_facts: false
tasks:
- name: Push baseline config
cisco.ios.ios_config:
backup: true # snapshot running-config first
lines:
- ntp server 10.0.0.53
- logging host 10.0.0.60
register: result
- name: Save if changed
cisco.ios.ios_config:
save_when: changed
when: result.changed
Run --check --diff first to preview. Point: idempotency + a backup + save-on-change is the
safe pattern.
# group_vars/ios.yml
vlans:
- { id: 10, name: users }
- { id: 20, name: voice }
# templates/vlans.j2
{% for v in vlans %}
vlan {{ v.id }}
name {{ v.name }}
{% endfor %}
- name: Render + apply VLANs
cisco.ios.ios_config:
src: templates/vlans.j2
Point: data (SoT) in vars, structure in the template — one template, many devices.
- name: VLANs as data, fully declarative
cisco.ios.ios_vlans:
config:
- { vlan_id: 10, name: users }
- { vlan_id: 20, name: voice }
state: overridden # make the device match exactly (prunes extras)
Point: resource modules beat raw ios_config lines — merged vs
replaced vs overridden vs deleted give true desired-state config.
- name: Rolling change, 1 device at a time
hosts: ios
serial: 1 # batch size — blast-radius control
gather_facts: false
tasks:
- name: Pre-check reachability
cisco.ios.ios_ping: { dest: 10.0.0.53 }
- name: Apply change
cisco.ios.ios_config:
lines: [ "ip name-server 10.0.0.53" ]
notify: save config
- name: Post-check (fail the batch if broken)
cisco.ios.ios_command:
commands: show ip name-server
register: out
failed_when: "'10.0.0.53' not in out.stdout[0]"
handlers:
- name: save config
cisco.ios.ios_config: { save_when: changed }
Point: serial + pre/post checks stop a bad change from hitting the whole fleet.
# netbox_inv.yml (ansible-inventory -i netbox_inv.yml --graph)
plugin: netbox.netbox.nb_inventory
api_endpoint: https://netbox.example.com
token: "{{ lookup('env','NETBOX_TOKEN') }}"
group_by: [device_roles, sites]
Point: the inventory becomes a live query against your source of truth — no hand-maintained host lists. (Build your own in Python: Ansible Lab.)
Treat config as code: lint + dry-run on a PR, deploy on merge.
# .gitlab-ci.yml (or GitHub Actions equiv)
stages: [lint, check, deploy]
lint:
stage: lint
script:
- ansible-lint site.yml
- yamllint .
check: # runs on merge requests
stage: check
script:
- ansible-playbook site.yml -i inventory.yml --check --diff
rules: [ { if: '$CI_PIPELINE_SOURCE == "merge_request_event"' } ]
deploy: # runs only on main after merge
stage: deploy
script:
- ansible-playbook site.yml -i inventory.yml
rules: [ { if: '$CI_COMMIT_BRANCH == "main"' } ]
Point: the interview answer for "how do you make network changes safe & reviewable" — Git PR
review, automated lint, --check --diff as a required gate, deploy only from main, secrets
from the CI vault. Pair with molecule to test roles in containers.
serial, pre/post checks, --check gate, backup)--check --diff + molecule in CI)state: overridden do that merged doesn't? (prunes config not in your data)