kadet-hub logokadet-hub

NetBox on Kubernetes without Helm, with Kapitan

NetBox v4.7.4, the source of truth for network infrastructure and IP addresses, with its RQ worker, rendered by the kadet-hub kubernetes generator 1.14.0 from NetBox’s Helm chart 8.3.99. The included recipe postgresql is the database, valkey holds tasks and cache.

What it deploys

Required parameters

applications.netbox.superuser.name, applications.netbox.superuser.email and applications.netbox.superuser.password are the superuser’s login, address and password. The image creates the superuser on a start when no user of that name exists and leaves an existing one alone. A later change of the address or the password in the target does not reach NetBox, and a new name creates a second superuser.

applications.netbox.secret_key is NetBox’s SECRET_KEY, at least 50 characters, as openssl rand -hex 32 prints them. Another key ends every session.

applications.netbox.api_token_pepper is the pepper with ID 1 of API_TOKEN_PEPPERS, with which NetBox hashes v2 API tokens. It must be at least 50 characters. The recipe writes it into a JSON string without encoding, so use letters and digits only, as openssl rand -hex 32 prints them. Another pepper invalidates every v2 token hashed with the old one.

applications.valkey.password is the password of valkey.

Set the passwords, the key and the pepper to refs with the backend of your choice. Without one of these parameters the compile stops with Interpolation key '<key>' not found. postgresql requires no parameter.

Include

A target includes the recipe and its options as classes and can override anything the recipe sets:

classes:
  - recipes.netbox
parameters:
  namespace: netbox
  kapitan:
    vars:
      target: netbox
  applications:
    netbox:
      superuser:
        name: <login>
        email: <address>
        password: <ref>
      secret_key: <ref>
      api_token_pepper: <ref>
    valkey:
      password: <ref>

Fetching it

The quickstart goes through these steps on a kind cluster with the uptime-kuma recipe, from installing Kapitan to the running app.

A separate fetch inventory pins the recipe by version and digest. This is fetch/targets/recipes.yml for version 1.0.1:

parameters:
  kapitan:
    dependencies:
      - type: oci
        source: ghcr.io/kadet-hub/recipes/netbox:1.0.1@sha256:cf792f9704c9e8595385edcb883c72f4879e2dc79e979b5e61bd055af6dbfe59
        output_path: inventory/classes/recipes/netbox

Version 1.0.1 includes postgresql 1.1.1 and valkey 1.0.3. The consumer workflow adds the included recipes at the digests the index lists. Kapitan's --fetch reads no index, so for a local compile pull them by digest first:

tmp=$(mktemp -d)
oras pull -o "$tmp" ghcr.io/kadet-hub/recipes/postgresql@sha256:1e8465058c256cc86995dc8d6f1eb2f095df244fadcaf756935599f4d9ead6a9
mkdir -p inventory/classes/recipes/postgresql
tar -xf "$tmp/postgresql.tar" -C inventory/classes/recipes/postgresql --no-same-owner
oras pull -o "$tmp" ghcr.io/kadet-hub/recipes/valkey@sha256:a50d43cf9080b58040cf84909b0cd7497392709a23f5759e985bd912a9137ce5
mkdir -p inventory/classes/recipes/valkey
tar -xf "$tmp/valkey.tar" -C inventory/classes/recipes/valkey --no-same-owner

The first command fetches the recipe into inventory/classes/recipes/netbox, the second the generators and compiles:

kapitan compile --inventory-path fetch --fetch --output-path .
kapitan compile --fetch

The first command exits with status 1 because the fetch target compiles nothing. Kapitan's --fetch checks neither the digest nor the attestation. This verifies the attestation of the recipe and of the recipes it includes:

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/netbox@sha256:cf792f9704c9e8595385edcb883c72f4879e2dc79e979b5e61bd055af6dbfe59 \
  --owner kadet-hub \
  --cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/postgresql@sha256:1e8465058c256cc86995dc8d6f1eb2f095df244fadcaf756935599f4d9ead6a9 \
  --owner kadet-hub \
  --cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/valkey@sha256:a50d43cf9080b58040cf84909b0cd7497392709a23f5759e985bd912a9137ce5 \
  --owner kadet-hub \
  --cert-identity https://github.com/kadet-hub/registry/.github/workflows/release.yml@refs/heads/main

The attested index must also list each digest under its recipe with the pinned version, and not as yanked. Verifying by hand shows how to read it.

Settings

<c> stands for components.netbox, <w> for components.netbox-worker, <s> for generators.kubernetes.secrets.netbox-config.string_data.

Chart value Inventory
image.tag <c>.image, tag and digest
replicaCount <c>.replicas, with ReadWriteMany in <c>.persistent_volume_claims.netbox-media.access_modes
worker.replicaCount <w>.replicas
allowedHosts, csrf.trustedOrigins, loginRequired, timeZone, email ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS, LOGIN_REQUIRED, TIME_ZONE, EMAIL in a file of extraConfig, see below
email.password <s>.email_password.value
apiTokenPeppers <s>.api_token_peppers.value, for a second pepper
externalDatabase.host, .database, .username applications.postgresql.cluster, .database and .owner, which postgresql’s README lists
persistence.size, persistence.storageClass <c>.persistent_volume_claims.netbox-media.storage, .storage_class, 1Gi
resources, worker.resources <c>.resources, <w>.resources
nodeSelector, tolerations, affinity <c>.node_selector, <c>.tolerations, <c>.affinity, and the same below <w>
service.type <c>.service.type

The ConfigMap netbox holds the chart’s netbox.yaml for the values of tests/values.yaml. configuration.py loads it first and then every /run/config/extra/<n>/*.yaml, each merged over the settings before it. The chart’s extraConfig changes settings this way. To change a setting, put a YAML file with the changed keys into a ConfigMap of the target. Add it to <c>.volumes and <w>.volumes, and mount it at /run/config/extra/0 in both volume_mounts.

The database connection uses sslmode=require, because CloudNativePG serves TLS. NetBox keeps its tasks in database 0 and its cache in database 1 of valkey. The worker mounts the media claim too, so with a ReadWriteOnce claim it runs on the same node as NetBox.

The pod annotations checksum/config and checksum/secret are the chart’s hashes for the values of tests/values.yaml. They do not follow the target’s values, so after changing the ConfigMap or a Secret restart both Deployments with kubectl rollout restart deployment/netbox deployment/netbox-worker. configuration.py and netbox.yaml in the ConfigMap netbox are the chart’s, with its license in netbox.yml.LICENSE.

The chart’s housekeeping CronJob is off by default, because NetBox 4.6 and later run housekeeping as a job of their own. The chart’s Ingress, HTTPRoute, LDAP, metrics, ServiceMonitors, PodDisruptionBudgets, autoscalers and the claims for reports and scripts are off by default and not part of the recipe either. Its PostgreSQL and Valkey subcharts are Bitnami’s and stay off.

Any list set in the target is appended to the recipe’s list. To replace one, set the key to ~ in a class included after the recipe and the new list in the target.

Left out

The chart’s test pod runs only inside helm test. tests/smoke checks the same and more on the installed recipe. It is not rendered: Pod/netbox-test-connection.

Proof

check netbox --level deploys with tests/values.yaml and tests/target.yml, on kind for each Kubernetes minor CI tests, with cnpg installed first. The objects of postgresql and valkey are left out of the comparison and applied with the recipe. The Deployments and the Service select on name, the chart on app.kubernetes.io/name, instance and component. They select the same pods. The Role netbox grants the ServiceAccount netbox the same as the chart’s. tests/target.yml runs one PostgreSQL instance. NetBox migrates its empty database on the first start. On kind the Deployment became available up to 570 seconds after the apply, so the proof needs check 0.14.1 or later, which waits up to 900 seconds for a rollout.

tests/smoke waits for the Cluster and gets /login/ and /api/status/ through the Service and the API server’s service proxy. The status must report the version of the Deployment’s image tag and at least one running worker. It provisions a v2 token through /api/users/tokens/provision/ with the superuser’s login, and another token must get 403. With the token it creates a site and the prefix 192.0.2.0/24 on it and reads both back. It syncs a local data source, a job that NetBox queues in Valkey, and the job must finish in the worker. Every connection of app to PostgreSQL must use TLS.

Versions

The recipe's files are in kadet-hub/recipes. All recipes