Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

Kubernetes CRD Operator

Purpose

The CRD sync operator is an optional component that makes Orloj resources (Agents, AgentSystems, Tools, etc.) real Kubernetes Custom Resource Definitions. When deployed, you can manage Orloj configuration with kubectl apply, store manifests in Git, and let Argo CD or Flux reconcile them — the operator watches CRD objects and syncs them into the Orloj Postgres store automatically.

Without the operator, Orloj works exactly as before: you create resources via orlojctl apply, the REST API, or the web console. The operator adds an alternative input path; it does not replace anything.

When Do You Need It?

ScenarioOperator needed?
Getting started / local devNo
Small team, orlojctl apply in CINo
GitOps (Argo CD, Flux, Crossplane)Yes
Multi-team platform with RBAC on manifestsYes
Unified kubectl workflow for all cluster resourcesYes
You only use the web consoleNo

How It Works

┌──────────────┐   watch    ┌───────────────────┐   upsert    ┌──────────────┐
│  K8s CRDs    │──────────► │  orloj-operator   │────────────►│  Postgres    │
│  (etcd)      │◄────────── │  (controller-     │             │  (orlojd     │
│              │  status    │   runtime)        │             │   store)     │
└──────────────┘            └───────────────────┘             └──────────────┘
       ▲                                                            │
       │ kubectl apply                                              │ serves
       │                                                            ▼
  Git repo / CI                                              orlojd API + UI
  1. You kubectl apply an Orloj CRD (e.g. Agent).
  2. The operator's reconciler converts the CRD spec to an Orloj resource and upserts it into Postgres.
  3. The resource is now visible in the REST API, web console, and orlojctl get.
  4. On delete, the operator's finalizer (orloj.dev/sync) removes the resource from the store.
  5. A periodic status writer syncs status.phase, observedGeneration, and lastSyncedAt back to the CRD subresource.

Every synced resource gets the annotation orloj.dev/managed-by: crd-sync, which orlojd uses for conflict detection (see CRD conflict policy).

Install

Enable via Helm

helm upgrade --install orloj oci://ghcr.io/orlojhq/charts/orloj \
  --namespace orloj \
  --reuse-values \
  --set operator.enabled=true \
  --set operator.installCRDs=true

This deploys:

  • CRD manifests for all supported resource kinds
  • The orloj-operator Deployment (connects to the same Postgres as orlojd)
  • A ServiceAccount with RBAC for CRD watch/list/get/update/patch and status updates
  • A PodDisruptionBudget (minAvailable: 1)
  • Optional ServiceMonitor for Prometheus scraping

Verify

# CRDs registered
kubectl get crd agents.orloj.dev
 
# Operator running
kubectl -n orloj rollout status deploy/orloj-operator
 
# List Orloj resources (empty initially)
kubectl get agents,agentsystems,tools,mcpservers,modelendpoints,memories,agentpolicies,secrets.orloj.dev

Helm Values Reference

All operator values live under the operator.* key:

ValueDefaultDescription
operator.enabledfalseDeploy the CRD sync operator.
operator.installCRDstrueInstall CRD manifests with the chart.
operator.replicaCount1Operator replicas. Leader election ensures only one is active.
operator.image.repositoryorlojhq/orloj-operatorOperator container image.
operator.image.tag"" (appVersion)Image tag override.
operator.resources.requests.cpu100mCPU request.
operator.resources.requests.memory128MiMemory request.
operator.resources.limits.cpu500mCPU limit.
operator.resources.limits.memory256MiMemory limit.
operator.statusSyncInterval5sInterval for syncing CRD status back to Kubernetes.
operator.leaderElectiontrueEnable leader election for HA.
operator.healthzPort8081Health probe port.
operator.metricsPort8080Prometheus metrics port.
operator.serviceMonitor.enabledfalseCreate a Prometheus ServiceMonitor.
operator.serviceMonitor.interval30sScrape interval.
operator.serviceMonitor.labels{}Extra labels on the ServiceMonitor.
operator.pdb.enabledtrueCreate a PodDisruptionBudget.
operator.pdb.minAvailable1Minimum available replicas.

The operator also requires access to the same Postgres database as orlojd. The chart wires --postgres-dsn and --secret-encryption-key from the shared release secret automatically.

CRD Conflict Policy

When the operator is running, resources it creates are annotated orloj.dev/managed-by: crd-sync. If someone then edits that same resource through the REST API, the change will be overwritten on the next operator reconcile. The --crd-conflict-policy flag on orlojd controls how the API server handles this:

ModeBehavior
offNo conflict detection. REST writes silently proceed.
warn (default)REST writes succeed but orlojd logs a warning and sets the X-Orloj-CRD-Managed response header.
rejectREST writes to CRD-managed resources return 409 Conflict.

Set the policy via Helm:

crdConflictPolicy: reject

Or via the environment variable ORLOJ_CRD_CONFLICT_POLICY.

Namespace Mapping

By default, the operator maps the Kubernetes namespace of a CRD object directly to the Orloj namespace in the store. A resource in K8s namespace team-a is stored in Orloj namespace team-a.

For setups where K8s namespaces don't align with Orloj namespaces (e.g., a single gitops namespace holds all manifests but they target different Orloj namespaces), use the orloj.dev/target-namespace annotation:

apiVersion: orloj.dev/v1
kind: Agent
metadata:
  name: summarizer
  namespace: gitops-manifests           # K8s governance namespace
  annotations:
    orloj.dev/target-namespace: production  # Orloj store namespace
spec:
  model_ref: gpt-4o
  prompt: "You are a concise summarizer."
Annotation set?Orloj namespace used
Nometadata.namespace (K8s namespace)
Yesorloj.dev/target-namespace value

This is useful when:

  • Your K8s namespace structure is governed by platform policy (e.g., one namespace per ArgoCD Application)
  • You have many Orloj namespaces but don't want to create a matching K8s namespace for each
  • You want to decouple K8s RBAC boundaries from Orloj resource organization

If you don't set the annotation, behavior is unchanged — K8s namespace = Orloj namespace.

First Use Walkthrough

1. Enable the operator

helm upgrade --install orloj oci://ghcr.io/orlojhq/charts/orloj \
  --namespace orloj --reuse-values \
  --set operator.enabled=true \
  --set operator.installCRDs=true

2. Apply an Agent CRD

apiVersion: orloj.dev/v1
kind: Agent
metadata:
  name: summarizer
  namespace: default
spec:
  prompt: You are a concise summarizer.
  model_ref: gpt-4o
  limits:
    max_steps: 5
kubectl apply -f summarizer-agent.yaml

3. Verify sync

# CRD status shows "Synced"
kubectl get agent summarizer -o jsonpath='{.status.phase}'
 
# Resource visible via orlojctl
orlojctl get agent summarizer

4. See in the web console

Open the Orloj web console — the agent appears in the Agents list with a "CRD Managed" badge.

GitOps Setup

Argo CD

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: orloj-resources
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/your-org/orloj-config
    targetRevision: main
    path: manifests/orloj
  destination:
    server: https://kubernetes.default.svc
    namespace: default
  syncPolicy:
    automated: { prune: true, selfHeal: true }

Place Orloj CRD manifests (Agents, Tools, AgentSystems, etc.) under manifests/orloj/ in your config repo. Argo CD applies them; the operator syncs them into the store.

Flux

apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: orloj-config
  namespace: flux-system
spec:
  url: https://github.com/your-org/orloj-config
  ref:
    branch: main
  interval: 1m
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: orloj-resources
  namespace: flux-system
spec:
  sourceRef:
    kind: GitRepository
    name: orloj-config
  path: ./manifests/orloj
  prune: true
  interval: 5m

Migrating Existing Resources to CRDs

If you already have resources created via orlojctl apply or the REST API, you can adopt them as CRDs:

  1. Export the resource:
orlojctl get agent my-agent -o yaml > my-agent.yaml
  1. Add the CRD apiVersion and strip runtime status fields:
apiVersion: orloj.dev/v1
kind: Agent
metadata:
  name: my-agent
  namespace: default
spec:
  # ... your existing spec
  1. Apply:
kubectl apply -f my-agent.yaml

The operator upserts the resource. Because the name matches, the existing store entry is updated and adopted — it gains the orloj.dev/managed-by: crd-sync annotation. From this point, manage it via kubectl and Git.

Secrets and GitOps

For secrets that tools and MCP servers need at runtime, you have two options:

Option A: Orloj Secret CRD — store secrets directly in the Orloj store via the Secret CRD. Simple but means plaintext values in your manifests (not safe to commit to Git without additional encryption).

Option B: K8s-native Secrets + secretRef (recommended for GitOps) — use an external secrets operator (Bitnami Sealed Secrets, External Secrets Operator, HashiCorp Vault) to manage K8s-native Secrets, then reference them from Orloj resources:

apiVersion: orloj.dev/v1
kind: Tool
metadata:
  name: my-api-tool
spec:
  type: http
  endpoint: "https://api.example.com"
  auth:
    secretRef: my-k8s-secret   # K8s-native Secret created by Sealed Secrets

The KubernetesSecretResolver reads K8s-native Secrets at runtime. This keeps secrets out of Git, leverages K8s RBAC and audit logging, and integrates with existing secret management tooling.

Phase 1 Scope

The operator currently syncs these 8 resource kinds:

CRDAPI GroupStore Kind
agents.orloj.devorloj.dev/v1Agent
agentsystems.orloj.devorloj.dev/v1AgentSystem
tools.orloj.devorloj.dev/v1Tool
mcpservers.orloj.devorloj.dev/v1McpServer
modelendpoints.orloj.devorloj.dev/v1ModelEndpoint
memories.orloj.devorloj.dev/v1Memory
agentpolicies.orloj.devorloj.dev/v1AgentPolicy
secrets.orloj.devorloj.dev/v1Secret

Runtime resources (Tasks, TaskSchedules, TaskWebhooks, Workers) are not CRDs — they are created through the API at runtime or via orlojctl.

Relationship to Agent K8s Execution

The CRD operator and the Kubernetes agent/tool execution backends are orthogonal features that stack:

FeatureWhat it doesHelm values
CRD OperatorSyncs resource definitions (Agents, Tools, ...) from K8s CRDs into the Orloj store. Manages the configuration plane.operator.enabled
Tool K8s IsolationRuns individual tool invocations as ephemeral K8s Jobs. Manages the execution plane for tools.toolIsolation.kubernetes.enabled
Agent K8s ExecutionRuns entire agent steps as ephemeral K8s Jobs. Manages the execution plane for agents.agentExecution.kubernetes.enabled

You can use any combination: CRDs without K8s execution, K8s execution without CRDs, or all three together.

Related Docs