---
title: "Getting started with Kodama on Kubernetes"
description: "Install PostgreSQL and NATS with Helm, deploy Kodama, authenticate the CLI with OAuth, and register and run your first pipeline."
canonical_url: https://optimi.com/en/docs/kodama/getting-started
md_url: https://optimi.com/en/docs/kodama/getting-started.md
last_updated: 2018-10-20
---

# Getting started

This guide takes you from a Kubernetes cluster to a completed pipeline:
**prepare prerequisites → install backend charts → install Kodama → authenticate
the CLI → register and run a pipeline**. Commands use a standalone installation,
one tenant (`t_acme`) and one project. Replace example domains, credentials,
cloud identities and network ranges with your own.

## 1. Prerequisites

| Requirement | Prepare before installation |
| --- | --- |
| Kubernetes | Kubernetes 1.29+ compatible with the selected charts, a default persistent StorageClass, a NetworkPolicy-enforcing CNI and permission to install CRDs, namespaces and RBAC |
| Workstation | Helm, `kubectl`, `curl`, `jq`, OpenSSL and Go 1.27.2 or a compatible toolchain for installing the CLI |
| HTTPS ingress | An ingress controller, `kodama.example.com`, a TLS certificate and a workload-reachable HTTPS callback origin |
| Object storage | An S3-compatible bucket, endpoint, region and scoped access/secret keys |
| Git | An HTTPS repository with an initial commit on `main`, plus separate read and publish tokens |
| Registry | Access to `ghcr.io/optimiware/kodama` and the digest-pinned workload images you will run |
| OIDC | An issuer issuing JWT access tokens for the factory audience, a public OAuth client and a designated initial administrator |
| Key management | One installation KMS: AWS KMS, GCP Cloud KMS or Scaleway Key Manager, with the server identity authorized to use its wrapping key |
| Backend TLS | A trusted certificate for the NATS endpoint and the CA material needed to validate PostgreSQL and runtime callbacks |

Register the OAuth client as `kodama-cli`, enable **Authorization Code + S256
PKCE**, and allow the exact redirect URI `http://127.0.0.1:18999/callback`.
Use `https://kodama.example.com/api` as the access-token audience in this example.
Record the issuer’s discovery, authorization and token URLs. An ID token is
not an API access token.

Choose a Kodama release from [GitHub](https://github.com/optimiware/kodama/tags)
and record its container digest from [GHCR](https://github.com/optimiware/kodama/pkgs/container/kodama).
Use the same release for the chart, server image and CLI:

```sh
export KODAMA_REF=0.0.1
export KODAMA_DIGEST='sha256:replace-with-the-release-image-digest'
export FACTORY=https://kodama.example.com
export TENANT_ID=t_acme

kubectl create namespace kodama-backends
kubectl create namespace kodama-system
umask 077
mkdir -p credentials runtime-secrets migration-secrets
```

`0.0.1` is an example release selection; use your chosen tag and its real digest.
Keep the generated credentials outside version control.

## 2. Install the backend Helm charts

This walkthrough uses the upstream **CloudNativePG** charts for PostgreSQL and
the upstream **NATS** chart for JetStream. S3, Git, the registry, OIDC and KMS
use the services prepared above.

```sh
helm repo add cnpg https://cloudnative-pg.github.io/charts
helm repo add nats https://nats-io.github.io/k8s/helm/charts/
helm repo update
helm search repo cnpg/cloudnative-pg --versions
helm search repo cnpg/cluster --versions
helm search repo nats/nats --versions
```

Pin the chart versions. The examples below use these versions; check their
compatibility with your cluster and retain the pins with your installation values:

```sh
export CNPG_OPERATOR_VERSION=0.29.1
export CNPG_CLUSTER_VERSION=0.9.0
export NATS_CHART_VERSION=2.15.1
```

### PostgreSQL

Install the operator, then create distinct migration and runtime login secrets:

```sh
helm upgrade --install cnpg cnpg/cloudnative-pg \
  --namespace cnpg-system --create-namespace \
  --version "$CNPG_OPERATOR_VERSION" --wait --timeout 10m

printf %s "$(openssl rand -hex 32)" > credentials/db-migration-password
printf %s "$(openssl rand -hex 32)" > credentials/db-runtime-password
kubectl -n kodama-backends create secret generic kodama-db-migration \
  --type=kubernetes.io/basic-auth --from-literal=username=kodama_migration \
  --from-file=password=credentials/db-migration-password
kubectl -n kodama-backends create secret generic kodama-db-runtime \
  --type=kubernetes.io/basic-auth --from-literal=username=kodama_runtime \
  --from-file=password=credentials/db-runtime-password
```

Save `postgres-values.yaml`:

```yaml
fullnameOverride: kodama-postgres
type: postgresql
version:
  postgresql: "17"
cluster:
  instances: 3
  enableSuperuserAccess: false
  storage:
    size: 20Gi
  initdb:
    database: kodama
    owner: kodama_migration
    secret:
      name: kodama-db-migration
  roles:
    - name: kodama_runtime
      ensure: present
      login: true
      superuser: false
      bypassrls: false
      passwordSecret:
        name: kodama-db-runtime
```

```sh
helm upgrade --install kodama-postgres cnpg/cluster \
  --namespace kodama-backends --version "$CNPG_CLUSTER_VERSION" \
  -f postgres-values.yaml --wait --timeout 10m
kubectl -n kodama-backends wait cluster/kodama-postgres \
  --for=condition=Ready --timeout=600s
```

The writable endpoint is `kodama-postgres-rw.kodama-backends.svc.cluster.local:5432`.
The migration user owns the database; the runtime user must remain a non-owner
without superuser or RLS-bypass privileges. Kodama’s migration hook grants its
application permissions.

### NATS JetStream

Create a shared authentication token and the TLS Secret from your issued
certificate. Its DNS name must resolve to the NATS service from Kodama’s server
Pods; this example uses `nats.internal.example.com` and a certificate trusted
by the server image’s trust store.

```sh
printf %s "$(openssl rand -hex 32)" > runtime-secrets/nats-token
kubectl -n kodama-backends create secret generic kodama-nats-auth \
  --from-file=token=runtime-secrets/nats-token
kubectl -n kodama-backends create secret tls kodama-nats-tls \
  --cert=credentials/nats-tls.crt --key=credentials/nats-tls.key
```

Save `nats-values.yaml`:

```yaml
fullnameOverride: kodama-nats
config:
  jetstream:
    enabled: true
    fileStore:
      pvc:
        enabled: true
        size: 20Gi
  nats:
    tls:
      enabled: true
      secretName: kodama-nats-tls
  merge:
    authorization:
      token: << $NATS_TOKEN >>
container:
  env:
    NATS_TOKEN:
      valueFrom:
        secretKeyRef:
          name: kodama-nats-auth
          key: token
natsBox:
  enabled: false
```

```sh
helm upgrade --install kodama-nats nats/nats \
  --namespace kodama-backends --version "$NATS_CHART_VERSION" \
  -f nats-values.yaml --wait --timeout 10m
kubectl -n kodama-backends get service kodama-nats
kubectl -n kodama-backends get pods,pvc
```

Point `nats.internal.example.com` at the service through your cluster’s private
DNS. This example uses one persistent NATS server. Configure replication and
backups according to your availability requirements; persistent storage alone
does not establish high availability.

## 3. Install the Kodama Helm chart

Download the chart with its matching release archive:

```sh
curl --fail --location \
  "https://github.com/optimiware/kodama/archive/refs/tags/${KODAMA_REF}.tar.gz" \
  --output kodama-release.tar.gz
tar -xzf kodama-release.tar.gz
export KODAMA_CHART="$PWD/kodama-${KODAMA_REF}/deploy/helm/kodama"
```

### Prepare the installation Secrets

Create raw random keys and the runtime signing key:

```sh
openssl rand -out runtime-secrets/cursor-key 32
openssl rand -out runtime-secrets/idempotency-hash-key 32
openssl ecparam -name prime256v1 -genkey -noout \
  -out runtime-secrets/runtime-signing-key
```

Place the following files in the indicated directories. Passwords in database
URLs must be URL-encoded; the generated hex passwords above need no escaping.

| Directory/file | Value |
| --- | --- |
| `migration-secrets/database-migration-url` | `postgresql://kodama_migration:PASSWORD@kodama-postgres-rw.kodama-backends.svc.cluster.local:5432/kodama?sslmode=verify-full&sslrootcert=/secrets/ca.crt` |
| `runtime-secrets/database-runtime-url` | Same endpoint, with `kodama_runtime` and its own password |
| Both directories: `ca.crt` | Trust bundle containing the PostgreSQL CA and any roots needed by Git |
| `runtime-secrets/runtime-ca.crt` | CA bundle for the HTTPS runtime callback origin |
| `runtime-secrets/s3-access-key`, `s3-secret-key` | Credentials scoped to the artifact bucket |
| `runtime-secrets/git-read-token`, `git-write-token` | Repository read and publisher credentials |

CloudNativePG exposes its generated PostgreSQL CA in the `kodama-postgres-ca`
Secret (`ca.crt` key). Include that certificate in the installation trust bundle;
preserve the public/private roots needed by your other connections.

Save `runtime-secrets/git-credentials.json` with your repository URL:

```json
[
  {
    "ref": "secret://installation/repository-read",
    "username": "git",
    "passwordFile": "git-read-token",
    "urlPrefix": "https://git.example.com/acme/application.git",
    "tenants": ["t_acme"]
  },
  {
    "ref": "secret://installation/repository-write",
    "username": "git",
    "passwordFile": "git-write-token",
    "urlPrefix": "https://git.example.com/acme/application.git",
    "tenants": ["t_acme"]
  }
]
```

Create the Secrets in the release namespace, including the ingress certificate:

```sh
kubectl -n kodama-system create secret generic kodama-runtime \
  --from-file=runtime-secrets/
kubectl -n kodama-system create secret generic kodama-migration \
  --from-file=migration-secrets/
kubectl -n kodama-system create secret tls kodama-tls \
  --cert=credentials/kodama-tls.crt --key=credentials/kodama-tls.key
```

### Set the installation values

Save `kodama-values.yaml`. This example selects AWS KMS and an IAM role for the
server; use your cloud’s equivalent provider block and identity. Replace all
domains, bucket names, role/key identifiers and CIDRs. Configure the CNI and
endpoint addressing so workloads can reach these exact allowed destinations.

```yaml
profile: external
authority:
  mode: standalone
installation:
  id: acme-factory
image:
  repository: ghcr.io/optimiware/kodama
secrets:
  runtime: {existingSecret: kodama-runtime}
  migration: {existingSecret: kodama-migration}
migration:
  runtimeRole: kodama_runtime
serviceAccounts:
  server:
    annotations:
      eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/kodama-server
kms:
  provider: aws
  aws:
    keyId: alias/kodama
    region: eu-west-1
ingress:
  enabled: true
  className: nginx
  host: kodama.example.com
  tls: {enabled: true, secretName: kodama-tls}
config:
  nats:
    url: tls://nats.internal.example.com:4222
  s3:
    endpoint: https://s3.eu-west-1.amazonaws.com
    region: eu-west-1
    bucket: acme-kodama-artifacts
    prefix: kodama/
    pathStyle: false
    credentials: static
  git:
    url: https://git.example.com/acme/application.git
  registry:
    url: https://ghcr.io
  oidc:
    issuer: https://identity.example.com
    audience: https://kodama.example.com/api
    cliClientId: kodama-cli
    redirectUri: http://127.0.0.1:18999/callback
  tls:
    customCA: true
execution:
  tenants:
    - namespace: kodama-tenant-acme
      tenantId: t_acme
  rbac:
    manageSecrets: true
  runtime:
    url: https://kodama.example.com
    signingGeneration: 1
    egress:
      - name: runtime-callback
        cidr: 192.0.2.10/32
        ports: [{port: 443}]
  network:
    egress:
      - name: git
        cidr: 203.0.113.10/32
        ports: [{port: 443}]
      - name: object-storage
        cidr: 198.51.100.0/24
        ports: [{port: 443}]
      - name: registry
        cidr: 203.0.113.20/32
        ports: [{port: 443}]
```

The documented CIDRs are placeholders, not provider IP ranges. Tenant workloads
must not connect to PostgreSQL or NATS. The server’s backend access is separate
from the tenant execution egress policy.

```sh
helm lint --strict "$KODAMA_CHART" -f kodama-values.yaml \
  --set-string image.tag="$KODAMA_REF" \
  --set-string image.digest="$KODAMA_DIGEST"
helm upgrade --install kodama "$KODAMA_CHART" \
  --namespace kodama-system -f kodama-values.yaml \
  --set-string image.tag="$KODAMA_REF" \
  --set-string image.digest="$KODAMA_DIGEST" \
  --wait --timeout 10m

kubectl -n kodama-system rollout status deployment/kodama --timeout=300s
curl --fail "$FACTORY/readyz"
```

The migration hook runs before the server. Readiness must succeed before
continuing. If it does not, inspect `kubectl -n kodama-system logs deployment/kodama`
and the migration Job; verify the release supports your selected KMS and
configuration. See [operations](/en/docs/kodama/operations) for diagnosis.

## 4. Install the CLI and authenticate with OAuth

Install the CLI from the same published release archive:

```sh
mkdir -p "$HOME/.local/bin"
GOBIN="$HOME/.local/bin" go -C "$PWD/kodama-${KODAMA_REF}" install ./cmd/kodama
export PATH="$HOME/.local/bin:$PATH"
kodama --help
```

Configure the factory origin and the endpoints from your OIDC discovery document:

```sh
kodama context set --context acme --select \
  --factory "$FACTORY" --tenant "$TENANT_ID" \
  --issuer https://identity.example.com \
  --authorization-endpoint https://identity.example.com/authorize \
  --token-endpoint https://identity.example.com/token \
  --client-id kodama-cli --scopes openid,kodama \
  --redirect-uri http://127.0.0.1:18999/callback
kodama auth login
kodama context show
```

The browser opens your identity provider. After sign-in, the authorization code
returns to the local callback and the CLI exchanges it using its PKCE verifier.
Credentials use the OS keyring. `--credential-store file` is an explicit option
for a protected `0600` file store. Choose the scopes your issuer uses to issue
the factory-audience access token; `openid,kodama` is illustrative.

### Create the first project

Initialize the first tenant and administrator once using the
**[bootstrap procedure](/en/docs/kodama/operations#bootstrap-the-first-tenant)**.
Use `t_acme`, matching the execution namespace above, and the designated
administrator’s verified OIDC subject. Keep its short-lived `TOKEN` for the
initial project setup, then complete the bootstrap credential cleanup.

As the initial administrator, use a short-lived API access token from your
identity provider’s OAuth flow in `TOKEN`. The bootstrap procedure explains
the one-time credential setup. Save this as `project.json`:

```json
{
  "key": "application",
  "displayName": "Application",
  "activeWipLimit": 5,
  "settings": {
    "repository": {
      "id": "repo_application",
      "url": "https://git.example.com/acme/application.git",
      "baseRef": "refs/heads/main",
      "checkoutSecretRef": "secret://installation/repository-read",
      "publisherSecretRef": "secret://installation/repository-write"
    }
  }
}
```

```sh
curl --fail-with-body -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' --data-binary @project.json \
  "$FACTORY/api/v1/tenants/$TENANT_ID/projects" > project-result.json
export PROJECT_ID="$(jq -r .id project-result.json)"
export USER_ID="$(curl --fail -H "Authorization: Bearer $TOKEN" \
  "$FACTORY/api/v1/me" | jq -r .id)"

jq -n --arg user "$USER_ID" --arg project "$PROJECT_ID" \
  '{userId:$user, projectId:$project, role:"engineer"}' > engineer-binding.json
curl --fail-with-body -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' --data-binary @engineer-binding.json \
  "$FACTORY/api/v1/tenants/$TENANT_ID/memberships"
kodama context use --context acme --project "$PROJECT_ID"
```

This tutorial adds the engineer role to the administrator for the first run.
In a team, grant it to the engineer’s existing user ID instead. Administrative
access alone does not grant engineering or reviewer permissions.

## 5. Register your first pipeline and run it

Save `hello.yaml`. The pinned Alpine image needs no application source, model
provider or deployment credential; it writes a small retained report.

```yaml
apiVersion: kodama/v1alpha1
kind: Pipeline
metadata:
  name: hello
spec:
  inputs:
    name: {type: string, default: world}
  steps:
    - id: hello
      kind: container
      image: docker.io/library/alpine:3.22@sha256:5291449c3df73caf6ed85e649dec1b9e818b39a5d8c871e97afc13e9cd5e8fa8
      workspace: {access: none}
      command: ["/bin/sh", "-ec"]
      args: ['printf "Hello %s\n" "$NAME" > /kodama/outputs/report.txt']
      env:
        NAME: ${{ inputs.name }}
      timeoutSeconds: 60
      outputs:
        report: {type: artifact, path: /kodama/outputs/report.txt}
```

Create `first-run.md` describing the goal and `acceptance.json` containing
`[{"id":"AC-1","text":"The report contains the supplied name."}]`. Then create
and activate the work item:

```sh
kodama work create --kind task --title "First pipeline" \
  --spec-file first-run.md --acceptance-file acceptance.json \
  --output json > work.json
export WORK_ID="$(jq -r .id work.json)"
kodama work transition "$WORK_ID" --to ready \
  --if-version "$(jq -r .version work.json)" --output json > ready.json
kodama work transition "$WORK_ID" --to active \
  --if-version "$(jq -r .version ready.json)"
kodama workspace provision --work "$WORK_ID" --output json > workspace.json
export WORKSPACE_ID="$(jq -r .id workspace.json)"
kodama workspace get "$WORKSPACE_ID" --output json
```

Even a source-free step belongs to a work run with a pinned source baseline.
Wait until the workspace is `available`, then capture its accepted commit and
the current content revision:

```sh
kodama workspace get "$WORKSPACE_ID" --output json > workspace.json
export SOURCE_COMMIT="$(jq -r .acceptedCommit workspace.json)"
export CONTENT_REVISION="$(kodama work get "$WORK_ID" --output json | jq -r .contentRevision)"

kodama pipeline validate --file hello.yaml
kodama pipeline register --file hello.yaml --output json > definition.json
export DEFINITION_ID="$(jq -r .id definition.json)"
kodama run start --work "$WORK_ID" --definition "$DEFINITION_ID" \
  --content-revision "$CONTENT_REVISION" --source-commit "$SOURCE_COMMIT" \
  --input name=Acme --idempotency-key first-hello-001 \
  --output json > run.json
export RUN_ID="$(jq -r .id run.json)"
kodama run watch "$RUN_ID" --exit-on-terminal --timeout 5m
kodama run steps "$RUN_ID" --output json
kodama run logs "$RUN_ID" --step hello --tail 50
```

The run should reach `succeeded` and the `hello` step should expose a `report`
artifact containing `Hello Acme`. The report is a file output, so it need not
appear in stdout logs. Use the returned artifact ID with the artifact API to
retrieve its metadata/download information.

If submission loses its response, repeat the identical command with the same
key. A changed input requires a new key. A successful run leaves business
completion as an explicit human action.

Continue with **[CI/CD, agentic, business-process and business-signal
examples](/en/docs/kodama/developer-guide)**.
