---
title: Deploy on Kubernetes
description: Install OpenLM LicOps on Kubernetes with Helm to connect to OpenLM Platform, and optionally add the webhook that licenses annotated pods.
product: OpenLM Platform
---

Deploy LicOps on Kubernetes when the workloads you want to gate run in the cluster, or when the cluster is where you already run shared services. You install the Router with one Helm chart, connect it to OpenLM Platform, and optionally install a second chart: an admission webhook that makes the admit, dispatch, and release requests for pods you annotate.

For Slurm or LSF, you don't need Kubernetes. A Docker Compose deployment is simpler. See [Deploy OpenLM LicOps](https://www.openlm.com/documentation/cloud/deployment-operations/licops/deployment).

## Before you begin

Confirm these prerequisites:

- A cluster that meets the [requirements for Kubernetes](https://www.openlm.com/documentation/cloud/deployment-operations/licops/system-requirements#requirements-for-kubernetes), and `kubectl` and Helm 3 configured for it.
- MongoDB 7.x and Redis 7.0 or higher that the cluster can reach, configured as described in [System requirements](https://www.openlm.com/documentation/cloud/deployment-operations/licops/system-requirements#redis-requirements).
- The LicOps Helm charts. OpenLM supplies them in the LicOps deployment bundle: `openlm-router` installs the Router, and `openlm-gke` installs the admission webhook. Contact OpenLM to get the bundle.
- Your cluster's pod network range, its pod CIDR. On many clusters `kubectl cluster-info dump | grep -m1 cluster-cidr` prints it.

## Create the secrets

The charts read their connection strings and certificate from secrets you create beforehand. They never create these secrets themselves. Run these commands in the namespace you deploy to:

```bash
export NS=openlm-licops

kubectl create namespace $NS

kubectl -n $NS create secret generic openlm-generic-mongo-db-credentials \
  --from-literal=MONGODB__CONNECTIONSTRING='mongodb://<user>:<password>@<mongo-host>:27017/?authSource=admin'

kubectl -n $NS create secret generic openlm-generic-redis-connection-credentials \
  --from-literal=REDIS__CONNECTIONSTRING='<redis-host>:6379,abortConnect=false'

kubectl -n $NS create secret tls openlm-lb-cert --cert=<certificate.pem> --key=<private-key.pem>
```

The TLS secret `openlm-lb-cert` is needed only while Traefik serves HTTPS, with `traefik.enabled` and `traefik.tls.enabled` both `true`, which is the default.

Then create the encryption key, once, on the first installation only:

```bash
kubectl -n $NS create secret generic openlm-router-platform-encryption \
  --from-literal=ADMISSION_PLATFORM_ENCRYPTION_KEY="$(openssl rand -base64 32)"
```

:::warning[Create the encryption key once and keep it]

The key decrypts the OpenLM Platform credentials stored in MongoDB. On an upgrade the secret already exists and this command fails. That is correct, so skip it. Do not delete and recreate the secret to get past the error: that replaces the key, and the stored OpenLM Platform credentials become unreadable.

Pass the key as plain text, exactly as `openssl` printed it. `--from-literal` encodes it for you. If you write the secret as a manifest instead, put the key under `stringData:`, not `data:`, or it is encoded twice and the first save of the OpenLM Platform connection fails.

:::

## Install the Router

Create a values file, `licops-values.yaml`, that makes the Router a LicOps Router managed from OpenLM Platform:

```yaml
routerapi:
  tag: "1.261006.0752"  # olm-routerapi
  encryptionKey:
    secretName: openlm-router-platform-encryption
  admissionController:
    proxyProtocol:
      trustedCidrs:
        - <your-pod-CIDR>
  extraEnv:
    # Required. The Router takes its configuration and rules from the OpenLM Platform.
    - name: licOpsConfiguration__enabled
      value: "true"
    - name: LICOPSCONFIGURATION_ENABLED
      value: "true"
    # Required. Declares a LicOps Router. Fixed at the first start.
    - name: ROUTER_CONFIG_BOOTSTRAP_MODE
      value: loadBalancer
    # Keep the background-job dashboard off.
    - name: hangfire__dashboard__enabled
      value: "false"

routerui:
  tag: "1.261008.0932"  # olm-routerui
```

To install the chart, run this command:

```bash
helm upgrade --install openlm-router ./licops-deploy/helm/openlm-router \
  -n $NS --set namespace=$NS -f licops-values.yaml
```

Set these values carefully:

- **`--set namespace=$NS`**: The chart places its resources in the namespace named in its own values, `openlm` by default, whatever namespace you pass to Helm. Set both to the same namespace.
- **`tag`**: The chart has no default tag. A missing tag renders an image reference ending in a colon, which fails only when the image is pulled.
- **`trustedCidrs`**: The Router uses it to trust the identity the webhook's sidecar sends with each checkout. Set it to your pod CIDR. An empty list stops the Router from starting. A range that does not cover your pod network passes that check with no error, but the Router then does not read the job identity the sidecar sends, so checkouts from those pods are not matched to their jobs.
- **The `extraEnv` entries**: The chart does not set OpenLM Platform management or the LicOps mode by itself. Without them, nothing you define in **LicOps Configuration** reaches the Router.

The chart creates two Services: `openlm-router-api`, the admission API on port `8100`, and `openlm-router-data`, the LicOps listen port `30001`. Workloads in the cluster use the API at `http://openlm-router-api.<namespace>.svc:8100`.

:::warning[The chart publishes the Router through Traefik]

By default the chart routes `/router-management` and `/api/v1` on your Traefik entry point to the Router, for any host name. These paths lead to the administration interfaces of the Router. Allow access to them only from your administration network, and do not publish them to the internet. Set `traefik.host` to a host name only your administrators use and restrict access to it, or set `traefik.enabled=false` and reach the Router with `kubectl port-forward`.

:::

Traefik is optional. With Traefik turned on, the chart expects the entry points `web`, `websecure`, and a TCP entry point `routertcp` on port `30001`, and `routertcp` publishes the LicOps listen port on the load balancer. Without Traefik, both Services are reachable only from inside the cluster.

To confirm the Router started, run these commands. Until you connect the Router to OpenLM Platform, the pod status is `Running` with `0/1` Ready, so `kubectl rollout status` does not finish. The API is reachable through the port-forward meanwhile.

```bash
kubectl -n $NS get pods
kubectl -n $NS port-forward svc/openlm-router-api 8100:8100
```

In a second terminal, run this command:

```bash
curl http://localhost:8100/api/v1/status
```

The response reads `"status": "unconfigured"` with `"shape": "serviceManaged"`, as described in [Deploy OpenLM LicOps](https://www.openlm.com/documentation/cloud/deployment-operations/licops/deployment#start-the-stack). The Router is running and is not connected to OpenLM Platform.

## Connect the Router to OpenLM Platform

With the port-forward running, connect the Router as described in [Configure OpenLM LicOps](https://www.openlm.com/documentation/cloud/deployment-operations/licops/configuration). Use the REST commands there against `http://localhost:8100`. If you kept Traefik turned on, you can also open the Router UI at `https://<traefik-address>/router-management/`.

A port-forward is for your own terminal only and stops when you close it. Workloads in the cluster use the Service address, never `localhost`.

## Install the admission webhook

The webhook is optional. It licenses pods without any change to their images: for every pod you annotate, it calls admit when the pod is created, adds init containers that dispatch and redirect the pod's license traffic, and adds a sidecar that releases the seat when the pod ends. If each job calls the API itself, skip this section and see [Kubernetes workloads](https://www.openlm.com/documentation/cloud/deployment-operations/licops/integrations/kubernetes#call-the-api-from-the-job).

:::warning[Pre-release]

The sidecar and the two init containers, tagged `dev-8f79e85`, are pre-release builds. The webhook image itself is a numbered release.

:::

```bash
helm upgrade --install openlm-gke ./licops-deploy/helm/openlm-gke -n $NS \
  --set image.tag=dev-8f79e85 \
  --set image.webhookTag=1.260819.1521 \
  --set certManager.enabled=false \
  --set router.restEndpoint=http://openlm-router-api.$NS.svc:8100 \
  --set router.host=openlm-router-data.$NS.svc
```

- **Two tags**: `image.tag` sets the sidecar and init-container images, and `image.webhookTag` sets the webhook. They are released separately.
- **The certificate**: The webhook needs a certificate the Kubernetes API server trusts. `certManager.enabled=false` makes the chart generate a self-signed certificate that is valid for 365 days. The chart makes a new one each time you run this `helm upgrade --install` command, and the webhook loads it only when it starts. After every run, restart the webhook with `kubectl -n $NS rollout restart deployment/openlm-gke`. If the certificate expires, every pod you opted in is rejected. To generate it, the chart pulls `alpine/k8s` from Docker Hub and might install `openssl`, so it needs internet access. For production, run cert-manager and pass `--set certManager.issuerRef.name=<your-issuer>` instead. With neither, the installation fails at once.
- **The Router addresses**: `router.restEndpoint` and `router.host` are required. Give `router.restEndpoint` the Service address and port only. The webhook adds the API path itself, so a value that already ends in `/api/v1/admission-controller` makes every admit fail and every opted-in pod is rejected.

:::warning[An unavailable webhook blocks pod creation]

The webhook is registered with a failure policy of `Fail`, so while it is unavailable, no pod can be created in a namespace you opted in. That keeps pods from starting without a LicOps seat during a webhook outage. To let pods start without a seat, install with `--set webhook.failurePolicy=Ignore`. This setting covers the webhook only: when the webhook runs but cannot reach LicOps, it lets pods through unchanged, as described in [Kubernetes workloads](https://www.openlm.com/documentation/cloud/deployment-operations/licops/integrations/kubernetes).

:::

The chart pulls the images `olm-pod-sidecar`, `olm-dispatch-init`, and `olm-iptables-init`.

To opt namespaces and pods in, see [Kubernetes workloads](https://www.openlm.com/documentation/cloud/deployment-operations/licops/integrations/kubernetes).

## Upgrade

Change the tags in `licops-values.yaml`, then run the same `helm upgrade --install` command. If you installed the admission webhook, upgrade it too: run its `helm upgrade --install` command again with the new `image.tag` and `image.webhookTag`. If you use the self-signed certificate, then restart the webhook with `kubectl -n $NS rollout restart deployment/openlm-gke`. Skip the encryption-key secret: it already exists and must not change. To list the available builds, see [Find the newest build](https://www.openlm.com/documentation/cloud/deployment-operations/licops/deployment#find-the-newest-build). The configuration and rules live in OpenLM Platform, and the open sessions live in Redis, so they survive the upgrade.
