How to Build a BGP Provisioning Pipeline with NetBox and Ansible for ISP Networks

How to Build a BGP Provisioning Pipeline with NetBox and Ansible for ISP Networks

This tutorial walks through an advanced, production-oriented approach to provisioning BGP consistently across multi-vendor ISP networks using netbox as the source of truth and Ansible as the automation engine. The goal is not “push config from a spreadsheet,” but a repeatable pipeline with strong modeling, idempotence, CI gates, safe rollout, and verifiable outcomes.

Architecture and prerequisites: components, CI/CD model, role of NetBox vs. automation tooling, target platforms (Cisco IOS-XE/IOS-XR/Junos)

Core components

  • NetBox (source of truth)
  • Stores intent and relationships: devices, interfaces, VRFs, prefixes, ASNs, peers, sites, tags, metadata.
  • Exposes data via REST API (and GraphQL if enabled).
  • Ansible (render + deploy)
  • Reads inventory and variables from NetBox.
  • Renders vendor-specific config from templates.
  • Applies config via network modules (or NAPALM).
  • CI system (quality gates)
  • Validates data and templates.
  • Runs linting, unit tests, and dry-run diffs.
  • Controls promotion from “candidate” to “production.”
  • Config transport
  • IOS-XE: ansible.netcommon.network_cli (SSH), or RESTCONF where available.
  • IOS-XR: SSH/NETCONF (module-dependent), often network_cli or vendor collections.
  • Junos: NETCONF is typical (junipernetworks.junos).

CI/CD model (practical pattern)

A workable model for ISP change control is:

  1. Data commit: update NetBox objects (BGP peer, prefix, policy) with clear states/tags.
  2. Pipeline build:
  3. Pull NetBox data.
  4. Render intended config per device.
  5. Run template/unit tests.
  6. Pre-deploy validation:
  7. Compare candidate config to device running config (diff).
  8. Ensure only allowed changes are present.
  9. Controlled deployment:
  10. Batch or blue/green group deployment.
  11. Automated post-checks.
  12. Write-back/audit:
  13. Record change outcome back to NetBox (custom fields, journal entries, change log).

Prerequisites

  • NetBox (v3.6+ recommended), with:
  • Tenancy, VRFs, Prefixes in use.
  • Device roles / sites / platforms populated.
  • API token for automation.
  • Ansible (2.15+ recommended) and collections:
  • netbox.netbox
  • ansible.netcommon
  • cisco.ios, cisco.iosxr (as appropriate)
  • junipernetworks.junos
  • A Git repo containing:
  • Playbooks, roles, templates
  • CI config (GitHub Actions/GitLab CI/Jenkins)
  • Pre-commit hooks and tests
  • A convention for “what is intent vs. what is derived”:
  • NetBox holds intent and structured relationships.
  • Templates derive vendor syntax and ordering.

NetBox data model for BGP: VRFs, RRs, peers, policies, prefixes; custom fields and relationship modeling to support automated generation

NetBox does not ship a full “BGP peer/policy” native schema out of the box. For ISP-grade BGP provisioning you typically model:

  • Topology: devices, interfaces, loopbacks, addressing, ASN
  • Routing context: VRFs (L3VPN, internet, etc.)
  • Peerings:
  • iBGP (RR clients, full mesh subsets)
  • eBGP (transit, IX, PNIs, customers)
  • Policies: per neighbor or peer-group
  • Prefix intent: what should be originated/accepted/advertised

Recommended modeling approach (balanced between structure and flexibility)

1) Use existing NetBox objects wherever possible

  • VRFs: use NetBox VRF objects, including rd and route_targets if relevant.
  • Prefixes:
  • Use prefixes inside the appropriate VRF.
  • Tag prefixes for policy use (export-to-transit, customer-aggregate, blackhole, etc.).
  • Devices/Interfaces/IPs:
  • Store loopbacks and peering subnets as IP addresses on interfaces.
  • Use interface roles (e.g., loopback, p2p, ix) via tags or naming conventions.

2) Add a “BGP peer” data structure

There are two common ways:

  • NetBox Custom Objects / Plugins: best if you want a real schema and UI workflows.
  • Generic modeling using Custom Fields + Relationships + Tags: simpler to adopt and works well if designed carefully.

A practical “no-plugin” pattern:

  • Create a Custom Field on Device (or VRF) for:
  • local_asn (integer)
  • router_id (IPv4)
  • bgp_role (enum: rr, rr-client, edge, core)
  • Create a Custom Field on Interface or IP Address for:
  • bgp_peer_group (string)
  • bgp_md5 or reference to a secret ID (do not store raw secrets in NetBox)
  • Use Relationships to model peerings:
  • Relationship: bgp_peers between Device ↔ Device (or Device ↔ IP Address)
  • Relationship attributes can be encoded via tags + custom fields on endpoints (limitations apply)

If you can use a plugin/custom object, a stronger schema is:

  • Object: BGPSession
  • local_device
  • local_vrf
  • local_ip
  • remote_asn
  • remote_ip
  • session_type (ibgp, ebgp)
  • address_family (ipv4, ipv6, vpnv4, evpn, etc.)
  • peer_group
  • import_policy, export_policy
  • max_prefix, bfd, multihop, ttl_security, password_ref
  • state (planned, staged, active, decommission)

3) Model policy intent explicitly

BGP policies are where automation often breaks down because “policy” can mean anything. Keep it composable:

  • Define policy building blocks:
  • prefix-set / prefix-list intent (from NetBox Prefix objects + tags)
  • community sets (small YAML data in Git, or NetBox “Config Context”)
  • as-path sets (usually in Git; could be netbox config contexts)
  • Define policy attachment points:
  • Per neighbor / peer-group: import_policy, export_policy
  • Per VRF: default policies

In practice:
- Store the attachment (which policy applies where) in NetBox.
- Store the policy logic (exact vendor syntax) in templates + version-controlled data.

4) Use Config Contexts for “global but scoped” data

NetBox Config Contexts (scoped by site/role/platform/tenant) are useful for:

  • RR cluster IDs and RR regions
  • Default timers, BFD defaults
  • Standard peer-groups (TRANSIT, IXP, CUSTOMER, CORE)
  • Per platform knobs (e.g., IOS-XR route-policy vs. IOS route-map)

This keeps device variables consistent while still being centrally managed.


Ansible inventory and playbook design: netbox inventory plugin, playbook structure, Jinja2 templates for BGP neighbors/policy maps, handling vendor divergences (Cisco examples)

Inventory: use the NetBox inventory plugin

Use NetBox to select devices by site/role/tag and populate host vars automatically. Typical grouping:

  • by site.slug
  • by device_role.slug (e.g., rr, pe, p)
  • by platform.slug (e.g., iosxe, iosxr, junos)

Example inventory/netbox.yml:

yaml
plugin: netbox.netbox.nb_inventory
api_endpoint: "https://netbox.example.net"
token: "{{ lookup('env', 'NETBOX_TOKEN') }}"
validate_certs: true

group_by:
  - sites
  - device_roles
  - platforms

query_filters:
  - status: active
  - has_primary_ip: "true"

compose:
  ansible_host: primary_ip4.address | default(primary_ip.address) | regex_replace('/\\d+$','')
  netbox_device_id: id

Playbook structure: separate phases (build → validate → deploy → verify)

A practical structure:

  • playbooks/build.yml
    Pull NetBox data + render candidate configuration to artifacts.
  • playbooks/deploy.yml
    Deploy to devices with batch controls.
  • playbooks/verify.yml
    Run post-change verification and parse outputs.
  • roles/bgp/
    Vendor-specific tasks + templates + parsing.

Design principles:

  • Idempotence: generate full intended BGP stanza per device/VRF and apply with “replace block” or vendor module merge semantics.
  • Separation: keep data gathering separate from rendering and deployment.
  • Vendor abstraction: normalize data into a vendor-neutral structure first, then render per NOS.

Normalizing NetBox data into an internal model

Avoid passing raw NetBox objects directly into templates. Instead:
- Gather NetBox data (API lookups, config contexts, relationships)
- Build a normalized bgp_intent dict per host, like:

yaml
bgp_intent:
  local_as: 65001
  router_id: 192.0.2.1
  vrfs:
    - name: INTERNET
      afs: [ipv4, ipv6]
      neighbors:
        - name: TRANSIT-A
          remote_as: 64500
          remote_ip: 203.0.113.9
          peer_group: TRANSIT
          import_policy: TRANSIT-IN
          export_policy: TRANSIT-OUT
          max_prefix: 200000
          bfd: true

This allows templates to stay stable even if you change how NetBox stores the raw inputs.

Templates: BGP neighbors and policies (Cisco IOS-XE example)

For IOS-XE, a common approach is generating:
- router bgp <asn> stanza
- address-family ipv4/ipv6 vrf <VRF>
- neighbor peer-groups + neighbor statements
- route-maps + prefix-lists

Example Jinja2 snippet (conceptual):

jinja2
router bgp {{ bgp_intent.local_as }}
 bgp router-id {{ bgp_intent.router_id }}
{% for vrf in bgp_intent.vrfs %}
 address-family ipv4 vrf {{ vrf.name }}
{% for n in vrf.neighbors %}
  neighbor {{ n.remote_ip }} remote-as {{ n.remote_as }}
  neighbor {{ n.remote_ip }} description {{ n.name }}
  neighbor {{ n.remote_ip }} route-map {{ n.import_policy }} in
  neighbor {{ n.remote_ip }} route-map {{ n.export_policy }} out
{% if n.max_prefix is defined %}
  neighbor {{ n.remote_ip }} maximum-prefix {{ n.max_prefix }}
{% endif %}
{% endfor %}
 exit-address-family
{% endfor %}

Handling vendor divergences:

  • IOS-XR: route-policy language, neighbor-group vs. neighbor, VRF syntax differs.
  • Junos: policy-options + protocols bgp groups; import/export policies on group, not per neighbor (commonly).

Treat these as separate template sets under the same role:

  • templates/iosxe/bgp.j2
  • templates/iosxr/bgp.j2
  • templates/junos/bgp.j2

Then select with ansible_network_os or platform.slug.

Deployment modules (choose a strategy per NOS)

Common strategies:

  • IOS-XE:
  • cisco.ios.ios_config with src: rendered file, plus match:/replace: carefully (or parent blocks).
  • IOS-XR:
  • cisco.iosxr.iosxr_config
  • Junos:
  • junipernetworks.junos.junos_config using load: merge or load: override and committing with confirmed commit options.

Keep “how config is applied” vendor-specific, but keep “what config should be” driven by normalized intent.


Testing, idempotence and CI: local dry-run with napalm/ansible, unit-testing templates, molecule for network roles, pre-commit hooks and staging to prevent config drift

Idempotence checklist

  • Stable ordering in templates (sort neighbors, policies, prefix lists) to avoid diff churn.
  • Explicit defaults: do not rely on platform implicit defaults if they differ across releases.
  • Block-based replacement (where safe): render a clearly delimited block you own.
  • Avoid “append-only” config behavior for policy constructs; ensure old objects are pruned if removed from intent.

Local dry-run and diffs

  • Render candidate configs locally (no device access required).
  • Use --check and diff mode where supported:
  • ansible-playbook ... --check --diff
  • For NAPALM-capable flows:
  • Use “load merge candidate” + compare diff + discard.

Unit-testing templates

A pragmatic approach:

  • Create fixtures: sample bgp_intent dicts for edge/core/rr scenarios.
  • Render templates in Python tests (pytest) and assert:
  • Required lines exist
  • No forbidden statements exist
  • Deterministic output across runs

This catches regressions when you modify templates or data normalization.

Molecule for network roles

Molecule can be used to validate role logic (rendering, variable transforms, sanity checks) even without fully emulating a router:

  • Use delegated driver
  • Verify rendered artifacts and linting output
  • Optionally run in containerized NOS labs if available (more complex, but valuable)

Pre-commit hooks

Recommended hooks for this pipeline:

  • ansible-lint (with a project baseline)
  • yamllint
  • ruff / black if you have Python normalization code
  • jinja2 linting (where available) or basic syntax checks
  • A custom hook: “render all templates from fixtures and ensure no diff”

Staging to prevent drift

Two important patterns:

  • State gating: only deploy sessions/policies marked staged in NetBox (custom field or tag).
  • Two-step promotion:
  • planned → rendered artifacts + review
  • staged → eligible for deployment
  • active → deployed and verified; write-back status

This prevents “someone changed intent but it immediately rolled out.”


Safe deployment, validation and rollback: blue/green and batched rollouts, automated verification (show commands, ee/parse), automated rollback recipes and audit logging back to NetBox

Deployment safety patterns

Batched rollout (common for ISP backbones)

  • Batch by site/POP, then by role:
  • Route reflectors first (or last), depending on your design
  • Core before edge (or vice versa), depending on dependency direction
  • Limit concurrency:
  • serial: 1 for critical devices
  • serial: 10% for larger fleets
  • Maintenance windows and explicit change IDs in playbook vars

Blue/green for policy-heavy changes

For high-risk policy changes (e.g., transit filtering), use parallel policy names:

  • Deploy new policy objects:
  • TRANSIT-IN-V2, TRANSIT-OUT-V2
  • Attach to a subset of peers/devices first
  • Validate, then flip remaining peers
  • Remove old policies after a defined soak period

Automated verification (device outputs + parsing)

Verification should be automated and tied to intent:

  • Session state:
  • show bgp summary / show bgp vpnv4 unicast summary / Junos show bgp summary
  • Prefix counters:
  • received/accepted/advertised counts match expectations (within tolerances)
  • Policy attachment:
  • ensure correct route-map/route-policy/policy-statement applied

For parsing:
- Use structured output when possible (platform-dependent):
- TextFSM/Genie parsers for IOS-XE
- XML/JSON output options in Junos
- Or use lightweight regex checks for critical lines if structured output isn’t feasible.

Rollback recipes

Rollback should be designed per platform:

  • Junos:
  • Use commit confirmed 5 (or similar) for safety.
  • If verification fails, do not confirm; device reverts automatically.
  • IOS-XE / IOS-XR:
  • Prefer replace operations scoped to owned blocks.
  • Keep “last known good” artifact per device and be able to re-apply it quickly.
  • If using checkpoints/rollback features, integrate them (platform and operational maturity vary).

A practical Ansible rollback flow:

  • Save pre-change running config snapshot to artifacts store.
  • Deploy candidate.
  • Verify.
  • On failure, re-apply snapshot (or run explicit “undo” tasks).
  • Record outcome.

Audit logging back to NetBox

NetBox can be used to record:

  • Change ID (from CI pipeline)
  • Deployed version (git SHA)
  • Outcome (success/failure)
  • Timestamp + operator identity (via CI identity)

Implement via:
- NetBox Journal Entries on the device
- Or custom fields like last_bgp_deploy_sha, last_bgp_deploy_time, last_bgp_deploy_status

This closes the loop: NetBox remains the source of truth and reflects operational reality.


Key takeaways

  • netbox is best used for intent + relationships, not as a raw config store. Normalize into a vendor-neutral model before templating.
  • A reliable pipeline separates build/render, pre-check/diff, deploy, and verify with CI gates between steps.
  • Vendor divergence is unavoidable; handle it with per-NOS templates and tasks, while keeping a shared intent structure.
  • Idempotence depends on stable template ordering, explicit defaults, and careful config application semantics.
  • Safe rollouts rely on batching/blue-green, automated verification, and a tested rollback mechanism, with audit write-back to NetBox.

Common pitfalls

  • Modeling BGP peers only as free-form text: becomes untestable and impossible to validate at scale.
  • Storing secrets in NetBox custom fields: keep passwords/keys in a secrets manager; store references/IDs in NetBox.
  • Template churn from unordered data: always sort peers/prefixes/policies to reduce meaningless diffs.
  • Overusing “merge” without cleanup: stale neighbors and policies accumulate; build pruning logic or use owned-block replacement.
  • No post-deploy verification: configuration success is not routing success; verify session state and prefix counters.
  • Deploying directly from “planned” state: require explicit staging/promotion to prevent accidental rollout of incomplete intent.
  • Ignoring platform commit semantics: Junos confirmed commit and IOS-style config application differ—design rollback per NOS.