Ansible for Network Engineers
Capstone: A Complete Ansible-Based Configuration Deployment
What you'll learn
- How inventory, variables, the template module, loops, and check mode combine into one deployment
- How this solves the exact same problem as Configuration & Template Automation's capstone
- Why Ansible's per-host recap is coarser than the 5-state model built by hand in Reliable Automation
- That Ansible is an orchestration layer over everything already learned, not a replacement for it
Listen to a guided explanation of this lesson.
Spoken narration isn't supported in this browser.
The whole curriculum, one more time
- Phase 2.6 — Python automationhand-written inventory, connection, validation, report
- Phase 2.7 — Netmikoa real SSH connection replaces the simulation
- Phase 2.8 — Reliable Automationretry, dry-run, pre/post-check, structured results — all hand-written
- Phase 2.9 — Jinja2 templatesstructured data + a template replace a hardcoded config string
- Phase 3.0 — Ansible orchestrates itthe same inventory, the same template, now run declaratively
This capstone solves the exact same problem Configuration & Template Automation's capstone did: deploy required VLANs across the SW1–SW5 fleet, safely. Same devices, same required VLANs, same template file. What's different is what orchestrates it.
Inventory and variables
switches:
hosts:
SW1:
ansible_host: 10.10.10.1
SW2:
ansible_host: 10.10.10.2
SW3:
ansible_host: 10.10.10.3
SW4:
ansible_host: 10.10.10.4
SW5:
ansible_host: 10.10.10.5
vars:
ansible_network_os: ios
ansible_connection: network_cliUnchanged from Lesson 3 — the same 5-device fleet, translated from devices.csv.
vlans_by_host:
SW1:
- { id: 10, name: USERS }
- { id: 20, name: VOICE }
SW2:
- { id: 10, name: USERS }
SW3:
- { id: 10, name: USERS }
- { id: 30, name: MGMT }
SW4:
- { id: 10, name: USERS }
SW5:
- { id: 10, name: USERS }The exact REQUIRED_VLANS data from Configuration & Template Automation's capstone, unchanged — SW3 needs two VLANs, the rest need one each. Only the file format changed, from a Python dict to a YAML variables file.
The complete playbook
- name: Deploy required VLAN configuration
hosts: switches
gather_facts: false
vars:
required_vlans: "{{ vlans_by_host[inventory_hostname] }}"
tasks:
- name: Render this device's VLAN configuration
ansible.builtin.template:
src: templates/vlan_config.j2
dest: "/tmp/{{ inventory_hostname }}_vlans.cfg"
delegate_to: localhost
- name: Apply required VLANs
cisco.ios.ios_config:
lines:
- "vlan {{ item.id }}"
- "name {{ item.name }}"
parents: "vlan {{ item.id }}"
loop: "{{ required_vlans }}"inventory_hostname is Ansible's built-in variable for 'whichever host this task is currently running for' — it's what makes vlans_by_host[inventory_hostname] pick out each device's own VLANs automatically, once per host in the switches group. delegate_to: localhost means the template renders on your machine, not on the switch itself — switches don't have a filesystem for Jinja2 to write to the way a Linux control node does.
Preview before anything real happens
$ ansible-playbook -i inventory.yml deploy_vlans.yml --check --diff
TASK [Apply required VLANs] **********************************************
--- before
+++ after
@@ -1,3 +1,5 @@
!
+vlan 20
+ name VOICE
changed: [SW2] => (item={'id': 20, 'name': 'VOICE'})
ok: [SW1] => (item={'id': 10, 'name': 'USERS'})
ok: [SW1] => (item={'id': 20, 'name': 'VOICE'})
...
PLAY RECAP ******************************************************************
SW1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0
SW2 : ok=1 changed=1 unreachable=0 failed=0 skipped=0
SW3 : ok=1 changed=1 unreachable=0 failed=0 skipped=0
SW4 : ok=1 changed=0 unreachable=0 failed=0 skipped=0
SW5 : ok=1 changed=0 unreachable=0 failed=0 skipped=0This describes Ansible's own documented check-mode/diff output for this exact data — SW1 already has both required VLANs (changed=0), SW2 and SW3 each need one new VLAN (changed=1), matching Configuration & Template Automation's own already-compliant / needs-change split for the same devices under DRY_RUN=True.
Compare this to Phase 2.9's own report, device by device
Configuration & Template Automation's template_deployment_report.json, under DRY_RUN=True, classified SW1 as already-compliant and SW2/SW3 as dry-run-preview. This playbook's --check recap shows the same underlying facts — SW1 changed=0, SW2 and SW3 changed=1 — expressed in Ansible's own vocabulary instead of that hand-built 5-state model.
What Ansible's recap doesn't tell you that Reliable Automation's report did
- already-compliant / dry-run-preview / changed / change-not-verified / unreachable / auth-failed
- Attempt counts and retry flags, per device
- You wrote every state's exact meaning
- ok / changed / unreachable / failed / skipped, per host
- No built-in attempt-count or retry detail
- The framework defines what each bucket means
A real trade-off, not a strict upgrade
Ansible's recap is coarser than the 5-state model Reliable Automation built by hand. You gain a framework that handles idempotency and check-mode for you — you give up the fine-grained, custom result detail (retry counts, a distinct change-not-verified state) that hand-written Python gave you complete control over. Neither is simply "better" — this is the real trade-off between imperative and declarative automation this module opened with in Lesson 1.
Applying it for real — Option A only
Only against a disposable lab device you fully control
Removing --check and running this playbook for real requires the cisco.ios collection installed and a real or virtual Cisco device reachable over SSH — never production, and never without reviewing the --check --diff output first. This was not executed against a real device in this authoring environment; it is not claimed as tested, verified, or confirmed.
What this module actually built
Nothing here replaced Python, Netmiko, Reliable Automation's safety discipline, or Configuration & Template Automation's Jinja2 skill. This capstone reused the same fleet, the same required-VLANs data, and the exact same template file — and wrapped them in a framework that provides idempotency and check-mode as guarantees instead of hand-written functions. That's the whole point of Lesson 1's declarative-vs-imperative framing, now proven end to end.
Key takeaways
- This capstone solves the exact same VLAN deployment problem as Configuration & Template Automation's capstone — same fleet, same required-VLANs data, same vlan_config.j2 file.
- inventory_hostname + vlans_by_host[inventory_hostname] is how one playbook picks out each device's own data automatically, the same role a Python loop over devices played by hand.
- This lesson's --check/--diff output describes Ansible's own documented behavior for this exact data; it was not independently executed against ansible-playbook in this authoring environment, and is never claimed as tested or verified.
- Ansible's PLAY RECAP is genuinely coarser than the 5-state model Reliable Automation built by hand — a real trade-off between a framework's guarantees and hand-written custom control, not a strict upgrade.
- Applying this for real (Option A) requires the cisco.ios collection and a real or virtual device, and was not performed in this authoring environment — never claimed as live-tested.