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.
Components Overview#
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.
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#
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 replace it while retaining the
database. After the first successful installation, set bootstrap.enabled
to false.
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. Do not regenerate a missing CA for an existing installation; restore it.
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, then set bootstrap.enabled to false after the first
successful 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.
Prepare 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.
Deploy Components#
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:
$ 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 --wait-for-jobs --timeout 30m
After a successful chart-managed MEK and service-auth bootstrap, change both
bootstrap.enabled values to false in osmo-values.yaml and apply a
cleanup upgrade:
$ helm upgrade osmo osmo/osmo --version <chart-version> \
--namespace osmo \
--values /tmp/osmo-chart/osmo/profiles/split-plane-control.yaml \
--values osmo-values.yaml \
--wait --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.
Verify#
Check the release without reading Secret values:
$ 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:
$ curl --fail http://127.0.0.1:8080/api/version
Sign in through the UI or CLI, list pools and resources, and submit a small CPU workflow. Confirm that PostgreSQL contains the new workflow, 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 any missing retained CA rather than enabling initial generation. 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.
For all chart values and lifecycle procedures, refer to the unified chart README .