Migrate from NetBox or Nautobot
Migrate data from NetBox or Nautobot into Infrahub one model at a time. The existing system keeps running throughout, and you decide when — or whether — to retire it.
Before you start, install Infrahub Sync if you haven't already — see Install Infrahub Sync.
For a gradual migration, plan data ownership and workflow changes alongside the import. For the first connection and preview with existing systems, follow the NetBox or Nautobot tutorial.
The gradual migration approach
A one-shot cutover from NetBox or Nautobot to Infrahub carries real risk: the new system has to be completely ready, all dependent automation has to switch at the same time, and rollback is hard if something surfaces afterward. The gradual approach removes that risk by syncing data into Infrahub model by model, verifying each step, and migrating dependent workflows on your own schedule.
The phases:
- Prepare the Infrahub schema. Define the schema in Infrahub that will hold the migrated data.
- Build the sync project. Create a YAML configuration that maps NetBox or Nautobot models and fields to the corresponding Infrahub models and fields.
- Seed Infrahub with initial data. Run the sync to populate Infrahub with the current state of the source system.
- Run in side-by-side mode. Keep both systems active. Sync runs on a recurring schedule to keep Infrahub current with source-system changes.
- Migrate workflows. Update automation, scripts, and dashboards to read from Infrahub instead of the source system.
- Retire the legacy system (optional). Once all dependent workflows read from Infrahub and the legacy system is no longer the source of truth for any data, decommission it — or keep it running for the workflows where it remains the better tool.
For long-running side-by-side operation (where the legacy system is not being retired), see Use NetBox or Nautobot with Infrahub. The phases below focus on migration specifically.
Phase 1 — Prepare the Infrahub schema
Infrahub uses a custom schema that you define. Before migrating data, decide on the schema Infrahub will use for the data coming over from NetBox or Nautobot.
The Infrahub schema does not have to mirror the source system one-to-one. Migration is an opportunity to model the data the way you need going forward, not the way the previous tool happened to model it.
For a concrete NetBox starting point, follow the
NetBox demo tutorial. It provisions a
fresh Infrahub instance, loads the matching schema-library revision, configures the
Sync API and service worker, creates a current NetBox demo token, installs the adapter
dependency, and registers the from-netbox configuration package. Use
examples/nautobot_to_infrahub/ as a configuration reference for Nautobot, and replace
its model and field names with the equivalents in your Infrahub schema.
Phase 2 — Build the sync project
A sync project is a directory containing a config.yml that defines the source (NetBox or Nautobot), the destination (Infrahub), and the per-model schema mapping.
Start from the example
The Infrahub Sync repository includes configuration examples at
examples/netbox_to_infrahub/ and examples/nautobot_to_infrahub/. Copy the
directory matching your source system as the starting point. For NetBox, use the
demo tutorial to provision the schema
and source credentials that the example expects. Update model and field names to
match the schema you chose in Phase 1.
Define sync order
Models must be synced in dependency order: anything referenced by another model must be synced first. A typical order:
- Independent objects: tags, locations, manufacturers, device roles, platforms
- IP namespaces (nothing else has to exist first)
- Rack and site data (depends on locations)
- Devices (depends on locations, roles, manufacturers, platforms)
- Interfaces (depends on devices)
- IP addresses and prefixes (depends on IP namespaces, interfaces, VRFs)
- VLANs and VLAN groups
The exact order depends on your schema. See Sync instance configuration for the syntax of the order key.
IP namespaces are on that list because Infrahub identifies a prefix by namespace plus prefix, and an IP address by namespace plus address. Which mapping you need depends on whether your source has a namespace object of its own.
NetBox and Nautobot 1.x have none, so the namespace has to be derived. The NetBox example creates one Infrahub IP namespace per VRF name and sends objects with no VRF to Infrahub's built-in default namespace. Do the same in your own mapping if your source can hold the same prefix in more than one VRF — otherwise those prefixes collide with each other, and the run refuses rather than keeping them apart. Derive the namespace from something the source cannot repeat if its VRF names are not unique.
Nautobot 2.0 and later have native IPAM namespaces, added in 2.0.0. Map those directly and carry them into the prefix and address identities, as examples/nautobot-v2_to_infrahub/ does with its NautobotNamespace entry mapped from ipam.namespaces. Do not derive the namespace from the VRF name there: the source already separates the objects, and keying on the VRF name instead would make two VRFs that happen to share a name collide with each other.
Under the VRF-name pattern the namespace is keyed by that name, so it works only while the names are distinct. Two VRFs sharing a name produce one namespace record twice: the load refuses the second and the run fails against the source, before a plan exists. It is a refusal, not a merge. A VRF named default is a different case: its objects share the built-in namespace with every object that has no VRF, and a prefix or address value repeated inside that shared namespace collides there. Check your source for repeated VRF names before you depend on the split, or key the namespace on the source's own VRF ID instead, as Key the namespace on the VRF ID instead of its name does for NetBox — no source renaming required. Note that under an ID-derived recipe a VRF named default gets its own namespace rather than the built-in one. A source with native namespaces is not affected, because its namespaces are carried rather than derived.
Adopt namespaces in an Infrahub that already holds imported data
Changing a mapping or its identifiers changes what later runs create and match. It does not move objects an earlier sync already wrote. Only the objects whose resolved namespace changes take on a new identity: a prefix or address that was imported into default and still resolves to default goes on matching the object already there, while one that now resolves to a VRF-named namespace is created as a new object beside the old one.
Nothing retires the superseded objects for you. A saved plan records a delete for every destination object the source no longer accounts for, whatever diffsync_flags are set, but apply never executes one: it reports how many deletes it skipped and completes. The old records therefore stay exactly where they are, still carrying their relationships.
Review the plan before you write, and into a branch. Plan the run with infrahub-sync diff --config-id <config-id> --version <version> --branch <branch> --reason "...", read it with infrahub-sync runs plan <run-id> --detail, and apply it by checksum with infrahub-sync apply <run-id> --expected-checksum <checksum> --reason "...". The saved plan names every create it intends, so you can see which prefixes and addresses are about to appear under a new namespace before any of them exists. Afterwards, audit the old objects and everything pointing at them — interfaces, devices and any other relationship bound to the prefix or address — and retire or re-point them deliberately. Infrahub Sync does not migrate them for you, and wiping the destination is not a substitute: it discards Infrahub-only data the sync never owned.
Map fields between the source system and Infrahub
For each model, define how source fields map to Infrahub fields. The schema mapping section of the YAML handles direct field mappings, identifier mappings, references to other models, and static values. For detail on each mapping pattern, see Schema mapping reference.
For models the sync will create, identifiers must cover the destination kind's human-friendly ID.
Infrahub matches a newly created object on the human-friendly-ID values the write carries, and a write
that omits one does not match an existing object — it creates a second one and reports success. diff
therefore refuses a create it cannot prove carries every component, naming the kind and what is missing,
rather than letting the duplicate happen.
Objects that already exist are unaffected: an update is keyed by the destination object's own id,
recorded when the plan is built, so it changes the object it means even where identifiers and the
human-friendly ID differ — including when a rename changes the human-friendly ID itself.
If a refusal names one of your models, either add the missing field to that model's identifiers or
adjust the human-friendly ID in the destination schema.
Phase 3 — Seed Infrahub with initial data
Register the package, then use its immutable identity to verify what the sync will do:
infrahub-sync configs register <package.yml> --reason "register migration"
infrahub-sync diff --config-id <config-id> --version <version> --reason "review migration"
Creates must prove their own key. For a destination kind with a human-friendly ID,
Infrahub matches the write using its complete human-friendly ID. Sync requires a usable
value for every component, supplied by a mapped field or a relationship reference; a
relationship-crossing component comes from the referenced peer's identifiers. For a kind
without a human-friendly ID, the model's identifiers must cover at least one declared
uniqueness constraint so the destination refuses duplicates. diff refuses missing
coverage or unusable values, naming the kind and what is missing.
Updates are not subject to that rule. An update carries the destination object's own
id, recorded when the plan was built, so it writes the object it means even where
identifiers and the human-friendly ID differ — including a kind whose human-friendly ID
crosses a relationship, one that declares none at all, and a rename that changes the
human-friendly ID itself.
Apply is sequential: a refused operation writes nothing, operations applied before it stay
written, and no later operation runs. run.json records what was applied.
On the current from-netbox mapping and schema-library revision IpamPrefix and
IpamIPAddress satisfy that rule: their identifiers include ip_namespace, so their
creates key themselves. A namespace backed by a NetBox VRF resolves from the source plan
like any other peer, including a VRF named default. Where the source holds no namespace of
that name — the usual case for objects with no VRF — the built-in default is recorded as a
literal identity and resolved at apply against the destination's human-friendly ID for that
kind. Separately, LocationRack is not
convergent when different sites contain racks with the same name — the configuration
identifies a rack by name and site while the current schema-library revision keys it
by name alone — and diff now refuses those creates as a destination-identity collision
rather than silently merging them.
Plans written by an earlier version cannot be applied. The saved-plan format is now
version 3, which records the destination id on updates. A format-2 plan can still be read
and reviewed; apply refuses it and asks for a fresh diff.
Three live qualifications exercise the saved-plan write surface.
tests/integration/test_saved_plan_apply_integration.py runs the full
NetBox → Infrahub path on a keyed slice: BuiltinTag, LocationSite,
LocationRack, OrganizationManufacturer, DcimPlatform and DcimDeviceType
are seeded, and DcimDevice is the kind under test. Every one declares
human_friendly_id: ['name__value'], so every planned write renders a key. It
needs the pinned schema library loaded, a NetBox carrying the deterministic
dataset (sites site-a/site-b/site-c, devices dev-01…dev-40, tags
tag-01…tag-10), and NETBOX_URL / NETBOX_TOKEN alongside the Infrahub
variables. Six tests must pass. A seventh is optional: SC-016's ambiguous-peer
half skips when the destination schema admits no genuinely ambiguous peer, which
is the case on a keyed slice, since every kind is filtered on exactly the
component its uniqueness constraint pins. The skip message names the constraint
that establishes it.
It needs a fresh disposable Infrahub for each run. The suite writes and does not clean up: it perturbs the destination with a per-run canary and then asserts the derived plan carries the create, update and delete those perturbations imply. A second run against the same instance sees the first run's canary and fails in setup. Reset between runs.
Two further qualifications need no source system, only INFRAHUB_ADDRESS and
INFRAHUB_API_TOKEN:
tests/integration/test_infrahub_replace_set_shrink_integration.py— keyed planned writes, applied and re-applied with the same result;tests/integration/test_infrahub_keyed_write_integration.py— a kind whose human-friendly ID crosses a relationship, created and then re-applied to prove it converges rather than duplicating; an update keyed by its recorded id, renaming in place; and a recorded id the destination cannot find, refused with nothing written. Each runs on a branch the test creates and deletes.
The interface kinds are covered there: they are written like any other kind now, and what the test pins is that the destination converges them.
The diff prints the proposed changes to the terminal without modifying Infrahub. Review it model by model:
- Are the right models being created in Infrahub?
- Are field values being mapped correctly?
- Are relationships between models being resolved correctly?
- Are there any unexpected creates, updates, or deletes?
When the diff looks correct, run the sync:
infrahub-sync sync --config-id <config-id> --version <version> --reason "seed Infrahub"
The sync applies changes in the order defined by the project's order key. Independent models are created first, then dependent models, then models that reference earlier ones.
Migrate one model at a time
For the first sync, enable only the most independent models (tags, locations). Verify the data in Infrahub, then add the next layer of models (racks, devices), run again, and so on. This keeps the diff output reviewable and isolates any mapping issues to the model that caused them.
Phase 4 — Run in side-by-side mode
After the initial seed, schedule the sync to run on a recurring cadence so Infrahub stays current with changes in the source system. The cadence depends on how often the source data changes — hourly, daily, or on-demand are all reasonable.
Infrahub Sync has no built-in scheduler. Use cron, CI, Prefect, or another workflow tool — see Schedule sync runs.
Default behavior preserves Infrahub-only data
By default, the SKIP_UNMATCHED_DST flag is enabled — Infrahub objects without a corresponding object in the source system are left alone. This is the safe default. Add Infrahub-only data (new schemas, design objects, intent data) without worrying that a sync run will delete it.
To change this behavior for sync — for example, removing Infrahub objects when they're deleted in the source system — adjust the diffsync_flags in the sync configuration. See Sync instance configuration.
The saved-plan path behaves differently and the flags do not change it. A plan records a delete for every Infrahub object missing from the source, but apply never executes one: it applies the non-deletes, completes successfully, and reports how many deletes it skipped. Reviewing a plan against a destination holding Infrahub-only data will therefore show a non-zero delete count that no apply will act on. See Deletes in a plan.
Decide on conflict policy early
During side-by-side operation, the same model may be edited in both tools. Decide upfront which tool is authoritative for each model and document the decision in the project configuration. If both tools allow edits to the same model, conflicts will eventually occur — and Infrahub Sync's default behavior is for the source system to win.
Phase 5 — Migrate workflows
As Infrahub becomes a reliable mirror of the source system, update dependent automation to read from Infrahub instead. This is the longest phase of a migration and typically happens workflow by workflow:
- Identify each script, pipeline, or dashboard that reads from the source system.
- Update it to read from Infrahub (using the Python SDK, infrahubctl, or GraphQL).
- Verify the workflow behaves correctly with Infrahub as the source.
- Once you're confident, retire the source-reading version of the workflow.
For each migrated workflow, you can also stop writing to the source system for the affected data — at which point that data type is fully migrated.
Phase 6 — Retire the legacy system (optional)
Once all dependent workflows read from Infrahub and the legacy system is no longer the source of truth for any data:
- Decommission the legacy system entirely if it's no longer needed.
- Keep the legacy system running for specific workflows (rack elevation, cable tracing, or any other capability you prefer there). Reconfigure the sync to keep the legacy system in sync with Infrahub for those workflows, or stop syncing entirely if the data does not need to flow.
Many teams reach a stable side-by-side state and stay there indefinitely. See Use NetBox or Nautobot with Infrahub.
Differences between migrating from NetBox versus Nautobot
The same phases apply for both source systems. The mechanical differences:
- Use the NetBox adapter for NetBox sources and the Nautobot adapter for Nautobot sources.
- Start from
examples/netbox_to_infrahub/for NetBox,examples/nautobot_to_infrahub/for Nautobot. - Field names and nested structures differ between the two systems — for example, Nautobot status fields are objects while NetBox status fields are strings. The schema mapping work is source-specific, but the overall approach is identical.
Common issues
- Sync runs slowly on large datasets. Initial syncs of large source-system installations can take a while. Sync performance is an active area of development. Recurring sync runs are typically fast — only deltas are applied.
- A model is being created in Infrahub that should not be. Check the sync project's
orderkey and per-model filters — the model may be enabled when it shouldn't be, or a reference from another model may be pulling it in. - Field values are not mapping correctly. Check the schema mapping section of the YAML. Common causes: a source field has a different name than expected, or a relationship reference is not resolving because the referenced model was not synced first.
- Custom fields in the source system are not appearing in Infrahub. NetBox and Nautobot custom fields require explicit mapping in the YAML — they are not synced automatically.