Automation & Lab-as-Code

Updated

September 4, 2026

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

  1. Topology and config in git
  2. Idempotent as practical — re-run without fear
  3. Verify automatically — exit codes matter
  4. No click-ops source of truth
  5. 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 ok
make smoke
make down

up.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 --cleanup

Config 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 prove

Light 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: docker

Playbook 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 dependent
ansible-playbook -i ansible/inventory.yml ansible/playbooks/deploy-config.yml

Python 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.j2config/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 --cleanup

This 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.20

Topology can be generated or documented to match. Record digests for serious CI.

Idempotency drills

  1. make smoke twice without down — should not corrupt
  2. make down && make smoke — clean path
  3. Break config, make smoke fails exit ≠ 0
  4. Fix config, smoke passes

Secret hygiene

  • No production passwords in lab repos
  • Lab default creds documented as lab-only
  • Use .gitignore for 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 || true

Feed 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.