Networking Minerva User Guide

Install on Kubernetes

Before you begin, make sure your workstation and cluster meet the Kubernetes Requirements. Then install NVIDIA Minerva in this order. Each section ends with a check: confirm it before you move on.

  1. Unpack the release.

  2. Import the images into your registry.

  3. Create the namespaces and pull secrets.

  4. Create the certificates, trust bundle, and license.

  5. Install the Keycloak Operator.

  6. Install the NVIDIA Minerva operator.

  7. Prepare the external addresses.

  8. Configure the custom resources.

  9. Apply the custom resources.

When you finish, continue with Validate and Sign In.

The commands on this page are the simplest way to do each task. If you have your own tooling for registries, secrets, or manifests, use it. What matters is the result.

Confirm that you're on the right cluster before each section. KUBECONFIG is set per shell, so an exported variable is lost when you reconnect.

kubectl config current-context

Cluster-scoped objects such as CRDs apply successfully to the wrong cluster. Namespaced objects then fail with namespaces "minerva-*" not found, which almost always means you're on the wrong cluster, not that you missed a step.

Unpack the Release

  1. Compare the archive's SHA-256 digest with the one delivered with it, either as a .sha256 file beside the archive or on the download page:

    sha256sum <archive-file>
    
  2. Unpack the archive and go to its directory. Whatever the archive is called, it unpacks to minerva-k8s-<version>-<arch>/:

    tar xzf <archive-file>
    cd minerva-k8s-<version>-<arch>
    
  3. Read MANIFEST.txt. It names the platform release, the operator version, and the CPU architecture the images were built for. The archive runs only on that architecture. On any other, the import and installation seem to succeed, and then every pod fails with exec format error.

  4. Confirm that the directory contains the files listed in Release archive contents.

Release archive contents

Path

Contents

README.md

The installation guide

MANIFEST.txt

Version, architecture, contents, and provenance

charts/minerva-operator-<chart-version>.tgz

The operator Helm chart

examples/customer-minervainfra.yaml

Reference custom resource for the infrastructure tier

examples/customer-minervaidentity.yaml

Reference custom resource for the identity tier

examples/customer-minervaapp.yaml

Reference custom resource for the application tier

images/images.txt

One image path per line, relative to your registry

images/images.tar.gz

The images, in docker-save layout

licenses/

Open-source attribution, the component list, and the written offer for the corresponding source

Import the Images into Your Registry

images/images.tar.gz is a standard docker-save archive. images/images.txt lists each image by the path it must have under your registry. Lines beginning with # are comments. Use whatever you normally use to populate your registry: a load-and-push from a workstation, skopeo, or your registry's import or replication feature.

Note: Load images/images.tar.gz once, and then tag and push each image from there. Don't read the archive with skopeo copy docker-archive: once per image. That extracts the whole archive for every image, and it fails when your temporary directory is smaller than the unpacked image set.

  1. Import every image under one registry path: the value you chose for ${MINERVA_REGISTRY_URL}.

  2. Keep each image's path and tag exactly as listed in images.txt. Flat names stay flat, and vendor paths stay nested. For example, with the registry path registry.example.com/minerva, strimzi/operator:0.47.0 becomes registry.example.com/minerva/strimzi/operator:0.47.0, and api:1.1.0 becomes registry.example.com/minerva/api:1.1.0.

  3. Confirm that every entry in images.txt resolves from your registry at its expected path. Fix any missing image now. Otherwise, it shows up during the installation as ImagePullBackOff.

Docker 25 or later may report that only a single-platform image was loaded or pushed. For a single-architecture archive, that's expected.

Create the Namespaces and Pull Secrets

  1. Create four namespaces: minerva-system, minerva-infra, minerva-app, and minerva-identity.

  2. In each namespace, create an image-pull Secret for your registry named exactly minerva-registry, of type kubernetes.io/dockerconfigjson. Pull secrets don't cross namespaces, so you need all four. Use whatever you normally use to manage secrets, such as kubectl create secret docker-registry, a secrets operator, or GitOps. Use a credential that doesn't expire, or arrange its refresh.

  3. Confirm that kubectl get ns | grep minerva lists all four namespaces, and that each one has a minerva-registry Secret of type kubernetes.io/dockerconfigjson.

Create the Certificates, Trust Bundle, and License

The platform's front door uses the UI certificate, stored in the Secret minerva-app-nginx-tls in minerva-app. Besides your UI hostname, it must carry the in-cluster names shown below. A public CA doesn't issue in-cluster names, so use a private issuer or bring your own certificate. Sign-in does not need a certificate of its own: browsers reach the platform's login service through the same front door, under /identity on your UI hostname.

  1. Request the certificate. With cert-manager:

    kubectl apply -f - <<'YAML'
    apiVersion: cert-manager.io/v1
    kind: Certificate
    metadata:
      name: minerva-app-nginx-tls
      namespace: minerva-app
    spec:
      secretName: minerva-app-nginx-tls
      commonName: <ui-hostname>
      privateKey: { algorithm: RSA, size: 2048 }
      usages: ["server auth"]
      dnsNames:
        - <ui-hostname>
        - nginx.minerva-app
        - nginx.minerva-app.svc
        - nginx.minerva-app.svc.cluster.local
        - minerva-app-nginx
        - minerva-app-nginx.minerva-app
        - minerva-app-nginx.minerva-app.svc
        - minerva-app-nginx.minerva-app.svc.cluster.local
        - localhost
      issuerRef: { name: <issuer-name>, kind: <issuer-kind>, group: cert-manager.io }
    YAML
    

    To bring your own certificate instead, create the same Secret with kubectl -n minerva-app create secret tls minerva-app-nginx-tls --cert=<file> --key=<file>. The certificate must carry the same subject alternative names.

  2. If you used cert-manager, wait until the certificate is issued. Until then, reading the Secret fails with Could not read certificate.

    kubectl -n minerva-app wait --for=condition=Ready \
      certificate/minerva-app-nginx-tls --timeout=120s
    
  3. Check the UI certificate. The issuer must be your CA, and the subject alternative names must include your UI hostname:

    kubectl -n minerva-app get secret minerva-app-nginx-tls -o jsonpath='{.data.tls\.crt}' |
      base64 -d | openssl x509 -noout -issuer -dates -ext subjectAltName
    
  4. Create the trust bundle. Platform pods trust your CA through a Secret named ca-certs, with the key root-ca.crt, that holds the PEM chain that signed the UI certificate above. Create it in minerva-app, minerva-infra, and minerva-identity. If you plan to federate login to an identity provider whose certificate is privately signed, add that CA too.

  5. In each of the three namespaces, confirm that the bundle holds a real certificate. An empty file still creates the key and only fails later, inside the pods.

    kubectl -n minerva-app get secret ca-certs -o jsonpath='{.data.root-ca\.crt}' |
      base64 -d | openssl x509 -noout -subject -enddate
    

    The command prints your CA's subject and expiry date. unable to load certificate means the bundle is empty or isn't a PEM certificate.

  6. Create the license ConfigMap, so the license service finds the license when it first starts. To get a license file, see Get a License. The ConfigMap must be named minerva-license, with the license in the key license.lic:

    kubectl -n minerva-app create configmap minerva-license --from-file=license.lic=<license-file>
    

You'll confirm the license state after the installation, in Validate and Sign In. The trust bundle is a point-in-time copy: when your CA rotates, re-create it and restart the workloads that use it.

Install the Keycloak Operator

The application tier waits until identity is ready, so it never starts without Keycloak. NVIDIA Minerva creates the Keycloak resources, and the upstream Keycloak Operator, which you install, runs them. The Keycloak Operator has no Helm chart.

  1. Get these five files from the Keycloak project's keycloak-k8s-resources repository, at tag 26.7.5, directory kubernetes/, and put them in one directory:

    kubernetes.yml
    keycloaks.k8s.keycloak.org-v1.yml
    keycloakrealmimports.k8s.keycloak.org-v1.yml
    keycloakoidcclients.k8s.keycloak.org-v1.yml
    keycloaksamlclients.k8s.keycloak.org-v1.yml
    

    All four CRDs are required: the Keycloak Operator fails to start if any is missing. Its two images, keycloak/keycloak-operator:26.7.5 and keycloak/keycloak:26.7.5, are already in your registry.

  2. In the same directory, save the following overlay as kustomization.yaml, with your registry path filled in. It changes the namespace, the two image references, and the pull secret, and leaves the upstream files untouched.

kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: minerva-identity
resources:
  - keycloaks.k8s.keycloak.org-v1.yml
  - keycloakrealmimports.k8s.keycloak.org-v1.yml
  - keycloakoidcclients.k8s.keycloak.org-v1.yml
  - keycloaksamlclients.k8s.keycloak.org-v1.yml
  - kubernetes.yml
images:
  - name: quay.io/keycloak/keycloak-operator
    newName: <registry>/<path>/keycloak/keycloak-operator
    newTag: "26.7.5"
patches:
  # the Keycloak server image the operator deploys
  - patch: |-
      apiVersion: apps/v1
      kind: Deployment
      metadata:
        name: keycloak-operator
      spec:
        template:
          spec:
            containers:
              - name: keycloak-operator
                env:
                  - name: RELATED_IMAGE_KEYCLOAK
                    value: <registry>/<path>/keycloak/keycloak:26.7.5
  # your pull secret
  - patch: |-
      apiVersion: v1
      kind: ServiceAccount
      metadata:
        name: keycloak-operator
      imagePullSecrets:
        - name: minerva-registry
  # the namespace transformer skips this subject; without it the operator has no permissions
  - target:
      kind: ClusterRoleBinding
      name: keycloak-operator-clusterrole-binding
    patch: |-
      - op: replace
        path: /subjects/0/namespace
        value: minerva-identity
  1. Apply the overlay with server-side apply. Two of the CRDs are too large for a client-side apply.

    kubectl apply --server-side -k <that-directory>/
    
  2. Confirm that all four CRDs exist, that the operator rolls out, and that its image tag is 26.7.5:

    kubectl get crd | grep k8s.keycloak.org        # all four CRDs
    kubectl -n minerva-identity rollout status deploy/keycloak-operator --timeout=120s
    kubectl -n minerva-identity get deploy keycloak-operator \
      -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
    

Only one Keycloak controller may watch minerva-identity.

Install the NVIDIA Minerva Operator

  1. Apply the chart's CRDs with server-side apply, because they're large. Then install the chart with crds.install: false:

    mkdir -p work/chart && tar -xzf charts/minerva-operator-<chart-version>.tgz -C work/chart
    kubectl apply --server-side -f work/chart/minerva-operator/files/crds/
    
    helm upgrade --install minerva-operator charts/minerva-operator-<chart-version>.tgz \
      --namespace minerva-system --wait --timeout 10m \
      -f - <<YAML
    image:
      registry: "<registry>/<path>"
      repository: minerva-operator
      tag: "<operator-version>"
    imagePullSecrets:
      - name: minerva-registry
    replicaCount: 2
    webhook:
      enabled: true
      replicas: 2
      failurePolicy: Fail
      podDisruptionBudget:
        minAvailable: 1
    crds:
      install: false
    certManager:
      enabled: true
      issuer:
        create: true
    YAML
    

    On a single-node cluster, set replicaCount: 1 and webhook.replicas: 1. The webhook replicas can't be scheduled on the same node.

  2. Confirm that the controller and webhook pods are running and ready, and that the three NVIDIA Minerva CRDs exist:

    kubectl -n minerva-system get pods              # controller and webhook Running and Ready
    kubectl get crd | grep minerva.nvidia.com       # minervainfras, minervaidentities, minervaapps
    

The chart's own issuer serves only the operator's webhook and metrics certificates, not your UI certificate. Two events of the form BadConfig: Certificate will be issued with an empty Issuer DN come from it at this point, and they need no action. The commands are the same in Helm 3 and Helm 4.

Prepare the External Addresses

Two Services in minerva-app need LoadBalancer addresses. Each accepts traffic only from the networks in its loadBalancerSourceRanges.

Service

Ports

Used by

UI front door (nginx)

443/TCP

Browsers

Devices load balancer (telemetry)

4317/TCP, 4318/TCP, 9091/TCP, 4319/TCP, 514/UDP, and 57402/TCP when WJH is on

Devices and telemetry senders

  1. Set up how your load-balancing solution assigns the addresses. The reference custom resources name a MetalLB pool minerva-collectors. MetalLB doesn't fall back when a named pool is missing, so either create a pool with that name or change the annotation in the application file. On Amazon EKS, use your AWS Load Balancer Controller annotations, and confirm that your controller version supports a Service that mixes TCP and UDP.

  2. (Optional) To publish the UI through your existing gateway instead, route HTTPS to backend port 443, and preserve the browser's Host header and the X-Forwarded-* headers. Sign-in builds its redirect addresses from them. Then set spec.nginx.service.type: ClusterIP in the application file and remove its LoadBalancer-only settings. The telemetry endpoint carries gRPC and UDP, so it can't go behind an HTTP ingress.

You can assign the addresses later. The installation still completes, and the Services show <pending>. Until every LoadBalancer Service has an address, the application's ExternalAccessReady condition stays False.

Configure the Custom Resources

  1. Copy the three files from examples/ to your working directory.

  2. Replace the placeholders one file at a time, using Values to Decide Before You Start. Don't use one search-and-replace across all three files.

  3. Set the fields in the following table. Leave everything else as delivered: the inline comments explain each fixed setting, and several that look redundant aren't.

File

Field

Set to

All three

spec.registry.url

Your registry path

Infrastructure and application

spec.storageClass

Your StorageClass

Infrastructure and application

spec.version

The platform version from MANIFEST.txt

Infrastructure

spec.kafka.coexistWithExistingStrimzi

false, unless an existing Strimzi installation of the same version as this release's (the strimzi/operator entry in images.txt) owns the shared ClusterRoles, runs in another namespace, and doesn't watch minerva-infra

Identity

spec.provider.keycloak.hostname

Your UI hostname, the same value as spec.platform.platformAddress

Application

spec.platform.customer and spec.platform.clusterName

Your display names

Application

spec.platform.platformAddress

Your UI hostname

Application

Both loadBalancerSourceRanges lists

Your client networks, and your device networks

Application

The LoadBalancer annotations in the same two places

What your load balancer needs

Application

spec.nginx.service type and settings

Only if you publish the UI through your gateway

Warning: spec.version is required. If it's unset, the components don't fall back to the release version. Each one uses a built-in default tag that may not exist in your registry, and the failure shows up later as a job that can't pull its image, not as an error when you apply. For the same reason, spec.mysql.imageRepository: "mysql" is required and must not be removed.

  1. In the application file, set the collector options under spec.otelCollector. Most installations set only the device networks and the load-balancer annotations, both under the gateway-push instance. Devices reach the platform only through gateway-push, by way of the Devices load balancer (the minerva-app-otel-data-lb Deployment and Service). For the settings, see What to set. For the optional collectors, see Optional collectors.

What runs

All collector instances run in the namespace minerva-app:

Instance (instances[].name)

What it does

Reference configuration

gateway-push

Receives what devices send: OTLP gRPC 4317/TCP, OTLP HTTP 4318/TCP, Prometheus remote write 9091/TCP, NMX-T gRPC 4319/TCP, syslog 514/UDP, and gNMI dial-out 57402/TCP when WJH is enabled. Its queue is in memory, so queued data doesn't survive a restart. StatefulSet otel-collector-gateway-push-collector

1 replica, external access on

gateway-pull

Scrapes Prometheus endpoints, such as DCGM exporters on GPU nodes. Its targets come from the platform configuration, not from this file. StatefulSet otel-collector-gateway-pull-collector

0 replicas (off)

processor

Transforms what the gateways receive and writes it to the platform's Kafka. In-cluster only. Deployment otel-collector-processor-collector

1 replica

internal

Receives the platform's own telemetry and writes it to Kafka. In-cluster only. Deployment otel-collector-internal-collector

1 replica

What to set

Set these under the gateway-push instance unless noted:

Setting

What to put

externalAccess.loadBalancerSourceRanges

The source networks of every device that sends telemetry (the device occurrence of ${MINERVA_ALLOWED_CIDRS}). An empty list is rejected unless allowPublicInternet: true is set, which opens the endpoint, UDP 514 included, to any source and isn't recommended

externalAccess.annotations

Your load balancer's annotations, for example the MetalLB address pool. Keep externalTrafficPolicy: Local. On MetalLB in layer-2 mode, it's what preserves the devices' source addresses

externalAccess.lbReplicas

Devices load balancer replicas: 1 by default, 2 or more for high availability. Each device stays pinned to one replica

replicas, resources (per instance)

Scale gateway-push and processor with the size of your fleet. Don't lower the reference memory limits

Optional collectors

All are off in the reference configuration. The paths are under spec.otelCollector:

Collector

Turn it on with

What it adds

WJH (What Just Happened) from switches

wjh.enabled: true

Switches dial in to gateway-push over gNMI with TLS on port 57402 (wjh.port), which the operator adds to the Devices load balancer. Needs a certificate

Host metrics

health.enabled: true

A DaemonSet, otel-collector-health-collector, that reports each node's host metrics to the platform

Cluster state

health.orchestration: true

One Deployment, otel-collector-orchestration-collector, that reports workload replicas, node readiness, and restarts, with its own read-only cluster role. Independent of health.enabled

Prometheus scraping

replicas: 1 on the gateway-pull instance

Scrapes the targets set in the platform configuration. Leave it at 0 unless NVIDIA Support asks you to enable it

  1. If you turned on WJH, set wjh.secrets.dialoutTLS to exactly one of:

    • secretRef.name: an existing kubernetes.io/tls Secret in minerva-app with the keys tls.crt and tls.key. The Secret must exist before you apply.

    • issuer: cert-manager issues and renews the certificate. Name your issuer in issuerRef.name. issuerRef.kind defaults to Issuer, which must then exist in minerva-app; set kind: ClusterIssuer for a cluster-scoped issuer. Put the address the switches dial in ipAddresses or dnsNames. Until the first certificate is issued, gateway-push can't start, and the application's OtelCollectorReady condition stays False with the cert-manager reason in its message. If a later renewal fails, the current certificate keeps serving.

    Either way, the certificate must name the exact IP address or DNS name the switches dial, normally the Devices load balancer address, so keep that address stable by pinning it with your load balancer. The certificate must also chain to a CA your switches trust: a switch rejects an unknown CA during the TLS handshake. If the switches' gNMI servers require authentication, also set wjh.secrets.gnmiCreds.name to a Secret with the keys username and password.

  2. Confirm that no placeholder is left. Every line this command prints must be a comment:

    grep -n 'MINERVA_' customer-minerva*.yaml
    

Apply the Custom Resources

  1. Apply the infrastructure, identity, and application resources in that order, waiting for each one:

    kubectl apply --server-side -f customer-minervainfra.yaml
    kubectl -n minerva-infra wait --for=condition=Ready minervainfra/minerva-infra --timeout=30m
    
    kubectl apply --server-side -f customer-minervaidentity.yaml
    kubectl wait --for=condition=Ready minervaidentity/cluster-identity --timeout=20m
    
    kubectl apply --server-side -f customer-minervaapp.yaml
    kubectl -n minerva-app wait --for=condition=Ready minervaapp/minerva-app --timeout=30m
    

    Allow about 40 minutes on a cluster sized as in Capacity. In a reference run, the infrastructure took 19 minutes, identity 3, and the application 16. The operator resolves the order itself, so applying all three at once also works; applying them one at a time makes a failure easier to trace. While it settles, the following are normal:

    • KeycloakCRDsNotRegistered on the identity resource, for about a minute.

    • waiting for MinervaIdentity Ready=True on the application. The application is waiting for identity to be ready.

    • One or two restarts of two Druid pods.

    To watch progress:

    kubectl get minervainfra,minervaidentity,minervaapp -A
    kubectl get pods -n minerva-infra
    kubectl get pods -n minerva-app
    
  2. Point your UI hostname at the address of the UI Service, or at your gateway. It's the only address to publish. Sign-in doesn't have a hostname, route, or Ingress of its own; it runs under /identity on your UI hostname.

  3. Confirm that the collectors are ready:

    kubectl -n minerva-app get statefulset/otel-collector-gateway-push-collector \
      deployment/otel-collector-processor-collector deployment/otel-collector-internal-collector \
      deployment/minerva-app-otel-data-lb service/minerva-app-otel-data-lb
    

    Every workload must show all replicas ready, and the Service minerva-app-otel-data-lb must show an external address. It shows <pending> until your load balancer assigns one. If you turned on an optional collector, its workload must be ready too. Point your devices at the Service's address. If you enabled WJH, readiness alone doesn't prove that the switches trust the certificate or that their subscriptions work. Confirm that when you configure the switches.

Last updated: