Automation & Lab-as-Code

Updated

October 31, 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: DIY vs. Netlab

The DIY Approach (Jinja2 + custom scripts)

# 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 }

You can render frr.conf.j2 → config/r1/frr.conf with a custom Python/Jinja script or Ansible template module. Generated files can be committed for diff visibility or generated in CI.

The Netlab Declarative Approach

Writing custom Python Jinja compilers and manual IPAM spreadsheets becomes difficult to maintain when topologies exceed 3–4 nodes. Netlab provides a turnkey, declarative engine designed specifically for network labs:

# topology.yml
---
provider: clab
defaults.device: frr
module: [ ospf, bfd ]

nodes:
  r1:
  r2:
  r3:

links:
  - r1:
    r2:
  - r2:
    r3:
  - r3:
    r1:

validate:
  ospf_full:
    description: "Verify all OSPF adjacencies reach Full"
    nodes: [ r1, r2, r3 ]
    plugin: ospf_neighbor(count=2, state='Full')

Running netlab up calculates IPAM, renders Jinja2 templates for each device, stands up the Containerlab environment, and configures the routers. Running netlab validate executes the validation suite automatically.

Verification: verify.sh vs. netlab validate

Custom Shell Script (verify.sh)

#!/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)"

Netlab Automated Validation

With Netlab, assertions are declared directly in YAML and executed with exit codes suitable for CI:

# Returns exit code 0 on pass, non-zero on failure
netlab validate

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.