Deploy Service#
This guide provides step-by-step instructions for deploying OSMO service
components on a Kubernetes cluster. It uses the unified osmo Helm chart in
control-plane-only mode and externally managed PostgreSQL, Valkey, and object
storage.
Deployment Architecture#
OSMO deployment consists of several main components:
Component |
Description |
|---|---|
API Service |
Workflow operations and API endpoints |
Router Service |
Routing traffic to the API Service |
Web UI Service |
Web interface for users |
Worker Service |
Background job processing |
Logger Service |
Log collection and streaming |
Agent Service |
Client communication and status updates |
Delayed Job Monitor |
Monitoring and managing delayed background jobs |
Gateway |
Authentication, authorization, and routing into the control plane |
Prerequisites#
The cluster must run Kubernetes 1.30 or newer. Install Helm 3.19 or newer,
kubectl, and Python 3, select the target cluster context, and confirm that
the control plane can reach PostgreSQL, Valkey, object storage, and your
identity provider. Create the deployment namespace before creating Secrets:
$ kubectl create namespace osmo
The Secret manifests in this guide use stringData so their required keys
are clear. Replace every placeholder before applying them, restrict access to
the files, and never commit them to source control.
Prepare Values and Secrets#
Configure PostgreSQL Connection#
Create an empty PostgreSQL database for OSMO. The database user must be able to
create and update objects in that database. The username is non-secret
connection metadata configured in Helm values. Store the password under the
default db-password key and save the following as
postgresql-secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: osmo-postgresql
namespace: osmo
type: Opaque
stringData:
db-password: <postgresql-password>
$ kubectl create --filename postgresql-secret.yaml
Reference the endpoint and Secret in osmo-values.yaml:
If PostgreSQL uses a private CA, save the referenced trust Secret as
postgresql-ca-secret.yaml and create it first:
apiVersion: v1
kind: Secret
metadata:
name: osmo-postgresql-ca
namespace: osmo
type: Opaque
stringData:
ca.crt: |
-----BEGIN CERTIFICATE-----
<base64-encoded-certificate-body>
-----END CERTIFICATE-----
$ kubectl create --filename postgresql-ca-secret.yaml
externalDependencies:
postgresql:
host: postgresql.example.com
port: 5432
database: osmo
username: <postgresql-username>
tls:
enabled: true
sslMode: verify-full
caExistingSecret: osmo-postgresql-ca
caKey: ca.crt
secrets:
postgresql:
existingSecret: osmo-postgresql
keys:
password: db-password
Use sslMode: require and leave caExistingSecret empty only when the
connection must be encrypted but no CA bundle is available. This does not
authenticate the server; verify-full is preferred. For a server that does
not use TLS, set tls.enabled: false.
Configure Valkey Connection#
OSMO requires Valkey or Redis 7 or newer. Save a Secret whose default key is
redis-password as valkey-secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: osmo-valkey
namespace: osmo
type: Opaque
stringData:
redis-password: <valkey-password>
$ kubectl create --filename valkey-secret.yaml
When Valkey uses a private CA, save its trust bundle as
valkey-ca-secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: osmo-valkey-ca
namespace: osmo
type: Opaque
stringData:
ca-bundle.crt: |
-----BEGIN CERTIFICATE-----
<base64-encoded-certificate-body>
-----END CERTIFICATE-----
$ kubectl create --filename valkey-ca-secret.yaml
Add the connection to osmo-values.yaml:
externalDependencies:
valkey:
host: valkey.example.com
port: 6379
database: 0
tls:
enabled: true
# Set these only when Valkey uses a private CA.
caExistingSecret: osmo-valkey-ca
caKey: ca-bundle.crt
secrets:
valkey:
generate: false
existingSecret: osmo-valkey
keys:
password: redis-password
When caExistingSecret is set, its selected key must contain the complete
CA trust bundle. Leave it empty for a public CA. Set tls.enabled: false only
for a trusted network endpoint that does not provide TLS.
Configure Storage Connection#
OSMO uses three locations for workflow state, logs, and application bundles.
All locations must use the same scheme: s3://, azure://, or
swift://.
Static credentials#
Create one Secret containing the credential document. The following S3 or
S3-compatible example can be saved as object-storage-secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: osmo-object-storage
namespace: osmo
type: Opaque
stringData:
object-storage.yaml: |
access_key_id: <access-key-id>
access_key: <secret-access-key>
$ kubectl create --filename object-storage-secret.yaml
Reference the Secret and locations in osmo-values.yaml:
externalDependencies:
objectStorage:
authentication:
type: static
locations:
workflows: s3://osmo-workflows/workflows
logs: s3://osmo-logs/logs
apps: s3://osmo-apps/apps
s3:
region: us-east-1
# Leave empty for AWS S3.
overrideUrl: https://s3.example.com
secrets:
objectStorage:
generate: false
existingSecret: osmo-object-storage
keys:
credentials: object-storage.yaml
The three locations may share a bucket or container. Grant only the read/write permissions needed for those prefixes.
Workload identity#
As an alternative, configure AWS IRSA, Azure Workload Identity, or GCP
Workload Identity and grant the identities used by the API, worker, and
workflow pods access to all three locations. For Azure Workload Identity, save
the workflow ServiceAccount as workflow-service-account.yaml and create it
before installing OSMO:
apiVersion: v1
kind: ServiceAccount
metadata:
name: osmo-workflow
namespace: osmo
annotations:
azure.workload.identity/client-id: <managed-identity-client-id>
$ kubectl create --filename workflow-service-account.yaml
Select the provider SDK’s default credential chain, add the identity to each target pool’s pod templates, and do not configure an object-storage Secret:
externalDependencies:
objectStorage:
authentication:
type: sdkDefault
locations:
workflows: azure://<account>/<container>/workflows
logs: azure://<account>/<container>/logs
apps: azure://<account>/<container>/apps
secrets:
objectStorage:
generate: false
existingSecret: ''
configuration:
podTemplates:
azure_workload_identity:
metadata:
labels:
azure.workload.identity/use: "true"
spec:
serviceAccountName: osmo-workflow
pools:
default:
common_pod_template:
- default_ctrl
- default_user
- azure_workload_identity
services:
api:
serviceAccount:
annotations:
azure.workload.identity/client-id: <managed-identity-client-id>
pod:
labels:
azure.workload.identity/use: "true"
worker:
serviceAccount:
create: true
annotations:
azure.workload.identity/client-id: <managed-identity-client-id>
pod:
labels:
azure.workload.identity/use: "true"
The annotations and pod labels are provider-specific. Use the equivalent IRSA
or GKE annotations for AWS or GCP. Federate the exact ServiceAccount subjects
used by the release; with release name osmo they are osmo-api,
osmo-worker, and osmo-workflow.
Configure Other Secrets#
For maximum recovery robustness, keep each production credential’s source of
truth in your organization’s secret manager and provision its Kubernetes Secret
before installation. The chart’s bootstrap mechanism remains available as a
convenience when external provisioning is not used. Back up every credential
together with the state it protects. Secrets created by bootstrap carry the
osmo.nvidia.com/credential-source annotation; bootstrap does not add it to
pre-existing Secrets.
MEK#
The master encryption key (MEK) protects encrypted values stored in PostgreSQL.
Chart-managed#
For a new database, the chart can create the retained
osmo-master-encryption-key Secret without placing key material in Helm
state:
secrets:
masterEncryptionKey:
managementMode: osmo
existingSecret:
name: osmo-master-encryption-key
key: mek.yaml
bootstrap:
enabled: true
Back up that Secret after installation. Never intentionally replace it while retaining the database.
User-managed#
For a user-managed MEK, save the following as mek-secret.yaml. The
currentMek value must name an entry in meks; each entry is a
base64-encoded symmetric JWK. Generate a random 32-byte key, encode it as
base64url without padding for the JWK’s k field, then base64-encode the
complete {"k":"...","kid":"key1","kty":"oct"} document for the
meks value:
apiVersion: v1
kind: Secret
metadata:
name: osmo-master-encryption-key
namespace: osmo
type: Opaque
stringData:
mek.yaml: |
currentMek: key1
meks:
key1: <base64-encoded-symmetric-jwk>
$ kubectl create --filename mek-secret.yaml
Set managementMode: external and bootstrap.enabled: false when the
Secret is user-managed.
Internal TLS#
gateway.tls protects traffic from Envoy to the OSMO services; it does not
configure public edge TLS.
Chart-managed#
Chart-managed mode creates and retains an internal CA, trust bundle, and one leaf Secret per service:
gateway:
tls:
enabled: true
generated:
enabled: true
Back up the retained TLS Secrets. OSMO-managed mode recreates a missing CA, but the new CA invalidates existing leaf certificates and trust. Restore the original CA when continuity matters.
User-managed#
For user-managed TLS, create a CA Secret containing ca.crt and a
kubernetes.io/tls Secret containing tls.crt and tls.key for each
enabled upstream. Each certificate’s DNS subject alternative name must match
the corresponding in-cluster Service host. Save the following as
internal-tls-secrets.yaml, adding one TLS Secret document for every enabled
upstream:
apiVersion: v1
kind: Secret
metadata:
name: osmo-internal-ca
namespace: osmo
type: Opaque
stringData:
ca.crt: |
-----BEGIN CERTIFICATE-----
<base64-encoded-ca-certificate-body>
-----END CERTIFICATE-----
---
apiVersion: v1
kind: Secret
metadata:
name: osmo-api-tls
namespace: osmo
type: kubernetes.io/tls
stringData:
tls.crt: |
-----BEGIN CERTIFICATE-----
<base64-encoded-api-certificate-body>
-----END CERTIFICATE-----
tls.key: |
-----BEGIN PRIVATE KEY-----
<base64-encoded-api-private-key-body>
-----END PRIVATE KEY-----
$ kubectl create --filename internal-tls-secrets.yaml
Point OSMO at the Secret names as follows:
gateway:
tls:
enabled: true
generated:
enabled: false
caSecret: osmo-internal-ca
upstreamCerts:
api: osmo-api-tls
router: osmo-router-tls
agent: osmo-agent-tls
logger: osmo-logger-tls
# Required only when services.mcp.enabled is true.
mcp: ''
Change gateway.tls.rolloutNonce after rotating user-managed TLS Secrets.
Service auth#
Service auth is OSMO’s stable JWT signing identity.
Chart-managed#
The chart can create the retained osmo-service-auth Secret for a new
installation:
secrets:
serviceAuth:
managementMode: osmo
existingSecret:
name: osmo-service-auth
key: authentication-config.json
bootstrap:
enabled: true
Back up the Secret after installation.
User-managed#
For a user-managed identity, generate the file with the OSMO service image:
$ mkdir --mode 0700 service-auth
$ docker run --rm --user "$(id -u):$(id -g)" \
--entrypoint service-auth-bootstrap \
--volume "$PWD/service-auth:/output" \
nvcr.io/nvidia/osmo/service:<image-tag> \
generate --output /output/authentication-config.json
Copy the generated JSON into service-auth-secret.yaml and create the
Secret before installation:
apiVersion: v1
kind: Secret
metadata:
name: osmo-service-auth
namespace: osmo
type: Opaque
stringData:
authentication-config.json: |
<contents-of-service-auth/authentication-config.json>
$ kubectl create --filename service-auth-secret.yaml
The JSON document must contain active_key and a matching entry in keys
with valid public_key and private_key JWK values. Set
managementMode: external and bootstrap.enabled: false. Change
rolloutNonce after an intentional update.
Assemble the Control-plane Values#
The chart packages a profiles/split-plane-control.yaml base profile for an
HA control plane with external PostgreSQL, Valkey, and object storage. The
profile enables the control plane, disables the compute plane and embedded
stateful dependencies, uses the chart application version for OSMO images, and
configures autoscaling, disruption budgets, and topology spreading. The
deployment commands below layer osmo-values.yaml after that profile.
Combine only the site-specific settings above in osmo-values.yaml. This
static-credential example uses the chart defaults for embedded Dex and uses
chart-managed MEK, service auth, and internal TLS. Replace every angle-bracket
placeholder:
externalUrl: https://osmo.example.com
externalDependencies:
postgresql:
host: <postgresql-host>
port: 5432
database: osmo
username: <postgresql-username>
tls:
enabled: true
sslMode: verify-full
caExistingSecret: osmo-postgresql-ca
caKey: ca.crt
valkey:
host: <valkey-host>
port: 6379
database: 0
tls:
enabled: true
objectStorage:
authentication:
type: static
locations:
workflows: s3://<bucket>/workflows
logs: s3://<bucket>/logs
apps: s3://<bucket>/apps
s3:
region: <region>
overrideUrl: ''
secrets:
postgresql:
existingSecret: osmo-postgresql
valkey:
generate: false
existingSecret: osmo-valkey
objectStorage:
generate: false
existingSecret: osmo-object-storage
masterEncryptionKey:
managementMode: osmo
existingSecret:
name: osmo-master-encryption-key
key: mek.yaml
bootstrap:
enabled: true
serviceAuth:
managementMode: osmo
existingSecret:
name: osmo-service-auth
key: authentication-config.json
bootstrap:
enabled: true
gateway:
envoy:
service:
type: LoadBalancer
tls:
enabled: true
generated:
enabled: true
Configure an External IdP#
By default, the unified chart enables an embedded Dex identity provider and bootstraps a statically configured admin identity. Embedded Dex uses volatile memory storage and is intended only to speed up development and evaluation; it is not suitable for production deployments. For production, disable Dex and use your organization’s external OIDC identity provider.
Before continuing, follow
Identity Provider (IdP) Setup to register the
required confidential browser and public CLI clients and collect their IDs,
endpoints, and claims. Then save the browser client secret and a random
32-byte cookie secret as external-oidc-secret.yaml:
$ openssl rand -base64 32
Use the command output as cookie_secret:
apiVersion: v1
kind: Secret
metadata:
name: osmo-external-oidc
namespace: osmo
type: Opaque
stringData:
client_secret: <oidc-browser-client-secret>
cookie_secret: <random-32-byte-cookie-secret>
$ kubectl create --filename external-oidc-secret.yaml
Disable embedded Dex and replace the authentication block in
osmo-values.yaml. Endpoint URLs are explicit so providers without complete
OIDC discovery remain supported:
embeddedDependencies:
dex:
enabled: false
authentication:
provider: externalOidc
bootstrap:
identities:
admin:
enabled: false
externalOidc:
issuer: https://idp.example.com
browserClientId: <browser-client-id>
cliClientId: <device-client-id>
authorizationEndpoint: https://idp.example.com/authorize
tokenEndpoint: https://idp.example.com/token
deviceEndpoint: https://idp.example.com/device
jwksUri: https://idp.example.com/keys
jwksHost: idp.example.com
userClaim: sub
rolesClaim: roles
scopes: [openid, email, profile]
logoutEndpoint: https://idp.example.com/logout
browserClientSecret:
existingSecret: osmo-external-oidc
key: client_secret
cookieSecret:
existingSecret: osmo-external-oidc
key: cookie_secret
See IdP Role Mapping and Sync Modes for role mapping.
The example disables the bootstrap administrator identity because human users
authenticate through the external provider. If an OSMO administrator token is
also required for CLI automation or recovery, replace the admin block above
with the following. This keeps the OSMO token without creating an embedded-Dex
identity or password:
admin:
enabled: true
username: admin
roles:
- osmo-admin
dex:
enabled: false
tokens:
primary:
managedSecret:
name: osmo-admin-token
Install OSMO#
Add the published OSMO chart repository and select the chart version to deploy. Pull that version once to obtain its matching control-plane profile for the values layering below:
$ helm repo add osmo https://helm.ngc.nvidia.com/nvidia/osmo
$ helm repo update
$ helm pull osmo/osmo --version <chart-version> \
--untar --untardir /tmp/osmo-chart
$ helm show chart osmo/osmo --version <chart-version>
Render and lint the release before changing the cluster:
$ helm lint /tmp/osmo-chart/osmo \
--values /tmp/osmo-chart/osmo/profiles/split-plane-control.yaml \
--values osmo-values.yaml
$ helm template osmo osmo/osmo --version <chart-version> \
--namespace osmo \
--values /tmp/osmo-chart/osmo/profiles/split-plane-control.yaml \
--values osmo-values.yaml > /tmp/osmo-rendered.yaml
Install the control plane and wait for bootstrap and migration Jobs. The
command uses Helm 4’s --wait=legacy strategy. With Helm 3, replace
--wait=legacy with --wait:
$ helm upgrade --install osmo osmo/osmo --version <chart-version> \
--namespace osmo \
--values /tmp/osmo-chart/osmo/profiles/split-plane-control.yaml \
--values osmo-values.yaml \
--wait=legacy --wait-for-jobs --timeout 30m
Configure public DNS and edge TLS for externalUrl after the gateway’s
LoadBalancer address is assigned. gateway.tls does not terminate public
TLS; use your load balancer, Ingress, or Gateway API implementation for that.
Log In#
Set the public OSMO URL and log in through the configured external OIDC provider:
$ OSMO_URL=https://osmo.example.com
$ osmo login "$OSMO_URL"
If the optional osmo-admin-token bootstrap credential was retained, it can
instead be used from a protected temporary file:
$ set -o pipefail
$ umask 077
$ OSMO_TOKEN_FILE="$(mktemp)" &&
kubectl --namespace osmo get secret osmo-admin-token \
--output jsonpath='{.data.token}' | base64 --decode > "$OSMO_TOKEN_FILE" &&
osmo login "$OSMO_URL" --method token --token-file "$OSMO_TOKEN_FILE"
$ OSMO_LOGIN_STATUS=$?
$ rm -f -- "${OSMO_TOKEN_FILE:-}" || OSMO_LOGIN_STATUS=$?
$ unset OSMO_TOKEN_FILE
$ test "$OSMO_LOGIN_STATUS" -eq 0
For embedded-Dex evaluation, retrieve the generated password, visit
$OSMO_URL, and sign in as admin@osmo.local:
$ kubectl --namespace osmo get secret osmo-embedded-dex-admin \
--output jsonpath='{.data.password}' | base64 --decode
$ printf '\n'
In production, use the external OIDC provider configured in
Configure an External IdP; the UI and default osmo login command
use that provider.
Verify the Deployment#
Check the release without reading Secret values:
# Verify the Helm release and control-plane workloads
$ helm status osmo --namespace osmo
$ kubectl --namespace osmo get pods,jobs,services
$ kubectl --namespace osmo get secret \
osmo-master-encryption-key osmo-service-auth
$ kubectl --namespace osmo wait --for=condition=Available deployment \
--selector=app.kubernetes.io/instance=osmo --timeout=10m
Verify the API through the same gateway used by clients. A port-forward avoids waiting for public DNS during initial validation:
$ kubectl --namespace osmo port-forward service/osmo-gateway 8080:80
In another terminal:
# Verify API availability, pools, and resources
$ curl --fail http://127.0.0.1:8080/api/version
$ osmo pool list
$ osmo resource list --pool default
After the compute plane is connected, follow Deploy Compute Backend to submit the CPU and object-storage verification workflows. Confirm that PostgreSQL contains the workflows, Valkey remains reachable, and objects appear under the configured workflow, log, and app locations.
Troubleshooting#
Start with release events and failed containers or Jobs:
$ helm status osmo --namespace osmo
$ kubectl --namespace osmo get pods,jobs
$ kubectl --namespace osmo describe pod <pod-name>
$ kubectl --namespace osmo logs <pod-name> --all-containers --previous
$ kubectl --namespace osmo logs job/<job-name>
Common failures include:
Values validation fails before install: run
helm lintand check that all external dependencies have endpoints and Secret references. Static object storage requires a Secret;sdkDefaultforbids one.A bootstrap executable is not found: the chart and OSMO images are from different releases. Use the same pinned
<chart-version>for every Helm command and do not override the control-plane profile’s chart-version image selection.PostgreSQL connection or migration fails: verify DNS, network policy, database ownership, the configured username, the
db-passwordSecret key, TLS mode, and CA bundle. Do not enabledatabaseMigrationfor a new database merely to retry connectivity.Valkey readiness fails: confirm Valkey/Redis is version 7 or newer, the selected database exists,
redis-passwordis correct, and the TLS trust bundle is complete.Object-storage operations fail: confirm all three locations use one URI scheme and that the API and worker identity or static credential can read and write each prefix. For workload identity, inspect the rendered ServiceAccount names and federation subjects.
MEK bootstrap fails: use it only with a new database. Correct the cause and increment
secrets.masterEncryptionKey.bootstrap.attemptto retry. Do not generate a replacement for an installation with retained encrypted data.Service-auth bootstrap fails: correct image pull or RBAC issues and increment
secrets.serviceAuth.bootstrap.attempt. Do not replace the retained signing identity during an ordinary upgrade.Internal TLS bootstrap fails: with generated TLS, restore a missing retained CA when continuity matters; an automatically recreated CA requires new leaf certificates and trust. With user-managed TLS, verify
ca.crt,tls.crt,tls.key, and each Service DNS SAN.Authentication redirects or JWKS fetches fail: verify
externalUrl, issuer and endpoint URLs,jwksHost, client IDs, Secret keys, and public edge TLS. Embedded Dex must be disabled whenprovider: externalOidc.Gateway has no public address: inspect the
osmo-gatewayService and cloud load-balancer events, or keep the Service internal and configureingressorhttprouteinstead.
Upgrade and Recovery#
Back up PostgreSQL, object storage, and every credential Secret before an upgrade. Reuse the same control profile and complete site values. For maximum recovery robustness, keep each production credential’s source of truth in your organization’s secret manager and provision its Kubernetes Secret before installation. The chart’s bootstrap mechanism remains available as a convenience when external provisioning is not used. Follow the chart lifecycle procedures linked below for database migrations and credential rotation.
Cleanup#
Uninstalling the control release preserves retained Secrets and externally managed data. Back them up and confirm that no compute plane still depends on this control plane before deleting the namespace:
$ helm uninstall osmo --namespace osmo --wait
For all chart values and lifecycle procedures, refer to the unified chart README .