Lesson 7 of 7

CI/CD with Ansible

Objective

In this lesson we integrate Ansible into a CI/CD pipeline using Git-based change management and automated testing. You will create a Git workflow for network changes, author an Ansible playbook that updates device configuration, and wire that process into a CI server so every change is validated (syntax and dry-run) before it is applied to routers. This matters in production because automated testing and controlled deployment reduce human error and provide audit trails for network changes — essential for large-scale enterprise networks.

Topology

Below is the topology referenced earlier in the course (Lesson 1). This lesson adds a Git server and a CI server to enable the CI/CD pipeline.

ASCII topology (management network 10.0.0.0/24):

     [Ansible Control]
     ansible.lab.nhprep.com
     10.0.0.10/24
           |
           | VLAN 10 mgmt
           |
     [SW1] management VLAN 10
           |
  -----------------------------
  |                           |

[Core1] [Edge1] core1.lab.nhprep.com edge1.lab.nhprep.com Gi0/0 10.0.0.1/24 Gi0/0 10.0.0.2/24

Additional infrastructure:

  • Git server: git.lab.nhprep.com — 10.0.0.20/24 (new)
  • CI server: ci.lab.nhprep.com — 10.0.0.30/24 (new)

Device Table

DeviceHostname (FQDN)Management IP
Ansible Controlansible.lab.nhprep.com10.0.0.10
Git servergit.lab.nhprep.com10.0.0.20
CI serverci.lab.nhprep.com10.0.0.30
Core routercore1.lab.nhprep.com (R1)10.0.0.1
Edge routeredge1.lab.nhprep.com (R2)10.0.0.2

Quick Recap

We already have two routers (Core1 and Edge1) reachable on the management network 10.0.0.0/24. For CI/CD we have added:

  • Git server at 10.0.0.20
  • CI server at 10.0.0.30
  • Ansible control already at 10.0.0.10

All management connections use SSH. The workflow will be: developers push Ansible code to Git → CI server triggers tests → if tests pass, CI invokes Ansible on ansible.lab.nhprep.com to apply the change to Core1 and Edge1.

Key Concepts

  • Git-based change management (GitOps): Your network intent is declared as code (Ansible playbooks). Every change is versioned. In production, this provides auditability and rollback capability.
  • Idempotency and Ansible modules: Network modules (e.g., ios_config) are designed to be idempotent — applying the same playbook twice leaves devices in the same state. This reduces configuration drift.
  • Syntax and dry-run testing: ansible-playbook --syntax-check validates YAML and module usage without connecting to devices. ansible-playbook --check attempts a dry-run; note that not all network modules fully support check mode — always validate results.
  • SSH and network_cli: Ansible uses SSH to connect to IOS devices. When using connection: network_cli, Ansible starts an SSH session, enters enable mode as needed, then sends configure terminal and configuration commands.
  • CI pipeline stages: Typical stages are linting (ansible-lint), syntax check, dry-run, test on lab devices, and deploy. In production, gate promotion through approvals at certain stages is common.

Steps

Step 1: Initialize central Git repository on git.lab.nhprep.com

What we are doing: Create a central bare Git repository on the Git server to store Ansible playbooks and host CI hooks. This central repo is the single source of truth for network changes and enables CI triggers on push/merge.

! No router commands in this step; Git server and control host commands shown below

On the Git server (10.0.0.20):

sudo mkdir -p /srv/git/ansible-network.git
cd /srv/git/ansible-network.git
sudo git init --bare
sudo chown -R git:git /srv/git/ansible-network.git

On the Ansible control host (10.0.0.10) initialize and push:

mkdir -p ~/ansible-network
cd ~/ansible-network
git init
cat > inventory <<EOF
[routers]
core1 ansible_host=10.0.0.1 ansible_user=ansible ansible_password=Lab@123 ansible_network_os=ios ansible_connection=network_cli
edge1 ansible_host=10.0.0.2 ansible_user=ansible ansible_password=Lab@123 ansible_network_os=ios ansible_connection=network_cli
EOF
git add inventory
git commit -m "Initial inventory"
git remote add origin ssh://git@10.0.0.20:/srv/git/ansible-network.git
git push -u origin master

What just happened: The Git server contains a bare repository ready to accept pushes. The Ansible control host committed an initial inventory and pushed it to the central repo. This allows CI to clone the repository and run tests against the same version of code developers push.

Real-world note: In production, repositories are typically hosted with access controls and pull-request workflows; a bare repo like this is acceptable for lab or internal use.

Verify:

ssh git@10.0.0.20 "ls -la /srv/git/ansible-network.git"

Expected output:

total 28
drwxr-xr-x 7 git git 4096 Apr  2 12:00 .
drwxr-xr-x 3 root root 4096 Apr  2 11:59 ..
-rw-r--r-- 1 git git   23 Apr  2 12:00 HEAD
drwxr-xr-x 2 git git 4096 Apr  2 12:00 branches
-rw-r--r-- 1 git git  113 Apr  2 12:00 config
drwxr-xr-x 3 git git 4096 Apr  2 12:00 hooks
drwxr-xr-x 2 git git 4096 Apr  2 12:00 info
drwxr-xr-x 2 git git 4096 Apr  2 12:00 objects
drwxr-xr-x 2 git git 4096 Apr  2 12:00 refs

Step 2: Configure device accounts for Ansible on Core1 and Edge1

What we are doing: Create a local user, set the domain, and enable SSH so Ansible can authenticate to the routers. Without SSH and appropriate privilege, Ansible cannot push configuration.

! On Core1 (10.0.0.1)
configure terminal
ip domain-name lab.nhprep.com
username ansible privilege 15 secret Lab@123
crypto key generate rsa modulus 2048
line vty 0 4
 login local
 transport input ssh
end
write memory
! On Edge1 (10.0.0.2)
configure terminal
ip domain-name lab.nhprep.com
username ansible privilege 15 secret Lab@123
crypto key generate rsa modulus 2048
line vty 0 4
 login local
 transport input ssh
end
write memory

What just happened: Each router now has a local user ansible with secret Lab@123. ip domain-name is required for RSA key generation and for SSH to present a proper host key. line vty changes allow SSH logins using local authentication. These steps let Ansible open an SSH session and enter enable/configuration mode as needed.

Real-world note: In production you should use AAA with TACACS+/RADIUS and certificate-based device authentication; local users are acceptable only in small environments or labs.

Verify:

! On Core1
show running-config | section username

Expected output:

username ansible privilege 15 secret 5 $1$abcd$abcdefghijk1234567890
! On Core1
show ip ssh

Expected output:

SSH Enabled - version 2.0
Authentication timeout: 120 secs; Authentication retries: 3

(Repeat the same verification on Edge1, showing the same user and SSH enabled output.)


Step 3: Write an Ansible playbook that configures interface descriptions

What we are doing: Create a reusable playbook using the ios_config module to set interface descriptions. This is the change we will manage via Git and test via CI. Using modules keeps operations idempotent.

Playbook file: set-interface-desc.yml

- name: Set interface descriptions on routers
  hosts: routers
  gather_facts: no
  connection: network_cli
  tasks:
    - name: Configure loopback description (example)
      ios_config:
        lines:
          - description Configured by Ansible via CI
        parents: 
          - interface Loopback0

Inventory (already committed in Step 1) references the ansible_user and ansible_password.

Run a syntax check locally before pushing:

ansible-playbook --syntax-check -i inventory set-interface-desc.yml

What just happened: The playbook is validated for YAML syntax and module usage without connecting to devices. connection: network_cli tells Ansible to use an SSH session suitable for network devices. The ios_config module will translate the lines into configure terminal commands when executed.

Real-world note: Use consistent parent/child configuration blocks to limit risk (e.g., change only an interface section), reducing blast radius.

Verify (syntax):

ansible-playbook --syntax-check -i inventory set-interface-desc.yml

Expected output:

playbook: set-interface-desc.yml

Now run a dry-run:

ansible-playbook -i inventory set-interface-desc.yml --check --diff

Expected output (example):

PLAY [Set interface descriptions on routers] *********************************

TASK [Configure loopback description (example)] *******************************
changed: [core1]
changed: [edge1]

PLAY RECAP *******************************************************************
core1                      : ok=1    changed=1    unreachable=0    failed=0
edge1                      : ok=1    changed=1    unreachable=0    failed=0

Note: Some network modules may not implement full check-mode; output may show changed even in --check. Understand the module behavior for production.


Step 4: Commit changes and push to Git; configure CI to run tests

What we are doing: Demonstrate the Git branch and push workflow and provide a CI pipeline definition (Jenkinsfile) that performs linting, syntax checks, dry-run, and deployment. This enables automated gating of changes.

On Ansible control:

git checkout -b feature/interface-desc
git add set-interface-desc.yml
git commit -m "Add interface description playbook"
git push -u origin feature/interface-desc

Example Jenkinsfile (ci.lab.nhprep.com will clone repo and run):

pipeline {
  agent any
  stages {
    stage('Checkout') {
      steps {
        git url: 'ssh://git@10.0.0.20:/srv/git/ansible-network.git', branch: 'feature/interface-desc'
      }
    }
    stage('Lint') {
      steps {
        sh 'ansible-lint set-interface-desc.yml || true'
      }
    }
    stage('Syntax Check') {
      steps {
        sh 'ansible-playbook --syntax-check -i inventory set-interface-desc.yml'
      }
    }
    stage('Dry-run') {
      steps {
        sh 'ansible-playbook -i inventory set-interface-desc.yml --check --diff'
      }
    }
    stage('Deploy') {
      steps {
        input message: 'Approve deployment to devices?'
        sh 'ansible-playbook -i inventory set-interface-desc.yml'
      }
    }
  }
}

What just happened: The Git push creates a branch holding the change. The CI pipeline clones the branch, runs linting and syntax checks to catch mistakes early, then runs a dry-run test. If everything passes and manual approval is given, the pipeline runs the playbook to apply the change to devices.

Real-world note: Production pipelines often include automated tests against a lab topology first, then require human approval before production deployment.

Verify (CI run simulated via shell):

# Simulate CI executing the deploy stage
ansible-playbook -i inventory set-interface-desc.yml

Expected output (full playbook run):

PLAY [Set interface descriptions on routers] *******************************

TASK [Configure loopback description (example)] *****************************
changed: [core1]
changed: [edge1]

PLAY RECAP *****************************************************************
core1                      : ok=1    changed=1    unreachable=0    failed=0
edge1                      : ok=1    changed=1    unreachable=0    failed=0

Step 5: Verify the configuration on the routers

What we are doing: Confirm that the intended configuration was applied on Core1 and Edge1. This is the final verification step — always inspect device state after automation makes changes.

! On Core1
show running-config interface Loopback0

Expected output:

Building configuration...

Current configuration : 57 bytes
!
interface Loopback0
 description Configured by Ansible via CI
!
end
! On Edge1
show running-config interface Loopback0

Expected output:

Building configuration...

Current configuration : 57 bytes
!
interface Loopback0
 description Configured by Ansible via CI
!
end

What just happened: The routers show the Loopback0 interface description set to the string added by the playbook. This validates the end-to-end workflow from Git push → CI checks → Ansible deployment → device configuration.

Real-world note: Always include verification steps in the pipeline (e.g., run show via Ansible and assert expected output) to automatically confirm success.

Verification Checklist

  • Check 1: The Git repository exists on git.lab.nhprep.com — verify with ssh git@10.0.0.20 "ls -la /srv/git/ansible-network.git"
  • Check 2: Both routers accept SSH logins for user ansible — verify with ssh ansible@10.0.0.1 (or ansible -m ping against core1)
  • Check 3: Playbook passes syntax check — ansible-playbook --syntax-check -i inventory set-interface-desc.yml
  • Check 4: Dry-run executes without error — ansible-playbook -i inventory set-interface-desc.yml --check --diff
  • Check 5: Configuration appears on devices — show running-config interface Loopback0 on both routers and verify description

Common Mistakes

SymptomCauseFix
CI job fails with SSH authentication errorWrong username/password or SSH keys not configuredEnsure inventory has correct ansible_user/ansible_password or configure SSH keys; test ssh ansible@10.0.0.1 manually
ansible-playbook --syntax-check passes but playbook fails on devicesNetwork modules may require enabling enable mode or different connection settingsEnsure inventory has correct ansible_connection=network_cli and ansible_become=yes if needed; set ansible_password and ansible_become_password
Dry-run (--check) shows changes but actual run makes unexpected changesSome modules do not fully implement check modeRead module docs; include pre-deployment interface/state checks and post-deployment verification
YAML parsing error in CI logsIncorrect indentation or tabs in playbookRun ansible-playbook --syntax-check locally and fix indentation; use spaces only

Key Takeaways

  • Use Git as the single source of truth for Ansible playbooks; branches and PRs allow controlled review of network changes.
  • Always perform syntax checks and dry-run tests before applying changes; CI should automate these stages to catch errors early.
  • Ansible's network_cli opens SSH sessions to devices and issues configure terminal commands; ensure devices permit SSH and appropriate credentials are available.
  • Build verification into the pipeline: after deployment, automatically query devices to confirm expected state — this turns a pipeline from deploy-only into a true CI/CD loop for networks.

Tip: In production, integrate change approval and auditing (e.g., signed commits, role-based access) and test changes in a dedicated lab fabric before applying to production routers.