Sync the public NetBox demo to a local Infrahub
This tutorial is for people getting started with Infrahub: it starts from a blank page, with no existing Infrahub instance or schema required. We will use Infrahub Sync to copy data from the public NetBox demo into Infrahub, installing and configuring everything we need along the way. This tutorial covers the fundamental work needed to get started; more advanced topics will be covered in subsequent guides.
For an existing NetBox and Infrahub with a Compose Sync deployment, follow Preview NetBox data in an existing Infrahub. That tutorial ends with a saved preview; this local demo continues through an import.
By the end of this tutorial, you will know how to:
- run Infrahub and the Sync service locally
- install Infrahub Sync
- load a schema into Infrahub
- create a NetBox → Infrahub sync project
- synchronize NetBox objects into Infrahub
- review the import in a branch and open a proposed change
Time: ~30 minutes
What you will build: A running Infrahub instance with a production-grade schema covering the same core domains as NetBox (locations, devices, interfaces, IPAM, and organizations), populated with data synchronized from the public NetBox demo into a branch, ready to review as a proposed change.
Prerequisites:
- Docker installed and running (Docker Desktop or OrbStack)
- uv (Python package manager)
- Python 3.11+
The NetBox demo instance is public and resets regularly. Object counts, names, and sample data may differ from the examples in this tutorial. Because anyone can edit it, it can also contain malformed or unexpected data that breaks the sync — see Troubleshooting if you run into errors.
Create a project
Copier is a project scaffolding tool. Use it to create a new Infrahub project from the official template, which includes the standard file structure, task definitions, and a schemas/ folder:
- Run the following command to create a new project directory:
uv tool run --from 'copier' copier copy https://github.com/opsmill/infrahub-template infrahub-automation
When prompted, enter a project name (for example, infrahub-automation), then press Enter to accept the default for every remaining prompt (they all default to No).
- Navigate to the project directory:
cd infrahub-automation
- Open the project in your IDE. If you have Visual Studio Code installed, you can run:
code .
Run ls in the project directory. You should see files including pyproject.toml, tasks.py, and a schemas/ folder.
Start Infrahub
The project template includes Invoke tasks that wrap Docker Compose commands.
- Start all Infrahub services with a single command:
uv run invoke start
The first run takes a few minutes while Docker downloads the container images.
-
Open your browser and go to http://localhost:8000.
-
Log in from the bottom-left corner using the default credentials:
- Username:
admin - Password:
infrahub
- Username:
You should see the Infrahub web interface with a navigation menu on the left side.
Install infrahub-sync
- Clone Infrahub Sync beside the automation project, then install the service profile from that checkout. The service profile is not published to PyPI yet, so the checkout keeps the executable, this tutorial, and its example package at one revision:
git clone https://github.com/opsmill/infrahub-sync.git ../infrahub-sync
uv add --editable "../infrahub-sync[service]" pynetbox
This installs the infrahub-sync command. pynetbox is required by the NetBox adapter.
- Verify infrahub-sync command is available:
uv run infrahub-sync --help
For more about supported adapters and their Python requirements, see the NetBox adapter documentation.
Load a schema into Infrahub
Infrahub stores data according to its schema. Before we can import any data, we need Infrahub to know the kinds of objects that the sync will create.
We provide a production-grade Infrahub schema covering DCIM and IPAM features similar to NetBox's. It is not a one-to-one port of NetBox's own data model, and that's intentional.
This is a production-grade Infrahub schema, not a copy of NetBox's data model. Infrahub gives you a flexible graph model, so a real migration can preserve the parts of NetBox that matter to you while adapting the model to your own workflows.
The schemas are published on the Infrahub Marketplace as the infrahub/traditional-infrastructure-sot collection. Download it with infrahubctl, which the project template already installs:
uv run infrahubctl marketplace get infrahub/traditional-infrastructure-sot --collection
The command writes the collection's 16 schema files into schemas/traditional-infrastructure-sot/. It reads from the marketplace only, so your Infrahub instance does not need to be reachable yet.
Run ls schemas/traditional-infrastructure-sot. You should see files including dcim.yml, location.yml, ipam.yml, and organization.yml.
Export the local Infrahub address and API token. Infrahub Sync will need them later to authenticate against your instance:
export INFRAHUB_ADDRESS="http://localhost:8000"
export INFRAHUB_API_TOKEN="06438eb2-8019-4776-878c-0941b1f1d1ec"
This is the default admin token that the project's Docker Compose stack seeds automatically, and it matches the default already configured in infrahubctl.toml. Do not reuse it for an internet-facing or shared Infrahub instance.
Then load the schema into Infrahub using the project's load-schema task, which walks the schemas/ folder recursively and picks up everything you just downloaded:
uv run invoke load-schema
Refresh the local Infrahub web interface. You should see additional schema objects in the left navigation. You can also open the schema view in the UI to explore the kinds and relationships that were loaded.
Create a NetBox API token
Create a token in the public NetBox demo instance so Infrahub Sync can read data from NetBox.
- Open the public NetBox demo.
- Log in with username
adminand passwordadmin. - Open your user profile.
- Create an API token.
- Copy the complete generated token value.
Export the token locally:
export NETBOX_URL="https://demo.netbox.dev"
export NETBOX_TOKEN="nbt_..."
Export the complete nbt_... token value. Do not include an authorization prefix such as Bearer, and do not copy only the short Key field.
Create the sync project
A sync project is a directory containing a config.yml file. The configuration names the source and destination adapters, maps source fields to destination fields, and includes references that let Infrahub Sync compute write order.
From your project directory (infrahub-automation), create a sync project directory and download the example NetBox to Infrahub configuration.
mkdir -p sync-projects/netbox-demo
cp ../infrahub-sync/examples/netbox_to_infrahub/config.yml \
sync-projects/netbox-demo/config.yml
cp ../infrahub-sync/examples/netbox_to_infrahub/package.yml \
sync-projects/netbox-demo/package.yml
The copied configuration is named from-netbox. It maps selected NetBox objects onto the Infrahub schema.
Open sync-projects/netbox-demo/config.yml in an editor and scan the top-level keys:
nameidentifies the sync project.sourceconfigures the NetBox adapter.destinationconfigures the Infrahub adapter.schema_mappingdefines how NetBox API resources become Infrahub objects.
The configuration includes default endpoint values, but the environment variables exported above take precedence for tokens and URLs.
This tutorial registers the package, uses diff to create a service plan, reviews that
plan, and then submits a confirmed sync. See Run a sync when you
need to apply the exact reviewed plan checksum instead of creating a new composed sync.
For a fuller explanation of this file, see Create a sync project and the schema mapping reference.
Start the Sync API and worker
The CLI sends configuration and run requests to a Sync API. A Prefect worker performs the adapter work. This local tutorial starts both; a production deployment should follow the Sync HTTP API reference.
Infrahub already listens on 127.0.0.1:8000, so the commands below bind the Sync API to
127.0.0.2:8000.
On macOS, create that loopback alias first with
sudo ifconfig lo0 alias 127.0.0.2 up. If a process is bound to port 8000 on all
interfaces, the second address cannot reuse that port; stop the conflicting process or
start the Sync API on another port with
uv run uvicorn --factory infrahub_sync.service.serve:build_app --host 127.0.0.2 --port 8001,
then use that port in INFRAHUB_SYNC_API_URL.
- Save this local service definition as
sync-services.yml:
services:
postgres:
image: postgres:17
environment:
POSTGRES_USER: sync
POSTGRES_PASSWORD: sync-local-only
POSTGRES_DB: infrahub_sync
ports:
- "127.0.0.1:5433:5432"
minio:
# Temporary Chainguard image while a maintained S3 store is selected.
image: cgr.dev/chainguard/minio:latest-dev@sha256:d7c906993247627c19f37fc1fa302c34cf2d209ae0e7dc7d52fb0be6ac2849ba
command: server /data
environment:
MINIO_ROOT_USER: sync-local
MINIO_ROOT_PASSWORD: sync-local-secret
ports:
- "127.0.0.1:9000:9000"
minio-client:
image: cgr.dev/chainguard/minio:latest-dev@sha256:d7c906993247627c19f37fc1fa302c34cf2d209ae0e7dc7d52fb0be6ac2849ba
entrypoint: /bin/sh
command:
- -c
- until mc alias set local http://minio:9000 sync-local sync-local-secret; do sleep 1; done; mc mb --ignore-existing local/infrahub-sync-artifacts
Start PostgreSQL and MinIO, then create the artifact bucket:
docker compose -f sync-services.yml up --detach postgres minio
docker compose -f sync-services.yml run --rm minio-client
Wait until both containers report ready before continuing. These fixed values are local tutorial credentials; do not use them outside an isolated development machine.
- In terminal 1, start Prefect:
uv run prefect server start --host 127.0.0.1 --port 4200
- In each remaining service terminal, change to the
infrahub-automationproject root and export the shared service, storage, worker, and adapter settings:
export PREFECT_API_URL="http://127.0.0.1:4200/api"
export INFRAHUB_SYNC_DATABASE_URL="postgresql://sync:sync-local-only@127.0.0.1:5433/infrahub_sync"
export INFRAHUB_SYNC_S3_BUCKET="infrahub-sync-artifacts"
export INFRAHUB_SYNC_S3_ENDPOINT_URL="http://127.0.0.1:9000"
export INFRAHUB_SYNC_S3_REGION="us-east-1"
export AWS_ACCESS_KEY_ID="sync-local"
export AWS_SECRET_ACCESS_KEY="sync-local-secret"
export INFRAHUB_SYNC_SERVICE_WORK_POOL="sync-process-pool"
export INFRAHUB_SYNC_CONFIG_DIRECTORY="$PWD/sync-projects"
export INFRAHUB_SYNC_SERVICE_BEARER_TOKENS='{"tutorial-admin":{"token":"tutorial-sync-api-token","administrator":true}}'
export NETBOX_URL="https://demo.netbox.dev"
export NETBOX_TOKEN="nbt_..."
export INFRAHUB_ADDRESS="http://localhost:8000"
export INFRAHUB_API_TOKEN="06438eb2-8019-4776-878c-0941b1f1d1ec"
Use the complete NetBox token created earlier. The worker, not the CLI, reads the NetBox
and Infrahub adapter credentials. The package declares them as references — $credential
entries under credentials naming NETBOX_TOKEN and INFRAHUB_API_TOKEN — so the worker
resolves the values from these variables. No NetBox or Infrahub adapter credential value is
written into the package or sent to the Sync API.
- In terminal 2, create the process pool, deploy the service flow, and start its worker:
uv run prefect work-pool create --type process sync-process-pool
uv run python -m infrahub_sync.service.deploy
uv run python -m infrahub_sync.service.worker --pool sync-process-pool
- In terminal 3, start the Sync API:
export INFRAHUB_SYNC_SERVICE_HOST="127.0.0.2"
uv run python -m infrahub_sync.service.serve
- In the terminal where you will run the CLI, configure the API credentials and verify compatibility:
export INFRAHUB_SYNC_API_URL="http://127.0.0.2:8000"
export INFRAHUB_SYNC_API_TOKEN="tutorial-sync-api-token"
curl --fail "${INFRAHUB_SYNC_API_URL}/version"
The API token authenticates the CLI to the Sync API. It is separate from
INFRAHUB_API_TOKEN, which the worker uses to access the destination Infrahub instance.
Register the configuration package
Register the downloaded package with the Sync API.
uv run infrahub-sync configs register sync-projects/netbox-demo/package.yml \
--reason "register NetBox demo import"
Record the returned config_id and registry_version. If you edit the package later, use
configs version <config-id> <package> to create another immutable registry version.
For more about the command flow, see Run a sync.
Create a branch
Infrahub tracks changes through branches, so you can review a batch of imported data as a proposed change before it lands on main. Create a branch to hold this import:
uv run infrahubctl branch create netbox-import
The rest of this tutorial runs diff and sync against this branch with
--branch netbox-import, so you can inspect the destination changes before merging the
Infrahub branch. For more about branches and proposed changes, see
Infrahub version control.
Preview the changes
Run a dry-run diff against the netbox-import branch before writing any data to Infrahub.
uv run infrahub-sync diff \
--config-id <config-id> \
--version <version> \
--branch netbox-import \
--reason "review NetBox demo import"
The service worker loads NetBox and Infrahub, compares both sides, and publishes a saved
plan. The CLI waits by default and prints its summary. Review it again with infrahub-sync runs plan <run-id> --detail. The sync command in the next step creates and applies a new
plan; it does not apply this saved artifact.
Review the output before continuing. On a first run against an empty branch, most planned changes should be creates. Exact counts depend on the current public NetBox demo data.
Sync the data
After inspecting the diff, run the sync against the same branch. The worker loads both systems again, calculates a new plan, and writes it. The CLI prints the service-owned run state.
uv run infrahub-sync sync \
--config-id <config-id> \
--version <version> \
--branch netbox-import \
--reason "import NetBox demo data"
The first sync can take a few minutes because it writes the imported objects and
relationships into the netbox-import branch — main is untouched until you merge the
resulting proposed change. Because this configuration omits order:, Infrahub Sync
derives the write order from the mapping references.
diff against this branch fails once interfaces existOnce the sync has written interfaces, running diff or sync against the same branch again fails while loading the destination. This is a known defect, and it is not specific to NetBox. See diff fails after a sync that wrote interfaces for the error and how to get moving again.
Verify the imported data
The imported data lives on the netbox-import branch, not on main. Open the local Infrahub web interface:
-
Use the branch selector in the top-left corner to switch from
maintonetbox-import. -
Browse the left navigation for imported objects from the NetBox demo. Depending on the current demo data and the example mapping, you may see objects such as:
- tags and organizations (manufacturers, providers, RIRs)
- sites and racks
- devices and interfaces
- IP namespaces, VRFs, VLANs, and VLAN groups
- prefixes, IP addresses, and aggregates
- circuits
NetBox has no IP namespace of its own, so the example creates one Infrahub IP namespace per NetBox VRF name. A prefix or IP address in a VRF goes to the namespace named after that VRF, and one with no VRF goes to Infrahub's built-in
defaultnamespace. That matters because Infrahub identifies a prefix by namespace plus prefix, and an IP address by namespace plus address: without the namespace, the same address in two different VRFs would become a single Infrahub object. The example still syncs the VRF itself, so you keep both the VRF relationship and a namespace that keeps overlapping addresses apart. Infrahub's built-indefaultnamespace usually has nothing on the NetBox side to match it, since only a VRF carrying that name produces one: when nothing does, the plan recordsdefaultas a literal identity and resolves it against Infrahub as the plan is applied, and when a VRF nameddefaultdoes exist the plan resolves it from NetBox like any other namespace.The name is what does the separating, and it has to be unique. Two distinct NetBox VRFs carrying the same name produce the same Infrahub namespace record twice: the second one is refused as it is loaded from NetBox, and the run fails there — before a plan exists, so nothing reaches Infrahub. The two are not merged into one namespace. A NetBox VRF actually named
defaultis a different case: its prefixes and addresses share the built-in namespace with everything that has no VRF, and a prefix or address value repeated inside that shared namespace collides there. You do not have to rename anything in NetBox to get past either — key the namespace on the VRF ID instead of its name.This is a minimal mappingSome records and relationships may be missing — that doesn't mean the sync failed.
-
Once you're happy with the data, open a proposed change from
netbox-importtowardmainso the import can be reviewed before it's merged.
If you switch back to main, none of this data is there yet — that's expected, since it's still isolated in the branch.
What happened
You used Infrahub Sync to move data from NetBox into Infrahub in a controlled sequence:
- Infrahub provided the destination graph and schema.
- NetBox provided the source data.
- A branch isolated the import from
mainso it could be reviewed first. config.ymldescribed the adapters, field mappings, references, filters, and Transformations.- The Sync API registered the configuration and issued the run identity.
diffasked the worker to compare the source and destination without writing changes.syncasked the worker to calculate and apply a new confirmed plan to the branch.
The same pattern applies to larger migrations: start with a clear schema, map a small set of objects, condition source data where needed, review the diff inside a branch, and then synchronize before opening a proposed change toward main.
Stop the local services
When you are finished, stop the API, worker, and Prefect processes with Ctrl-C. Then stop the Infrahub Docker Compose stack and remove the tutorial storage containers:
uv run invoke stop
docker compose -f sync-services.yml down
Troubleshooting
infrahub-sync fails with Both url and token must be specified
ERROR | infrahub_sync.cli | Failed to initialize the Sync Instance: Error initializing InfrahubAdapter: Both url and token must be specified!
The service worker cannot resolve a credential reference declared by the package. The
package and config.yml contain credential references and declarations, but secret
credential values belong only in the worker environment. The package names NETBOX_TOKEN
and INFRAHUB_API_TOKEN, and the worker reads their values. Export these variables before
starting the Prefect worker:
export NETBOX_URL="https://demo.netbox.dev"
export NETBOX_TOKEN="<your-netbox-token>"
export INFRAHUB_ADDRESS="http://localhost:8000"
export INFRAHUB_API_TOKEN="06438eb2-8019-4776-878c-0941b1f1d1ec"
This error names whichever adapter (NetBox or Infrahub) is missing its variables — the same message is raised for either one.
A plan run reports destination models missing from the schema
ERROR | One or more models are not present in the schema - ['LocationSite', ...]
The destination does not have the schema collection this configuration maps to. Download
and load every file listed in Load a schema into Infrahub,
then submit a new diff run. Use a fresh Infrahub instance or branch if an existing
InfraDevice or another overlapping kind prevents that collection from loading.
The similarly named models/examples/netbox/netbox.yml file in the Infrahub source
repository is not a replacement: it uses a different set of destination kinds.
infrahub-sync fails with Object ... already present
Two source records resolved to one identifier while NetBox was being read. The run stops there — before a plan exists and before anything is written to Infrahub — so the state at the destination is whatever the last successful run left. The kind in the message says which of two different problems you have.
Two IP addresses or prefixes in one namespace
ValueError: An error occurred while loading Netbox: ('Object 172.16.0.2/24__default already present', IpamIPAddress "172.16.0.2/24__default")
The part after __ is the IP namespace the object landed in, so two NetBox IP addresses share both an address and a namespace. That happens when they sit in the same VRF, when neither has a VRF and both went to default, or when one has no VRF and the other sits in a VRF actually named default.
That does not mean one of them is wrong. NetBox can hold the same address more than once on purpose — a shared VRRP or HSRP address, for example. It can also be an accidental duplicate. Open both rows in NetBox (IPAM > IP Addresses, search the address from the message) and compare their VRF, tenant, role, assigned object, and description before you change anything.
What you find points to one of two fixes:
- The source record is wrong. Correct or remove the entry that should not be there in NetBox.
- Both records are correct, and the mapping is collapsing a distinction Infrahub needs to keep. That needs the namespace mapping or the destination schema to change so the two addresses resolve somewhere different — editing
identifiersalone will not help, since Infrahub identifies the address by namespace plus address either way. Two genuinely identical addresses inside one namespace refuse under any namespace recipe.
Two VRFs with the same name
ValueError: An error occurred while loading Netbox: ('Object MGMT already present', IpamNamespace "MGMT")
The example names each IP namespace after a NetBox VRF, so two VRFs called MGMT produce that namespace record twice and the second is refused. They are separate objects in NetBox, each with its own rd value, and the sync does not merge them — it stops. The same source also collides at IpamVRF, which is keyed on the VRF name as well; namespaces load first, so the namespace is the one the message names.
Renaming one of the VRFs in NetBox resolves it, but you do not have to touch NetBox: key the namespace on the VRF ID instead of its name, which uses an identifier NetBox cannot repeat. That recipe covers namespaces, prefixes and addresses; the full example's IpamVRF entry is still keyed on the name and needs its own transform before the whole example runs against such a source.
Re-run the command once NetBox or the mapping reflects what you intend. Deleting a row purely to clear the message can destroy real data.
diff fails after a sync that wrote interfaces
ValueError: An error occurred while loading Infrahub: Cannot build unique_id for peer
InterfaceLag[18c6…] (relationship InterfacePhysical.bundle, parent id=18c6…): missing
identifier key(s) ['device']; required identifiers=['device', 'name'], present
keys=['local_id', 'name', 'description', ...]
A first diff succeeds. Once a sync or apply has written an InterfacePhysical whose bundle points at an InterfaceLag, every later diff against that branch fails — while loading the destination, before any plan is produced. The message names Infrahub, not Netbox: the source is not involved. Deleting the interface objects at the destination restores normal behavior, and the cycle repeats.
The cause is that the destination loader rebuilds each related peer's identifier from a cached copy of the peer node, and the copy it finds carries the peer's attributes but not its relationships. InterfaceLag is identified by device and name, so its identifier cannot be rebuilt and the load aborts.
The client does not expose a control that changes worker extraction behavior. Delete the interface objects at the destination, correct the mapping or destination data, then create a new plan. Those objects are re-imported by the next confirmed sync.
Two things to be clear about:
- This is not a NetBox-specific defect, and it is not introduced by the saved-plan workflow. It affects any configuration in which a mapped kind references a peer kind whose
identifiersinclude a relationship — thenautobot,ipfabric,slurpit, andaciexample configurations all contain that shape. - A source record that references a peer the source itself cannot resolve fails the whole run when the plan is derived. The client cannot soften that worker refusal.
Next steps
Now that you have completed a first sync, you have covered some of the basic objects in NetBox.
This is only the first step. You will likely want to bring in the parts that are unique to your own NetBox instance (for example, roles or custom fields).
Other guides will soon be available to cover:
- How to sync locations/regions
- How to deal with VLAN/Prefix/Device roles
- How to cover custom attributes / relationships
- Migrate configuration context