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_clior vendor collections. - Junos: NETCONF is typical (
junipernetworks.junos).
CI/CD model (practical pattern)
A workable model for ISP change control is:
- Data commit: update NetBox objects (BGP peer, prefix, policy) with clear states/tags.
- Pipeline build:
- Pull NetBox data.
- Render intended config per device.
- Run template/unit tests.
- Pre-deploy validation:
- Compare candidate config to device running config (diff).
- Ensure only allowed changes are present.
- Controlled deployment:
- Batch or blue/green group deployment.
- Automated post-checks.
- Write-back/audit:
- 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.netboxansible.netcommoncisco.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
rdandroute_targetsif 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_md5or reference to a secret ID (do not store raw secrets in NetBox)- Use Relationships to model peerings:
- Relationship:
bgp_peersbetween 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_devicelocal_vrflocal_ipremote_asnremote_ipsession_type(ibgp,ebgp)address_family(ipv4,ipv6,vpnv4,evpn, etc.)peer_groupimport_policy,export_policymax_prefix,bfd,multihop,ttl_security,password_refstate(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:
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:
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):
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.j2templates/iosxr/bgp.j2templates/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_configwithsrc:rendered file, plusmatch:/replace:carefully (or parent blocks).- IOS-XR:
cisco.iosxr.iosxr_config- Junos:
junipernetworks.junos.junos_configusingload: mergeorload: overrideand 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
--checkand 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_intentdicts 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
delegateddriver - 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)yamllintruff/blackif you have Python normalization codejinja2linting (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
stagedin NetBox (custom field or tag). - Two-step promotion:
planned→ rendered artifacts + reviewstaged→ eligible for deploymentactive→ 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: 1for critical devicesserial: 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/ Junosshow 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.