kadet-hub logokadet-hub

Qdrant on Kubernetes without Helm, with Kapitan

Qdrant 1.20.0, a vector database with a REST and a gRPC API, as one node with its data on a claim and an API key required on every request, rendered by the kadet-hub kubernetes generator 1.14.0 from Qdrant’s own Helm chart.

What it deploys

Required parameters

applications.qdrant.api_key is the API key that Qdrant requires on every request, in the header api-key or as Authorization: Bearer <key>. Set it as a ref with the backend of your choice. Without it the compile stops with Interpolation key 'applications.qdrant.api_key' not found. The recipe puts it into the Secret qdrant-apikey, key api-key, from which clients in the namespace can read it.

Include

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

classes:
  - recipes.qdrant
parameters:
  namespace: qdrant
  kapitan:
    vars:
      target: qdrant
  applications:
    qdrant:
      api_key: <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.0:

parameters:
  kapitan:
    dependencies:
      - type: oci
        source: ghcr.io/kadet-hub/recipes/qdrant:1.0.0@sha256:48d3dadaf619d3bf4d640abbfe48a4f46fb1c0ba988b6436531583a8feb6794b
        output_path: inventory/classes/recipes/qdrant

The first command fetches the recipe into inventory/classes/recipes/qdrant, 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:

gh attestation verify \
  oci://ghcr.io/kadet-hub/recipes/qdrant@sha256:48d3dadaf619d3bf4d640abbfe48a4f46fb1c0ba988b6436531583a8feb6794b \
  --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

Chart value Inventory
image.tag components.qdrant.image, tag and digest
apiKey applications.qdrant.api_key
env components.qdrant.env.<variable>, such as QDRANT__TELEMETRY_DISABLED: "true"
persistence.size components.qdrant.volume_claims.qdrant-storage.spec.resources.requests.storage
persistence.storageClassName components.qdrant.volume_claims.qdrant-storage.spec.storageClassName
resources components.qdrant.resources
config the key production.yaml of generators.kubernetes.configs.qdrant.data
service.type components.qdrant.additional_services.qdrant.type
podAnnotations components.qdrant.pod_annotations
nodeSelector, tolerations, affinity components.qdrant.node_selector, components.qdrant.tolerations, components.qdrant.affinity

Qdrant reads QDRANT__<SECTION>__<KEY> variables over its configuration file, so env is the shorter way to change one setting. With the chart’s defaults Qdrant sends anonymous usage statistics to Qdrant, which QDRANT__TELEMETRY_DISABLED: "true" turns off.

The recipe runs one node. replicaCount is not a setting, because the generator cannot render the chart’s podManagementPolicy: Parallel, and with the API default OrderedReady the chart warns that the nodes of a cluster wait for each other forever.

The pod annotation checksum/config is the chart’s hash for the values in tests/values.yaml. It does not change with the ConfigMap qdrant, so after changing it restart the pod with kubectl rollout restart statefulset/qdrant. A changed API key needs the same restart.

The script initialize.sh in the ConfigMap qdrant is the chart’s, rendered for one node without snapshot restoration. It is Apache-2.0, and its license is in qdrant.yml.LICENSE.

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 runs only inside helm test. tests/smoke checks more on the installed recipe. It is not rendered: ConfigMap/qdrant-test-db-interaction, Pod/qdrant-test-db-interaction.

Proof

check qdrant --level deploys with tests/values.yaml and tests/target.yml, on kind for each Kubernetes minor CI tests. tests/values.yaml switches to the unprivileged image, adds what Pod Security restricted asks for beyond the chart’s defaults, sets the API key from the recipe’s Secret, and sets podManagementPolicy: OrderedReady and the label name on the claim template, which the generator renders. The objects differ in five ways, all of classes check accepts. The StatefulSet and both Services select on name, the chart on app and app.kubernetes.io/*, and both select the same pods. The Services target the container ports by name, the chart by number. The container ports are sorted by name. The pod and the ServiceAccount set automountServiceAccountToken: true, which the chart leaves unset. The Secret qdrant-apikey exists only in the recipe, and the chart’s pod reads it. The chart binds no role.

tests/smoke requires that the namespace would pass Pod Security restricted, and the image tag’s version from / through the Service. It gets 401 without the API key and with a wrong one. With the key of the Secret it creates a collection, writes three points with vectors and payload, and gets the nearest point with its payload from a query. It deletes the pod qdrant-0, waits for its successor on the same claim, and finds the three points and the same nearest point again.

Versions

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