Automation & Lab-as-Code
Automation & Lab-as-Code
A lab you cannot rebuild is a rumor. This chapter automates the Containerlab lifecycle, config push patterns, and smoke verification so your networking skill compounds in git.
Learning goals
By the end of this chapter you can:
- Wrap deploy/destroy/verify in repeatable scripts
- Organize inventories for multi-node labs
- Push or render configs with light Ansible or shell
- Sketch CI that smoke-tests a topology
- Treat secrets and image pins correctly
Principles
- Topology and config in git
- Idempotent as practical — re-run without fear
- Verify automatically — exit codes matter
- No click-ops source of truth
- Small steps — shell first, Ansible when it pays off
Directory layout (automation-ready)
labs/ospf-tri/
topology.clab.yml
addressing.md
README.md
Makefile
verify.sh
up.sh
down.sh
config/
r1/frr.conf
r1/daemons
r2/...
ansible/
inventory.yml
playbooks/deploy-config.yml
Makefile pattern
TOPO := topology.clab.yml
.PHONY: up down verify smoke
up:
sudo containerlab deploy -t $(TOPO)
down:
sudo containerlab destroy -t $(TOPO) --cleanup
verify:
./verify.sh
smoke: up verify
@echo smoke okmake smoke
make downup.sh / down.sh
#!/usr/bin/env bash
# up.sh
set -euo pipefail
cd "$(dirname "$0")"
sudo containerlab deploy -t topology.clab.yml
./verify.sh#!/usr/bin/env bash
# down.sh
set -euo pipefail
cd "$(dirname "$0")"
sudo containerlab destroy -t topology.clab.yml --cleanupConfig strategies
| Strategy | Pros | Cons |
|---|---|---|
Bind-mount /etc/frr |
Simple, git-native | Image load quirks |
containerlab startup-config |
Kind-supported | Kind-specific |
| Ansible after deploy | Flexible templating | Extra tool |
| Render templates → bind | Pure git artifacts | Build step |
Prefer bind + pure files for FRR until pain appears.
Safe interactive explore → promote
docker exec -it clab-ospf-tri-r1 vtysh
# make change, show run
# copy running config back into config/r1/frr.conf
# redeploy clean to proveLight Ansible (optional)
ansible/inventory.yml:
all:
children:
routers:
hosts:
r1:
ansible_host: clab-ospf-tri-r1
ansible_connection: docker
r2:
ansible_host: clab-ospf-tri-r2
ansible_connection: docker
r3:
ansible_host: clab-ospf-tri-r3
ansible_connection: dockerPlaybook sketch:
- name: Push FRR config files
hosts: routers
gather_facts: false
tasks:
- name: Copy frr.conf
copy:
src: "{{ playbook_dir }}/../../config/{{ inventory_hostname }}/frr.conf"
dest: /etc/frr/frr.conf
- name: Reload FRR
shell: vtysh -c 'write' || true
# better: proper frr reload unit — image dependentansible-playbook -i ansible/inventory.yml ansible/playbooks/deploy-config.ymlPython with scrapli/netmiko is equally valid; pick one tool family and go deep later.
Templating addressing
addressing.yml:
links:
r1_r2: { r1: 10.0.12.1/24, r2: 10.0.12.2/24 }
lans:
h1: { gw: 192.168.1.1/24, host: 192.168.1.10/24 }Render frr.conf.j2 → config/r1/frr.conf with a small Python/Jinja script or Ansible template module. Generated files can be committed for diff visibility or generated in CI—pick one workflow and document it.
verify.sh mature pattern
#!/usr/bin/env bash
set -euo pipefail
fail() { echo "FAIL: $*" >&2; exit 1; }
need_full() {
local node=$1
docker exec "$node" vtysh -c 'show ip ospf neighbor' | grep -q Full \
|| fail "$node missing Full neighbor"
}
need_full clab-ospf-tri-r1
need_full clab-ospf-tri-r2
need_full clab-ospf-tri-r3
docker exec clab-ospf-tri-h1 ping -c 2 -W 1 192.168.2.10 || fail "h1->h2"
docker exec clab-ospf-tri-h2 ping -c 2 -W 1 192.168.1.10 || fail "h2->h1"
echo "OK $(date -Is)"CI smoke (GitHub Actions sketch)
Conceptual job (runner must support Docker + Containerlab—often self-hosted):
# conceptual — adapt to your runners
jobs:
lab-smoke:
runs-on: self-hosted-linux
steps:
- uses: actions/checkout@v4
- name: deploy+verify
working-directory: books/Networking/labs/ospf-tri
run: |
sudo containerlab deploy -t topology.clab.yml
./verify.sh
- name: cleanup
if: always()
working-directory: books/Networking/labs/ospf-tri
run: sudo containerlab destroy -t topology.clab.yml --cleanupThis book’s monorepo CI renders Quarto; lab smoke is optional local/self-hosted. Do not assume GitHub-hosted runners allow nested Containerlab without setup.
Image pins in automation
# .env or Makefile
FRR_IMAGE=quay.io/frrouting/frr:10.2.1
ALPINE_IMAGE=alpine:3.20Topology can be generated or documented to match. Record digests for serious CI.
Idempotency drills
make smoketwice withoutdown— should not corrupt
make down && make smoke— clean path
- Break config,
make smokefails exit ≠ 0
- Fix config, smoke passes
Secret hygiene
- No production passwords in lab repos
- Lab default creds documented as lab-only
- Use
.gitignorefor private env files
Graph and inventory export
sudo containerlab inspect -t topology.clab.yml --format json > inspect.json
sudo containerlab graph -t topology.clab.yml || trueFeed inspect outputs into your own tools later.
Predict → observe → fix
Predict: A peer clones the lab folder, runs make smoke, gets OK without chatting you.
Observe: Remove tribal knowledge; only README + scripts.
Fix: Missing apk packages, wrong container names, unpinned images.
Harden: Add make smoke badge or CI on self-hosted when ready.
Anti-patterns
| Anti-pattern | Replace with |
|---|---|
| Only screenshot proof | verify.sh |
| Snowflake node edits | bind + git |
latest tags |
pins |
| Huge mono playbook day one | Makefile smoke |
| CI without cleanup | always destroy |
Summary
- Lab-as-code = topology + config + verify in git
- Start with Makefile/scripts; add Ansible when templating hurts
- Exit codes turn labs into testable systems
- CI is optional but shaping; cleanup always
- Promote working state from nodes back into the repo
Next: capstone outline—projects that prove the whole journey.