本文へ移動
購読
GUIDE#infra更新 2026.09.25

NetBoxでIPアドレスと機器の台帳を作る:Docker構築から空きIP払い出しまで

NetBox 4.7.1をDocker Composeで起動し、小さなオフィスのVLAN・サブネット・機器を登録。REST APIで次の空きIPを払い出し、重複がどこまで弾かれるかも実機で確かめました。

level
中級
time
約45分
stack
NetBox 4.7.1(netbox-docker 5.1.1) / PostgreSQL 16 / Redis 7 / Docker Compose
author
@ryoma_t
// 前提条件
Docker Composeが使えるLinuxホスト(空きメモリ1.5GB以上) / curlとjq / IPアドレス・サブネット・VLANの基礎知識
NetBox 4.7のプレフィックス10.10.10.0/24のIPアドレス一覧。ゲートウェイ、予約済み範囲、srv-01とnas-01の割り当てと「243 IPが利用可能」が表示されている
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

NetBoxとは、IPアドレス・VLAN・機器・ラック・配線といったネットワークの「あるべき姿」を1か所に記録するオープンソースの台帳アプリケーション(IPAM/DCIM)です。稼働状況の監視やDHCPによるアドレス配布はしません。人が決めた設計をデータとして持ち、画面とREST APIの両方から同じデータを読み書きできる点が、ExcelでのIPアドレス管理との決定的な違いです。

対象は、ExcelのIP台帳が限界に近づいている情シスと、自宅ラボのサブネットを整理したい人です。執筆時点の最新版NetBox 4.7.1(netbox-dockerイメージ5.1.1)をDocker Composeで起動し、1拠点・2VLAN・2サブネット・スイッチ1台・サーバー1台の小さなオフィスを登録して、REST APIで次の空きIPを払い出すところまで進めます。出力と所要時間は、4 vCPUの検証環境で実際に動かしたときの値です。

この記事でわかること

  • Excel台帳・phpIPAM・NetBoxの判断表と、NetBoxが過剰になる条件
  • NetBox 4.7.1をPostgreSQL 16・Redis 7・バックグラウンドワーカーと一緒に動かす compose.yaml と、各設定を選んだ理由
  • 初回起動に約8分かかった内訳と、ヘルスチェックの start_period を600秒にした根拠
  • 拠点→VLAN→プレフィックス→機器→IPの順に登録するスクリプトと、available-ips で空きIPを払い出す方法
  • 重複IP・予約レンジ・VLAN IDの重複がどこまで弾かれるか(実際のエラーメッセージつき)

NetBoxとは:台帳であって監視ツールではない

NetBoxが扱うデータは大きく2系統です。IPAM(IP Address Management)はプレフィックス(10.10.10.0/24 のようなサブネット)、その中のIPアドレス、VLAN、VRFを扱い、DCIM(Data Center Infrastructure Management)は拠点、ラック、機器、インターフェース、ケーブルを扱います。両者はつながっていて、IPアドレスは機器のインターフェースに割り当て、プレフィックスは拠点やVLANに紐づけます。「このIPは何に使われているか」をたどると、サブネット→IP→インターフェース→機器→拠点まで一本の線でつながるのが台帳としての価値です。

できないことも先に書いておきます。pingやSNMPによるネットワークの走査はしません。入っているのは人やスクリプトが登録した内容だけで、実機と食い違っていてもNetBoxは気づきません。DHCPサーバーやDNSサーバーの機能もないため、台帳の内容をそれらに反映するにはAPIを読む仕組み(シェルスクリプト、Ansibleなど)を別に用意します。実機の状態を見張るのは監視ツール(例:PrometheusとGrafana)の担当で、NetBoxは「設計上こうなっているはず」を持つ側、と役割を分けて考えると導入後に迷いません。

動作要件は執筆時点でPostgreSQL 15以上、Redis 6.0以上です。4.7でPostgreSQL 14とRedis 5.xのサポートが終わり、階層構造のデータにPostgreSQLの ltree 拡張を使うようになりました。拡張はNetBoxのDBユーザーがデータベースに対する CREATE 権限を持っていれば自動で入るため、公式postgresイメージで POSTGRES_USER をそのままNetBox用ユーザーにする構成なら追加作業はありません。本体はApache License 2.0で公開されています。開発元のNetBox Labsが有償のクラウド版なども提供していますが、価格と契約条件は公式サイトで確認してください。

Excel台帳・phpIPAM・NetBoxの判断表

選ぶ基準は機能の多さではなく、「台帳が壊れる原因をどこまで機械が防いでくれるか」です。Excel台帳が壊れる典型は、行のコピーで重複IPが生まれる、変更した人と日時が残らない、機器側の情報と別ファイルになって更新が片方だけになる、の3つです。下の表はこれに自動化と運用の手間を足して並べています。

観点Excel/共有スプレッドシートphpIPAMNetBox 4.7
重複・範囲外の入力条件付き書式や数式を自作する。行コピーで簡単にすり抜けるサブネット内の重複を拒否する既定で重複IP・重複プレフィックスを拒否(ENFORCE_GLOBAL_UNIQUE の既定値がTrue)。予約レンジ内の登録やVLAN ID 4095も400エラー
変更履歴版の履歴を掘る。誰がどのセルを変えたかは追いにくい変更ログがある全オブジェクトに変更前後の値つき履歴。既定の保持は90日(CHANGELOG_RETENTION)
空きIPの払い出し目視で探す画面とAPIで空きを探せるavailable-ips が排他ロックを取って次の空きを確保して返す
機器・ポートとの紐づけ列を増やして手入力機器・ラックの管理はあるが、インターフェース単位の配線モデルは持たない(執筆時点)インターフェース、ケーブル、ラック搭載位置、電源まで台帳にできる
実機の走査(ping等)なしサブネットのスキャン機能があるなし(外部ツールで補う)
導入と維持不要PHPとMySQL/MariaDBの構成本体+ワーカー+PostgreSQL+Redis。検証環境の実測で合計約0.9GBのメモリ

phpIPAMは今回の検証環境では動かしていないので、表の機能は導入前に公式ドキュメントで確かめてください。「IPアドレスだけを管理したい」「スキャンで実際に使われているIPを拾いたい」「サーバーのメモリに余裕がない」がそろうなら、phpIPAMのほうが軽く済みます。

逆にNetBoxが過剰なのは、/24が1〜2個、担当者が1人、機器の入れ替えが年に数回、自動化の予定もない環境です。この条件ならExcelで足ります。ただし1行1IPとし、列は「アドレス/ホスト名/用途/担当者/最終更新日」を固定して、サブネットごとにシートを分けないでください(分けるとCOUNTIFによる重複チェックがシートをまたげません)。一方、拠点が2つ以上かVLANが5本以上ある、監査や障害対応で「誰がいつ変えたか」を聞かれる、スクリプトから空きIPを取りたい、機器のどのポートに何がつながるかを残したい、のどれか1つに当てはまった時点でNetBoxへ移る価値があります。

DockerでNetBoxを動かすcompose.yaml

netbox-dockerプロジェクトが配布するイメージ netboxcommunity/netbox を使い、compose.yamlは自分で書きます。タグは v4.7.1-5.1.1(NetBoxの版-イメージの版)のように固定します。latest はイメージを取り直したときに次の版へ進み、起動時にDBのマイグレーションまで自動で走るので、何年も使う台帳には向きません。作業ディレクトリは ~/netbox とし、Docker Composeの基本はDocker Compose入門の内容を前提にします。

compose.yaml
services:
  netbox:
    image: netboxcommunity/netbox:v4.7.1-5.1.1
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment: &netbox-env
      DB_HOST: postgres
      DB_NAME: netbox
      DB_USER: netbox
      DB_PASSWORD: ${DB_PASSWORD}
      REDIS_HOST: redis
      REDIS_PASSWORD: ${REDIS_PASSWORD}
      REDIS_DATABASE: "0"
      REDIS_CACHE_HOST: redis
      REDIS_CACHE_PASSWORD: ${REDIS_PASSWORD}
      REDIS_CACHE_DATABASE: "1"
      SECRET_KEY: ${SECRET_KEY}
      API_TOKEN_PEPPER_1: ${API_TOKEN_PEPPER_1}
      SKIP_SUPERUSER: "false"
      SUPERUSER_NAME: admin
      SUPERUSER_EMAIL: [email protected]
      SUPERUSER_PASSWORD: ${SUPERUSER_PASSWORD}
      SUPERUSER_API_KEY: ${SUPERUSER_API_KEY}
      SUPERUSER_API_TOKEN: ${SUPERUSER_API_TOKEN}
      TIME_ZONE: Asia/Tokyo
      GRANIAN_WORKERS: "2"
      CENSUS_REPORTING_ENABLED: "false"
    ports:
      - "127.0.0.1:18000:8080"
    volumes:
      - netbox-media:/opt/netbox/netbox/media
      - netbox-reports:/opt/netbox/netbox/reports
      - netbox-scripts:/opt/netbox/netbox/scripts
    mem_limit: 900m
    restart: unless-stopped
    healthcheck:
      test: curl -f http://localhost:8080/login/ || exit 1
      start_period: 600s
      interval: 30s
      timeout: 3s
      retries: 5

  netbox-worker:
    image: netboxcommunity/netbox:v4.7.1-5.1.1
    command: /opt/netbox/venv/bin/python /opt/netbox/netbox/manage.py rqworker
    depends_on:
      netbox:
        condition: service_healthy
    environment: *netbox-env
    mem_limit: 400m
    restart: unless-stopped

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: netbox
      POSTGRES_USER: netbox
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    mem_limit: 200m
    restart: unless-stopped
    healthcheck:
      test: pg_isready -U netbox -d netbox
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    command: redis-server --requirepass ${REDIS_PASSWORD} --save 60 1 --appendonly no
    volumes:
      - redis-data:/data
    mem_limit: 100m
    restart: unless-stopped
    healthcheck:
      test: redis-cli -a ${REDIS_PASSWORD} --no-auth-warning ping | grep -q PONG
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  postgres-data:
  redis-data:
  netbox-media:
  netbox-reports:
  netbox-scripts:

パスワードや鍵は同じディレクトリの .env に置き、Composeの変数展開で渡します。SECRET_KEY と API_TOKEN_PEPPER_1 はどちらも50文字以上でないと起動時の検証で落ちるので、64文字になる token_urlsafe(48) で作ります。SUPERUSER_API_KEY(12文字)と SUPERUSER_API_TOKEN(40文字)は、NetBoxが自分でトークンを作るときと同じ長さと文字種(英数字)にそろえています。

terminal
mkdir -p ~/netbox && cd ~/netbox
umask 077
cat > .env <<EOF
DB_PASSWORD=$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')
REDIS_PASSWORD=$(python3 -c 'import secrets; print(secrets.token_urlsafe(24))')
SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_urlsafe(48))')
API_TOKEN_PEPPER_1=$(python3 -c 'import secrets; print(secrets.token_urlsafe(48))')
SUPERUSER_PASSWORD=$(python3 -c 'import secrets; print(secrets.token_urlsafe(18))')
SUPERUSER_API_KEY=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 12)
SUPERUSER_API_TOKEN=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 40)
EOF
ls -l .env    # -rw------- になっていれば umask が効いている

イメージ内の /etc/netbox/config/configuration.py が環境変数を読んでNetBoxの設定に変換する仕組みなので、効くのはこのファイルが読む変数名だけです。値ごとに理由を書いておきます。

  • DB_HOST / REDIS_HOST:Composeの内部DNSがサービス名をコンテナのIPに解決するので、IPアドレスではなくサービス名を書きます。
  • REDIS_DATABASE: "0" と REDIS_CACHE_DATABASE: "1":NetBoxはRedisをジョブのキューとキャッシュの2用途に使います。1台のRedisでもDB番号を分けておけば、キャッシュだけを FLUSHDB で消してもキューに積まれたジョブは残ります。Redis側の --save 60 1 は、60秒の間に1件でも変更があればディスクに書き出す設定で、再起動時にキューの中身を失いにくくします。
  • SECRET_KEY:セッションとCSRFトークンの署名に使う値です。変えると全ユーザーがログアウトされます。
  • API_TOKEN_PEPPER_1:4.5で入った新形式のAPIトークン(v2)は、トークン本体ではなく「トークン+この値」から計算したHMAC-SHA256の値だけをDBに保存します。未設定でもNetBoxは起動しますが、v2トークンを1つも作れません。執筆時点のイメージが環境変数から読むのは1番だけで、鍵を入れ替えるために2番以降を足すときは追加の設定ファイルが必要です。
  • SKIP_SUPERUSER: "false" と SUPERUSER_*:初回起動時に管理者とそのAPIトークンを作ります。管理者が既にいれば何もしません。ここで作る管理者にはパスワードの強度チェックがかかりませんが、あとで画面から変えるときは12文字以上で数字・大文字・小文字を各1文字以上含む必要があります。
  • GRANIAN_WORKERS: "2":4.7のイメージはアプリケーションサーバーがgranianで、既定は4ワーカーです。4ワーカーではコンテナが約827MiBを使い、2に減らすと525〜575MiBでした。数人で使う台帳なら2で足ります。
  • CENSUS_REPORTING_ENABLED: "false":既定では日次の保守ジョブが、NetBoxの版・Pythonの版・SECRET_KEY から計算したIDを開発元に送ります。外向きの通信を増やしたくない社内環境では切ります。インターネットに出られない環境なら ISOLATED_DEPLOYMENT: "true" を足すと、この送信に加えてインターネット前提の機能(プラグインカタログの取得など)もまとめて止まります。
  • ports: "127.0.0.1:18000:8080":LANには直接公開しません。社内から使うときはCaddyなどのリバースプロキシでTLSを終端し、そのホスト名を ALLOWED_HOSTS に入れます。イメージの既定値は *(すべて許可)なので、公開するときは絞ります。
  • environment: &netbox-env と *netbox-env:YAMLのアンカーで、ワーカーに本体と同じ環境変数を渡しています。片方だけ直して接続先がずれる事故を防げます。
  • netbox-worker の command と depends_on:command を指定するとイメージ既定の起動スクリプト(DB待ち、マイグレーション、管理者作成)を通らずに rqworker だけが起動します。マイグレーションは本体の起動時にしか走らないので、本体がhealthyになるまでワーカーを待たせ、テーブルがそろう前に動き出さないようにしています。

入れていない設定もあります。DEBUG: "true" はエラー画面に設定値やスタックトレースを出すので使いません。LOGIN_REQUIRED: "false" は未ログインでも閲覧できるようにする設定で、台帳にはホスト名や用途が並ぶため社内でも有効にしません。起動のたびに FutureWarning: LOGIN_REQUIRED is deprecated という警告が出ますが、これはイメージ側の設定ファイルがこの変数を読んでいるためで、動作には影響しません。

IPの登録・検索・APIだけならワーカーなしでも動きます(検索インデックスは保存時に更新され、ワーカーがない状態で登録したIPもインデックスに入っていました)。それでもワーカーを入れているのは、変更履歴の90日超の削除や期限切れセッションの掃除を行う日次の「System Housekeeping」ジョブがワーカーでしか実行されないからです。ワーカーなしで機器を登録したときに積まれた「Render config context」ジョブは、ワーカーを起動した瞬間に処理されました。ワーカーのメモリは実測で約250MiBです。

初回起動は約8分:マイグレーションとstart_periodの関係

起動して、本体のログで進み具合を見ます。空のDBに対して336件のマイグレーションを順に当てるので、初回だけ時間がかかります。

terminal
docker compose up -d
docker compose logs --timestamps netbox | grep -E '⚙️|✅|💡|Listening'
出力(初回起動)
netbox-1  | 2026-09-24T06:47:47.589550762Z ⚙️ Applying database migrations
netbox-1  | 2026-09-24T06:54:24.094782911Z ⚙️ Running trace_paths
netbox-1  | 2026-09-24T06:54:33.626074279Z ⚙️ Removing stale content types
netbox-1  | 2026-09-24T06:54:41.668211710Z ⚙️ Removing expired user sessions
netbox-1  | 2026-09-24T06:54:49.541804837Z ⚙️ Building search index (lazy)
netbox-1  | 2026-09-24T06:55:03.684126671Z 💡 Superuser Username: admin, E-Mail: [email protected],
netbox-1  | 2026-09-24T06:55:03.684129121Z 💡 API Token: use with 'Bearer nbt_vvzv06g5h3Wg.<Your token>'
netbox-1  | 2026-09-24T06:55:04.724223884Z ✅ Initialisation is done.
netbox-1  | 2026-09-24T06:55:04.914427057Z [INFO] Listening at: http://:::8080

ログの時刻はUTCです。コンテナの起動は06:47:28で、マイグレーションだけで6分37秒、ログイン画面が200を返すまで約7分40秒かかりました。環境は4 vCPU(AMD EPYC 7713)で、同じホストで他のコンテナも動いていてロードアベレージは3.6前後でした。1回目の試行(granianワーカー4)では、マイグレーション7分29秒、ログイン画面まで約8分40秒です。2回目以降の起動は manage.py migrate --check が通るのでマイグレーションが丸ごと飛ばされ、docker compose restart netbox からログイン画面まで35秒でした。

起動の種類マイグレーションログイン画面が200を返すまで
初回(granianワーカー4)7分29秒約8分40秒
初回(granianワーカー2)6分37秒約7分40秒
2回目以降(restart)なし35秒

start_period: 600s はこの実測から決めました。1回目は start_period: 120s、interval: 15s で試したところ、マイグレーションの途中で猶予が切れて Up 8 minutes (unhealthy) になりました。Dockerはunhealthyのコンテナを自動で再起動しませんが、condition: service_healthy を条件にしているワーカーなどは起動されないままになります。600秒の猶予が切れたあとは30秒×5回の失敗でunhealthyになるので、初回起動に最大で約12分半まで耐えられます。

初回のマイグレーション中は manage.py migrate のプロセスだけで約820MBを使い、コンテナ全体では900MiBの上限に対して約850MBまで上がりました。mem_limit を小さくしたいときは、初回起動が終わってから下げてください。起動後の常用時は、本体(granianワーカー2)が525〜575MiB、ワーカーが約250MiB、PostgreSQLが50〜80MiB、Redisが約5MiBでした。

NetBoxの使い方:台帳に登録する順番

ブラウザで http://127.0.0.1:18000/ を開き、admin と .env の SUPERUSER_PASSWORD でログインします。ブラウザの言語が日本語なら、メニューやボタンは日本語で表示されます(「組織」「デバイス」「IPAM」など)。一部に「Hide Depth Indicators」のような英語が残ります。

登録する順番は、オブジェクト同士の参照関係で決まります。IPアドレスはインターフェースに、インターフェースは機器に割り当て、機器は拠点・機種・役割を必須項目として持ちます。逆順に作ろうとすると、たとえば役割を省いた機器の登録は {"role":["This field is required."]} で拒否されます。今回登録する設計は次のとおりです(例)。

対象値(例)NetBoxでの登録先
拠点名古屋オフィス組織 → サイト
VLAN10 servers/20 clients(VLANグループ nagoya-office)IPAM → VLAN
拠点全体10.10.0.0/16プレフィックス(ステータス:コンテナ)
サーバー用10.10.10.0/24プレフィックス(VLAN 10)
クライアント用10.10.20.0/24プレフィックス(VLAN 20)
ネットワーク機器用の予約10.10.10.2〜10.10.10.9IPアドレス範囲(予約済)
DHCPプール10.10.20.100〜10.10.20.199IPアドレス範囲
機器sw-01(L3スイッチ)、srv-01(サーバー)デバイス
ゲートウェイ10.10.10.1、10.10.20.1sw-01のVlan10/Vlan20に割り当て
サーバー10.10.10.10srv-01のeth0に割り当て

/16をステータス「コンテナ」で作るのは、IPを直接置かない入れ物であることを示すためです。コンテナの使用率は配下の/24の合計から計算され、この例では0.8%(512/65,536)と表示されました。VLANの設計そのものはVLANでネットワークを分離する手順で扱っています。画面から1件ずつ入れても構いませんが、10件を超えるならAPIでまとめて入れたほうが入力ミスが減ります。

APIトークン(v2)を発行する

4.7のAPIトークンは nbt_<キー>.<トークン> という形で、Authorization: Bearer ヘッダーで送ります。.env の値を使うと、初回起動で作られた管理者のトークンは nbt_${SUPERUSER_API_KEY}.${SUPERUSER_API_TOKEN} です。スクリプト用には別のトークンを用意し、管理者のものは初期構築だけに使うのがおすすめです。ユーザー名とパスワードからトークンを発行するエンドポイントがあるので、パスワードをコマンド履歴に残さないよう read -s で受け取って渡します。

terminal
read -rs NB_PASS
jq -n --arg u admin --arg p "$NB_PASS" '{username: $u, password: $p, description: "seed script"}' \
  | curl -s -X POST http://127.0.0.1:18000/api/users/tokens/provision/ \
      -H 'Content-Type: application/json' -H 'Accept: application/json' --data @- \
  | jq '{version, key, token, write_enabled, expires}'
出力(tokenの値は伏せています)
{
  "version": 2,
  "key": "mxTlUJ0X7mMZ",
  "token": "(英数字40文字)",
  "write_enabled": true,
  "expires": null
}

token の平文が返るのはこの応答だけで、あとで /api/users/tokens/ を見ても "token": null になっています。DBにはハッシュしか残らないためで、控え忘れたら作り直すしかありません。パスワードを間違えると {"detail":"Invalid username/password"}(HTTP 403)です。DHCPサーバーの設定を台帳から生成するような読み取り専用の用途には、write_enabled をfalseにしたトークンを使います。実際に読み取り専用トークンで空きIPの払い出し(POST)を試すと、{"detail":"You do not have permission to perform this action."} で拒否され、同じトークンでのGETは通りました。

REST APIで小さなオフィスをまとめて登録する

先ほどの表をそのまま登録するスクリプトです。curl と jq を使います。post 関数は作成されたオブジェクトのIDだけを返し、NetBoxが400などで拒否したら応答本文を表示してそこで止まります。途中で止まれば、どの入力が悪いかがNetBoxのメッセージでそのまま分かります。

seed.sh
#!/usr/bin/env bash
# 小さなオフィス(1拠点・2VLAN・2サブネット・機器2台)をNetBox 4.7のREST APIで登録する
# 使い方: NETBOX_TOKEN='nbt_<key>.<token>' ./seed.sh
set -euo pipefail
NB=${NETBOX_URL:-http://127.0.0.1:18000}/api
: "${NETBOX_TOKEN:?NETBOX_TOKEN を設定してください}"
HDR=(-H "Authorization: Bearer $NETBOX_TOKEN" -H 'Content-Type: application/json' -H 'Accept: application/json')

# POSTして、作られたオブジェクトのidだけを出力する。失敗したら応答本文を出して止まる
post() {
  local out
  out=$(curl -sS -X POST "$NB$1" "${HDR[@]}" --data "$2")
  jq -e '.id' <<<"$out" 2>/dev/null || { echo "POST $1 failed: $out" >&2; return 1; }
}
patch() {
  curl -sS -o /dev/null --fail-with-body -X PATCH "$NB$1" "${HDR[@]}" --data "$2"
}

# 拠点とVLANグループ(VLAN IDの重複チェックはグループ単位)
SITE=$(post /dcim/sites/ '{"name":"名古屋オフィス","slug":"nagoya-office","status":"active"}')
VG=$(post /ipam/vlan-groups/ "{\"name\":\"nagoya-office\",\"slug\":\"nagoya-office\",\"scope_type\":\"dcim.site\",\"scope_id\":$SITE}")
R_SRV=$(post /ipam/roles/ '{"name":"サーバー","slug":"servers"}')
R_CLI=$(post /ipam/roles/ '{"name":"クライアント","slug":"clients"}')
V10=$(post /ipam/vlans/ "{\"vid\":10,\"name\":\"servers\",\"site\":$SITE,\"group\":$VG,\"role\":$R_SRV,\"status\":\"active\"}")
V20=$(post /ipam/vlans/ "{\"vid\":20,\"name\":\"clients\",\"site\":$SITE,\"group\":$VG,\"role\":$R_CLI,\"status\":\"active\"}")

# プレフィックス: /16をコンテナにして、その下に/24を2つ
post /ipam/prefixes/ "{\"prefix\":\"10.10.0.0/16\",\"status\":\"container\",\"scope_type\":\"dcim.site\",\"scope_id\":$SITE,\"description\":\"名古屋オフィス 全体\"}" >/dev/null
P_SRV=$(post /ipam/prefixes/ "{\"prefix\":\"10.10.10.0/24\",\"status\":\"active\",\"scope_type\":\"dcim.site\",\"scope_id\":$SITE,\"vlan\":$V10,\"role\":$R_SRV,\"description\":\"サーバーセグメント\"}")
P_CLI=$(post /ipam/prefixes/ "{\"prefix\":\"10.10.20.0/24\",\"status\":\"active\",\"scope_type\":\"dcim.site\",\"scope_id\":$SITE,\"vlan\":$V20,\"role\":$R_CLI,\"description\":\"クライアントセグメント(DHCP)\"}")

# IPレンジ: mark_populated=true で範囲内の個別登録と払い出しを止める
post /ipam/ip-ranges/ '{"start_address":"10.10.10.2/24","end_address":"10.10.10.9/24","status":"reserved","mark_populated":true,"description":"ネットワーク機器用に予約"}' >/dev/null
# DHCPプール: mark_utilized=true で使用率の計算上は全部使用中として数える
post /ipam/ip-ranges/ '{"start_address":"10.10.20.100/24","end_address":"10.10.20.199/24","status":"active","mark_populated":true,"mark_utilized":true,"description":"DHCPプール"}' >/dev/null

# 機器: メーカー → 機種 → 役割 → 機器 → インターフェース の順
MFR=$(post /dcim/manufacturers/ '{"name":"Generic","slug":"generic"}')
DT_SW=$(post /dcim/device-types/ "{\"manufacturer\":$MFR,\"model\":\"L3 switch 24-port\",\"slug\":\"l3-switch-24\",\"u_height\":1}")
DT_SRV=$(post /dcim/device-types/ "{\"manufacturer\":$MFR,\"model\":\"1U server\",\"slug\":\"server-1u\",\"u_height\":1}")
DR_SW=$(post /dcim/device-roles/ '{"name":"コアスイッチ","slug":"core-switch","color":"2196f3"}')
DR_SRV=$(post /dcim/device-roles/ '{"name":"サーバー","slug":"server","color":"4caf50"}')
SW=$(post /dcim/devices/ "{\"name\":\"sw-01\",\"device_type\":$DT_SW,\"role\":$DR_SW,\"site\":$SITE,\"status\":\"active\"}")
SRV=$(post /dcim/devices/ "{\"name\":\"srv-01\",\"device_type\":$DT_SRV,\"role\":$DR_SRV,\"site\":$SITE,\"status\":\"active\"}")
IF_V10=$(post /dcim/interfaces/ "{\"device\":$SW,\"name\":\"Vlan10\",\"type\":\"virtual\"}")
IF_V20=$(post /dcim/interfaces/ "{\"device\":$SW,\"name\":\"Vlan20\",\"type\":\"virtual\"}")
post /dcim/interfaces/ "{\"device\":$SW,\"name\":\"Gi1/0/1\",\"type\":\"1000base-t\",\"mode\":\"access\",\"untagged_vlan\":$V10,\"description\":\"to srv-01 eth0\"}" >/dev/null
IF_ETH0=$(post /dcim/interfaces/ "{\"device\":$SRV,\"name\":\"eth0\",\"type\":\"1000base-t\"}")

# IPアドレスをインターフェースに割り当て、機器の代表IP(primary_ip4)にする
GW10=$(post /ipam/ip-addresses/ "{\"address\":\"10.10.10.1/24\",\"status\":\"active\",\"assigned_object_type\":\"dcim.interface\",\"assigned_object_id\":$IF_V10,\"description\":\"ゲートウェイ\"}")
post /ipam/ip-addresses/ "{\"address\":\"10.10.20.1/24\",\"status\":\"active\",\"assigned_object_type\":\"dcim.interface\",\"assigned_object_id\":$IF_V20,\"description\":\"ゲートウェイ\"}" >/dev/null
IP_SRV=$(post /ipam/ip-addresses/ "{\"address\":\"10.10.10.10/24\",\"status\":\"active\",\"assigned_object_type\":\"dcim.interface\",\"assigned_object_id\":$IF_ETH0,\"dns_name\":\"srv-01.office.internal\",\"description\":\"ファイルサーバー\"}")
patch /dcim/devices/$SW/ "{\"primary_ip4\":$GW10}"
patch /dcim/devices/$SRV/ "{\"primary_ip4\":$IP_SRV}"

echo "site=$SITE prefix_servers=$P_SRV prefix_clients=$P_CLI switch=$SW server=$SRV"
terminal
chmod +x seed.sh
export NETBOX_TOKEN='nbt_<キー>.<トークン>'
./seed.sh
./seed.sh    # 2回目はわざと実行して、止まり方を確認する
出力
site=2 prefix_servers=5 prefix_clients=6 switch=3 server=4
POST /dcim/sites/ failed: {"name":["site with this name already exists."],"slug":["site with this slug already exists."]}

1回目は2.8秒で終わりました。表示されるIDは環境によって変わります(この出力は一度データを消してから流し直したときのものです)。2回目は最初の拠点の作成で止まり、以降は何も作られません。ただしトランザクションではないので、途中の10件目で失敗すれば9件目までは残ります。その状態で再実行すると最初の拠点で止まるため、重複はしない代わりに続きも作られません。流し直すときは、作成済みの分を画面で消すか、失敗した行から後ろだけを実行します。3.x向けの古いサンプルを流用するときは、プレフィックスの site が4.2で scope_type/scope_id に、機器の device_role が4.0で廃止されて role だけになった点に注意してください。

次の空きIPを払い出す:available-ips

ここからがExcelでは難しい部分です。プレフィックスごとに /available-ips/ というエンドポイントがあり、GETで空きの一覧、POSTで次の空きを確保して登録します。

terminal
NB=http://127.0.0.1:18000/api
H=(-H "Authorization: Bearer $NETBOX_TOKEN" -H 'Content-Type: application/json' -H 'Accept: application/json')
PFX=$(curl -s "${H[@]}" "$NB/ipam/prefixes/?prefix=10.10.10.0/24" | jq '.results[0].id')

# 空きを3つ見るだけ(何も確保しない)
curl -s "${H[@]}" "$NB/ipam/prefixes/$PFX/available-ips/?limit=3" | jq -r '.[].address'

# 次の空きを1つ確保して、NASとして登録する
curl -s "${H[@]}" -X POST "$NB/ipam/prefixes/$PFX/available-ips/" \
  --data '{"status":"active","dns_name":"nas-01.office.internal","description":"NAS"}' \
  | jq '{id, address, status: .status.value, dns_name}'

# クライアント側でプリンター2台分をまとめて確保する(配列で送る)
PCLI=$(curl -s "${H[@]}" "$NB/ipam/prefixes/?prefix=10.10.20.0/24" | jq '.results[0].id')
curl -s "${H[@]}" -X POST "$NB/ipam/prefixes/$PCLI/available-ips/" \
  --data '[{"description":"printer-01"},{"description":"printer-02"}]' \
  | jq -r '.[] | "\(.address) \(.description)"'
出力
10.10.10.11/24
10.10.10.12/24
10.10.10.13/24
{
  "id": 15,
  "address": "10.10.10.11/24",
  "status": "active",
  "dns_name": "nas-01.office.internal"
}
10.10.20.2/24 printer-01
10.10.20.3/24 printer-02

サーバー側の最初の空きが .2 ではなく .11 なのは、.1 がゲートウェイ、.2〜.9 が予約レンジ、.10 がsrv-01で埋まっているからです。複数の担当者やスクリプトが同時に払い出しても重ならないよう、NetBoxはこの処理をPostgreSQLのアドバイザリロック(netbox/constants.py で available-ips に割り当てられたキー100200)で直列化しています。実際に5本のPOSTを同時に投げて確かめました。

terminal
for i in 1 2 3 4 5; do
  curl -s "${H[@]}" -X POST "$NB/ipam/prefixes/$PFX/available-ips/" \
    --data "{\"status\":\"reserved\",\"description\":\"parallel-$i\"}" | jq -r .address &
done; wait

# 試験用に作った5件を一括削除する
curl -s "${H[@]}" "$NB/ipam/ip-addresses/?description__ic=parallel" | jq -c '[.results[] | {id}]' > ids.json
curl -s -o /dev/null -w '%{http_code}\n' "${H[@]}" -X DELETE "$NB/ipam/ip-addresses/" --data @ids.json
出力
10.10.10.12/24
10.10.10.13/24
10.10.10.14/24
10.10.10.15/24
10.10.10.16/24
204

5本とも別々のアドレスが返り、重複はありませんでした。Excelで同じことをすると、2人が同時に「次の空き」を見て同じ行を使う事故が起きます。一括削除は {"id": …} の配列を本文にしたDELETEで、応答は204です。

IPアドレス範囲の2つのフラグは、払い出しと使用率の計算を別々に制御します。mark_populated をtrueにした範囲は、全アドレスが埋まっているものとして扱われ、空きの計算から外れ、範囲内への個別登録も拒否されます。mark_utilized をtrueにした範囲は、使用率の計算で全部使用中として数えます。プレフィックス一覧の使用率は、予約レンジ(mark_populated だけtrue)を持つ10.10.10.0/24が1.2%(3/254)、DHCPプール(両方true)を持つ10.10.20.0/24が40.6%(プール100+ゲートウェイ1+プリンター2で103/254)でした。判断基準は、DHCPプールは両方true、将来用に空けておく予約は mark_populated だけtrue、ラベルとして範囲を書いておきたいだけなら両方falseです。

重複と入力ミスはどこまで弾かれるか

台帳の信頼性は「間違った入力をどれだけ拒否するか」で決まるので、よくあるミスを実際にAPIで投げて応答を確かめました。VRFを使っていない状態(NetBoxの表示では「Global」)での結果です。

試した操作結果NetBoxの応答
登録済みの10.10.10.10/24をもう一度登録400Duplicate IP address found in global table: 10.10.10.10/24
同じアドレスを10.10.10.10/32で登録400上と同じ。マスク長が違っても同じホストとして検出
予約レンジ内の10.10.10.5/24を登録400Cannot create IP address 10.10.10.5/24 inside range 10.10.10.2-9/24.
10.10.10.0/24をもう一度登録400Duplicate prefix found in global table: 10.10.10.0/24
DHCPプールと重なる .150〜.160 のレンジを登録400Defined addresses overlap with range 10.10.20.100-199/24 in VRF None
VLAN ID 4095を登録400Ensure this value is less than or equal to 4094.
VLANグループなしで、同じ拠点にVID 10をもう1つ登録201作成されてしまう
VLANグループ内でVID 10をもう1つ登録400VLAN with this Group and VLAN ID already exists.

IPとプレフィックスの重複は、ENFORCE_GLOBAL_UNIQUE の既定値がTrueなので何も設定しなくても拒否されます。拠点ごとにVRFを分けて同じアドレス帯を使う設計にした場合は、VRFごとの enforce_unique(日本語画面では「重複を禁止する」、既定はオン)で重複の扱いが決まります。見落としやすいのがVLANで、VIDの重複を検査する単位はVLANグループです。拠点にだけ紐づけたVLANは同じVIDをいくつでも作れてしまうので、VLANを作る前にスイッチのL2ドメインごとにVLANグループを作ってください。seed.shもその順番にしてあります。

バックアップと復元の確認

台帳の中身はPostgreSQLにあります。pg_dump のカスタム形式(-Fc)で取り、別名のDBに戻して件数を数えるところまでを1セットにしておくと、「取れているつもり」のバックアップを防げます。

terminal
docker compose exec -T postgres pg_dump -U netbox -d netbox -Fc > netbox-$(date +%F).dump
ls -lh netbox-*.dump

# 復元テスト:別名のDBに戻して件数を数え、終わったら消す
docker compose exec -T postgres createdb -U netbox netbox_restore_test
docker compose exec -T postgres pg_restore -U netbox -d netbox_restore_test --no-owner < netbox-$(date +%F).dump
docker compose exec -T postgres psql -U netbox -d netbox_restore_test -Atc "select count(*) from ipam_ipaddress"
docker compose exec -T postgres dropdb -U netbox netbox_restore_test

この記事のデータ量ではダンプは1.2MBで、復元先のIPアドレスは6件(ゲートウェイ2、srv-01、nas-01、プリンター2)と元のDBに一致しました。ダンプとは別に .env も必ず保管します。API_TOKEN_PEPPER_1 を失うと、DBを戻してもv2トークンのハッシュを照合できず、全トークンの再発行になります。機器の写真などを添付する運用なら、netbox-media ボリュームもバックアップ対象に加えます。

イメージの版を上げるときは、先にダンプを取ってからタグを書き換えます。起動時に migrate --check が未適用のマイグレーションを見つけると自動で適用するため、新しい版で一度起動したDBは新しいスキーマに変わっています。古いイメージに戻しても動く保証はないので、戻すときはダンプからの復元になります。

つまずきポイント

「Waiting on DB...」が続き、30秒で止まる

イメージの起動スクリプトは manage.py showmigrations が成功するまでを「DB待ち」として扱うため、DBと関係のない設定エラーでも同じ表示になります。SECRET_KEY を短くして起動したときのログは次のとおりで、最後の行はDBの話ではなく鍵の作り方のヒントです。

SECRET_KEYが50文字未満のとき
  python /opt/netbox/netbox/generate_secret_key.py
[ Use DB_WAIT_DEBUG=1 in netbox.env to print full traceback for errors here ]
⏳ Waiting on DB... (27s / 30s)
❌ Waited 30s or more for the DB to become ready.

起動スクリプトはエラーの最終行しか表示しません。SECRET_KEY のエラーは2行に分かれていて、最終行が「鍵の生成コマンド」のほうなので、原因が見えなくなります。環境変数 DB_WAIT_DEBUG=1 を足すとトレースバック全体が出て、ImproperlyConfigured: SECRET_KEY must be at least 50 characters in length. が原因だと分かります。API_TOKEN_PEPPER_1 が短い場合は1行のエラーなので、Invalid pepper 1: Pepper must be at least 50 characters in length. がそのまま待機表示の前に出ます。本当にDBのパスワードが違う場合は、待機の直前に FATAL: password authentication failed for user "netbox" が出ます。公式postgresイメージの POSTGRES_PASSWORD はデータディレクトリが空のときにしか使われないので、初回起動後に .env の DB_PASSWORD だけ変えるとこの状態になります。構築直後なら docker compose down -v で作り直し、データがあるなら ALTER USER netbox PASSWORD '…'; でDB側を合わせます。

起動途中で (unhealthy) になる

初回のマイグレーション中にヘルスチェックの猶予が切れた状態です。docker compose ps で Up 8 minutes (unhealthy) のように見えます。ログに ⚙️ Applying database migrations が出たまま進んでいるなら、待てば ✅ Initialisation is done. のあとにhealthyへ戻ります(1回目の試行でもそうなりました)。unhealthyの間は、service_healthy を条件にしたワーカーが起動されていないことがあるので、healthyに戻ったら docker compose up -d をもう一度実行し、docker compose ps で netbox-worker が Up になったことを確かめます。根本的な対策は、start_period を初回起動の実測より長く(この記事では600秒)しておくことです。

管理者のAPIトークンが作られない、provisionが500を返す

API_TOKEN_PEPPER_1 を入れずに起動すると、起動ログに ⚠️ No API token was created as API_TOKEN_PEPPERS is not set が出て、トークン発行のAPIは次のように500を返します。

POST /api/users/tokens/provision/ の応答(HTTP 500)
{"error": "API_TOKEN_PEPPERS is not defined", "exception": "ValueError", "netbox_version": "4.7.1-Docker-5.1.1", "python_version": "3.14.4"}

ペッパーを足してコンテナを作り直せば発行できるようになりますが、管理者のトークンは自動では作られません。管理者の作成処理は「そのユーザーが既にいれば何もしない」作りなので、2回目以降の起動では SUPERUSER_API_TOKEN が読まれないからです。前の節の provision で発行し直してください。

ログイン画面が「Server Error」になる

コンテナは起動しているのに画面が500になる場合は、Redisを疑います。REDIS_HOST を存在しないホスト名にして試すと、エラー画面に redis.exceptions.ConnectionError と Error -3 connecting to redis-cache:6379. Temporary failure in name resolution. が表示されました。パスワードが違うときの例外は redis.exceptions.AuthenticationError: invalid username-password pair or user is disabled. でした(manage.py から接続して確認)。ログイン画面を返す処理の中でもRedisに接続するため、DBが正常でも最初の画面から失敗し、ヘルスチェック(/login/ へのcurl)も通らなくなります。compose.yamlでは REDIS_PASSWORD と REDIS_CACHE_PASSWORD に同じ変数を入れているので、.env を1か所直せば両方がそろいます。

動作確認

4つのサービスの状態、APIの応答、台帳の中身の順に確かめます。NETBOX_TOKEN には前の節で作ったトークンを入れておきます。

terminal
docker compose ps --format 'table {{.Service}}\t{{.Status}}'
curl -s -H "Authorization: Bearer $NETBOX_TOKEN" http://127.0.0.1:18000/api/status/ \
  | jq '{"netbox-version", "rq-workers-running"}'
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:18000/api/ipam/prefixes/
出力
SERVICE         STATUS
netbox          Up 12 minutes (healthy)
netbox-worker   Up 13 minutes
postgres        Up 5 hours (healthy)
redis           Up 5 hours (healthy)
{
  "netbox-version": "4.7.1",
  "rq-workers-running": 1
}
403

rq-workers-running が0ならワーカーが動いていません。トークンなしのリクエストが403になることで、未ログインでは台帳を読めない状態も確認できます。

続いて台帳の中身です。1つ目はサーバー用プレフィックスのIPを割り当て先の機器と一緒に一覧にし、2つ目はDNS名が入っているアドレスを /etc/hosts 形式で書き出します。2つ目の形にしておけば、台帳からDNSやDHCPの設定ファイルを生成する処理にそのままつなげられます。

terminal
curl -s -H "Authorization: Bearer $NETBOX_TOKEN" \
  "http://127.0.0.1:18000/api/ipam/ip-addresses/?parent=10.10.10.0/24" \
  | jq -r '.results[] | [.address, .status.value, (.assigned_object.device.name // "-"), (.dns_name | if . == "" then "-" else . end)] | @tsv'

curl -s -H "Authorization: Bearer $NETBOX_TOKEN" \
  "http://127.0.0.1:18000/api/ipam/ip-addresses/?parent=10.10.0.0/16&status=active&dns_name__empty=false&limit=0" \
  | jq -r '.results[] | "\(.address | split("/")[0])\t\(.dns_name)"'
出力
10.10.10.1/24	active	sw-01	-
10.10.10.10/24	active	srv-01	srv-01.office.internal
10.10.10.11/24	active	-	nas-01.office.internal
10.10.10.10	srv-01.office.internal
10.10.10.11	nas-01.office.internal

画面では「IPAM」→「プレフィックス」→「10.10.10.0/24」→「IP アドレス」タブで、ゲートウェイ、予約レンジ、srv-01、nas-01の下に「243 IP が利用可能」と表示されれば、ここまでの登録がすべて反映されています(使えるアドレス254から、ゲートウェイ1、予約8、サーバーとNASの2を引いた数です)。

よくある質問

NetBoxで何ができますか?

IPアドレス・プレフィックス・VLAN・VRFの管理(IPAM)と、拠点・ラック・機器・インターフェース・ケーブルの管理(DCIM)を1つのデータベースで行えます。すべての操作にREST APIがあり、変更履歴も残ります。監視、ネットワークの自動検出、DHCPやDNSの配布機能は持たないので、それらは別のツールと組み合わせます。

phpIPAMとNetBoxの違いは何ですか?

phpIPAMはIPアドレス管理が中心で、サブネットのスキャンで使用中のIPを拾えます。一方のNetBoxはスキャンを持たない代わりに、機器・インターフェース・配線までを台帳にし、IPをインターフェースに割り当てて関係をたどれます。IPだけを軽く管理したいならphpIPAM、機器構成や自動化の起点にしたいならNetBoxが向きます。

NetBoxでIPアドレス管理はできますか?

できます。プレフィックスの階層、予約やDHCPプールを表すIPアドレス範囲、重複の自動拒否、使用率の表示、APIによる次の空きIPの払い出しまでそろっています。ただしDHCPサーバーではないので、実際の配布は既存のDHCPサーバーが行い、NetBoxは設計と割り当ての記録を持つ形になります。

オープンソース版のNetBoxに費用はかかりますか?

オープンソース版はApache License 2.0で、ライセンス費用はかかりません。必要なのは動かすサーバーの費用と運用の手間で、この記事の構成ではメモリを約0.9GB使いました。開発元が提供する有償のクラウド版などを検討する場合は、価格と含まれる機能を公式サイトで確認してください(執筆時点)。

まとめ

NetBox 4.7.1はcompose.yamlと .env の2ファイルで起動でき、初回は約8分のマイグレーションを start_period: 600s で待てば、あとの再起動は35秒でした。登録は拠点→VLANグループ→VLAN→プレフィックス→機器→IPの順にし、空きIPは available-ips に任せれば同時に払い出しても重なりません。次にやることは、今使っているExcel台帳のうち1つのサブネットだけを seed.sh の形に書き換えて流し込み、「IP アドレス」タブの空き数がExcelの認識と一致するかを確かめることです。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。