Skip to main content
This runbook takes an empty cluster to a working ChatCLI AIOps platform. Every manifest below validates against the current CRDs. Run the steps in order. Most of them end with a check to run before you move on. At the end you will have:
  • the operator in chatcli-system, with its REST API, web dashboard, metrics and an optional NetworkPolicy;
  • a ChatCLI server (Instance) in chatcli, with a credential, TLS, explicit resources and a watcher on your production workloads;
  • notification, escalation, SLO, SLA and approval policies;
  • Grafana dashboards and Prometheus scraping;
  • a chaos drill that shows the pipeline detecting a failure.

Prerequisites

  • A Kubernetes cluster, and cluster-admin rights for the install (the chart creates CRDs, ClusterRoles and ClusterRoleBindings).
  • kubectl, Helm 3.8 or later (for OCI charts; Helm 4 included) and openssl.
  • At least one LLM provider key (the examples use Anthropic and OpenAI).
  • For observability: the Prometheus Operator (for ServiceMonitor) and Grafana with the dashboard sidecar. Both are optional.
  • The chatcli CLI on your workstation, for the end-to-end check in Verify.

Where each resource lives

The operator looks resources up in specific namespaces. Putting one in the wrong namespace does not fail. It is silently ignored.
Only one Instance per cluster drives AIOps. The operator’s alert bridge connects to the first ready Instance it lists across all namespaces, and the AI analysis and remediation use that same connection. If several Instances are ready, which one it picks is not defined. Run a single Instance with a watcher per cluster.

1. Install the operator

Write a values file. Every key below exists in the chart’s schema, and the schema rejects unknown keys.
The chart installs the 17 CRDs, the operator Deployment, its Service (chatcli-operator, ports metrics 8080, health 8081, api 8090), cluster RBAC, the pre-provisioned chatcli-watcher ClusterRole, and a pre-install/pre-upgrade hook that re-applies the CRDs on every helm upgrade. It pins the server image that Instances use to the same release, unless an Instance sets spec.image.tag. Check:
The operator chart has no PodDisruptionBudget template. With replicaCount: 2, create one yourself if node drains must keep a replica up (selector app.kubernetes.io/name: chatcli-operator).

2. Create the Instance Secrets

All of these go in the Instance’s namespace.
The operator watches every Secret an Instance references. When you rotate one, the pods roll automatically, because a hash of the referenced keys is stamped on the pod template.

TLS certificate

The operator always dials the server over TLS 1.3. It has no plaintext mode, so an Instance without TLS runs but the operator cannot reach it (ServerReachable=False), and alerts, AI analysis and remediation stop there. The operator dials <instance>.<namespace>.svc.cluster.local, here chatcli-prod.chatcli.svc.cluster.local. The certificate must be valid for that name. If it is not signed by a public CA, the Secret must also carry the CA as ca.crt, which the operator uses as its trust root for this Instance.
There is no Instance field for the address the operator dials. It is always the Instance Service name above, on spec.server.port. Operator-wide overrides exist for the trust root and a client certificate (chart security.grpcTLS.*), but a ca.crt in the Instance’s own Secret is simpler and wins over them.

3. Create the production Instance

What the operator creates, and what it does not:
  • A Deployment chatcli-prod whose pods run as non-root with a read-only root filesystem, drop all capabilities, and meet the restricted Pod Security Standard. Startup, readiness and liveness probes call GET /healthz on the metrics port.
  • A Service chatcli-prod with ports grpc and metrics. With replicas > 1 it is headless, and the operator balances over the pods.
  • A ServiceAccount, and for watcher targets in other namespaces a ClusterRoleBinding to chatcli-watcher, so the server can read those workloads.
  • No PodDisruptionBudget, HPA or NetworkPolicy. Create them yourself if you need them (a NetworkPolicy example follows).
Persistence and replicas. persistence.enabled: true creates one ReadWriteOnce PVC (chatcli-prod-sessions) that every replica mounts. The access mode cannot be changed. With replicas > 1, pods scheduled on different nodes stay ContainerCreating with a Multi-Attach error. Either keep replicas: 1 with persistence, or run several replicas without it, as above. With persistence the operator sets the Deployment strategy to Recreate, so the old pod releases the volume before the new one starts: each rollout has a brief downtime. Without it the strategy is RollingUpdate. The AIOps pipeline does not need the session volume. Without it, sessions of remote chatcli connect users live only as long as the pod does.
Fallback. The server builds its fallback chain only when at least two of the listed providers have credentials, and uses it only for requests that bring no credentials or provider of their own. The AIOps pipeline’s requests qualify.
A NetworkPolicy for the Instance, allowing gRPC from the operator’s namespace and scrapes from monitoring (add the namespaces of your chatcli connect users):
Kubelet probes are exempt from NetworkPolicy on most CNIs, but not all. If the pods fail readiness after you apply the policy, also allow port 9090 from your node CIDR.
Check: every condition is True and ServerReachable reads Serving:
The VERSION column of kubectl get instance shows the server version the operator last read. A ready Instance is re-probed every five minutes, so allow up to five minutes for ServerReachable to reflect a change; after a failed probe it is retried every 30 seconds. A SourceRepository gives the AI recent commits and code around stack traces for one workload. Create it, and its git Secret, in the workload’s namespace. The operator clones it into its writable /tmp emptyDir (chart value tmpVolume.sizeLimit, step 1).
The Secret keys depend on authType: token for token, username and password for basic, ssh-key and known_hosts for ssh. HTTPS credentials reach git through a GIT_ASKPASS helper for each command and are never written to the clone’s .git/config; a clone made by an earlier version, which kept the token in the origin URL, has it stripped from origin and its reflog expired on the next sync. The operator image ships openssh-client, and an SSH clone checks the server’s host key against the Secret’s known_hosts with StrictHostKeyChecking=yes:
Without known_hosts an SSH sync fails (secret ... has no "known_hosts" key) unless the SourceRepository sets spec.sshHostKeyPolicy: acceptNew, which trusts the key the host presents on first contact and rejects a different key afterwards (remembered for the life of the operator pod). The default, strict, is the safe choice: pin the host keys in the Secret.
Check: kubectl -n production get sourcerepositories shows the repository ready after the first sync.

5. Notifications

Put the webhook URLs, routing keys and passwords in a Secret. Every key of a channel’s secretRef Secret is merged into its config. Keep them out of config itself: GET /api/v1/policies/notification returns the whole spec to any key with the viewer role.
Keep deduplicationWindow short. A notification inside the window is dropped, not queued, so with the default 5m an Issue that resolves quickly may never send Resolved to Slack or email. Resolved to PagerDuty and OpsGenie is never throttled, by the window or by maxPerHour, so the page always closes. Contained means the platform silenced the impact (for example, scaled a workload to zero) without fixing the cause, so a human must act: route it to your pager. See Notifications for every channel’s keys and the templates.

6. Escalation

An escalation starts when an Issue reaches Escalated, which happens when remediation attempts run out. It stops when the Issue is Resolved or when someone acknowledges it. The operator uses the first enabled policy, from any namespace, whose severities include the Issue’s.
targets are informational: they are written into the message, and the message goes to each level’s notifyChannels, looked up by name in the enabled NotificationPolicies.
How an escalation behaves:
  • The current level is saved on the Issue (platform.chatcli.io/escalation-level, with escalation-notified-at for the last page). Each timeoutMinutes moves it one level up, until the last level, where it stays. chatcli_operator_escalation_level_reached goes up by one for every level reached.
  • repeatIntervalMinutes re-sends the current level’s message at that interval; on the last level it keeps repeating. 0 (the default) never repeats.
  • Acknowledge (dashboard, or POST /api/v1/incidents/<name>/acknowledge) stops the escalation where it is: no further level, no repeat. The EscalationPolicy records acknowledgedAt and acknowledgedBy (the caller’s API key role) in status.activeEscalations.
  • Snooze (POST /api/v1/incidents/<name>/snooze with a positive duration, such as "30m") holds every notification of the Issue except Resolved, and holds the escalation. A level page that falls inside the snooze is marked escalation-pending-notify and goes out when the snooze ends; the level’s timer restarts at that moment.
  • Issues from a chaos drill never escalate.

7. SLOs and SLAs

SLO

SLOs are computed from the operator’s own Issues and Anomalies for the resource, not from Prometheus. Create the SLO in the workload’s namespace.
A firing burn-rate window opens an Issue with signalType: slo_violation, which the slo-violations rule from step 5 routes.
  • pageOnBudgetExhausted: true opens one critical Issue when the budget reaches zero. It pages again only after the budget has climbed back above zero and run out again; while that exhaustion Issue is still open, no second one is created.
  • The SLO’s own slo_violation Issues do not count as downtime, so a page does not deepen the breach it reports.
  • metricSource: prometheus is not queried: the SLI is still computed from Issues and Anomalies, and the SLO reports the condition MetricSourceSupported=False. prometheusQuery, latencyPercentile and latencyThresholdMs are not evaluated, and alertPolicy.notificationPolicyRef is reserved: SLO Issues are notified by every NotificationPolicy whose rules match them.

SLA

An IncidentSLA in the Issue’s namespace wins. Otherwise, one with the same severity from any namespace applies, so a set in chatcli-system works as the cluster-wide default.
A breach is recorded in the IncidentSLA status, on the Issue (platform.chatcli.io/sla-violated, plus platform.chatcli.io/sla-name naming the SLA that counted it) and in the audit trail. status.activeViolations goes back down when the Issue resolves. An invalid duration sets the SLA’s Ready condition to False with reason InvalidDuration. A breach does not send a notification: escalationPolicyRef and notificationPolicyRef are reserved and not read. Alert on chatcli_operator_sla_violations_total instead. See SLOs and SLAs for the calculation and the status fields.

8. Dashboard and REST API keys

The REST API and the web dashboard share port 8090 on the operator Service. Every /api/ call needs an X-API-Key header. Keys come from the Secret chatcli-operator-secrets (key api-keys) in the operator’s namespace. The name is fixed. Without it, every /api/ call returns 401.
Any other role string gets no access at all. The operator re-reads the Secret every 30 seconds, so key changes need no restart. Editing the Secret to remove a key revokes it, and an empty list leaves no valid key. YAML that does not parse keeps the last valid key set in force (the operator logs it once per version), so fix the typo rather than relying on it to revoke. A Secret without an api-keys entry falls back to the ConfigMap; deleting both the Secret and the ConfigMap chatcli-operator-config revokes every key within about 30 seconds (401 from then on). A read error other than not found keeps the keys in force. Startup applies the same rules as the poll. Each entry also accepts an optional name, the identity recorded on the approvals that key makes (falling back to description, then to a key-<hash> fingerprint). A quorum counts each key once, so give every approver their own key. Instead of kubectl, the chart can render the same Secret (--set apiKeys.create=true with apiKeys.entries), but the keys are then stored in the Helm release. Open the dashboard:
To publish it, serve it at the root path of its own host. The dashboard calls /api/v1/... with an absolute path, so a sub-path with rewrite-target loads the page but breaks every API call.
The REST rate limit counts requests without a valid key per client host (30 per minute), and each valid key on its own (600 per minute). Behind an ingress, the client host is the ingress controller, so all unauthenticated traffic shares one bucket. For TLS on port 8090 itself, mount a certificate with extraVolumes/extraVolumeMounts and set security.apiTLS.certFile/keyFile. There is no SSO or OIDC login: access is by API key only.
Notes for other ingress controllers, TLS served by the operator itself and local clusters are in Web Dashboard: Accessing and publishing.

9. Approvals

An ApprovalPolicy gates RemediationPlans in its own namespace. Within a policy, the first matching rule wins. A rule’s actionTypes match when any action in the plan is listed. Put the strict rules first, and end with a catch-all.
How to read it:
  • auto without autoApproveConditions means the policy does not gate the plan. An auto rule with autoApproveConditions parks the plan and approves it automatically only when every condition holds (maxSeverity is compared with the Issue severity; an unknown severity does not meet it); otherwise a human decides. A plan that contains a restart and an action no earlier rule lists also runs under low-risk-auto.
  • defaultMode is informational: kubectl get ap shows it, but the operator does not apply it. Without a matching rule the plan is not gated by the policy, which is why the last rule (match: {}) is a catch-all that gates everything else.
  • DrainNode cordons the node and evicts its pods through the policy/v1 Eviction API with a 30-second grace period, so PodDisruptionBudgets are honored. Evictions refused with 429 or a server error are retried every 5 seconds until the optional timeout param (default 2m, at most 10m); after that the action fails, naming the pods it could not evict. DaemonSet and mirror pods are skipped. It still belongs behind a manual rule.
  • The gate fails closed. If the Issue, the AIInsight or the ApprovalPolicies cannot be read, or the decision engine errors, the plan stays Pending, a Warning Event ApprovalGateUnavailable is recorded on it, and it is retried with backoff. A plan whose parent Issue is gone fails (“Parent issue not found…”). A missing AIInsight counts as confidence 0, which only makes the gate stricter.
Approve with an annotation on the ApprovalRequest (named approval-<plan>, in the plan’s namespace). Restrict update/patch on approvalrequests with RBAC to the people allowed to approve, because the approver name is free text:
The dashboard and POST /api/v1/approvals/{name}/approve (or /reject) record one decision on a Pending request, the same way the annotation does. The approver is stored as <typed name> (api-key: <key identity>), and the ApprovalReconciler then evaluates requiredApprovers and the change window. Over REST a quorum counts distinct API keys, so a shared key cannot satisfy requiredApprovers: 2; annotation approvers are counted by name, case-insensitively. Any rejection rejects the request. A request that is not Pending, or a second decision by the same key, answers 409.
The change window only gates when a decision takes effect: decisions are recorded at any time, and a rejection applies immediately. The timeout clock runs only while the window is open, and a request already approved that waits only for the window does not expire; it turns Approved when the window opens, with the condition ChangeWindow=False (reason OutsideChangeWindow) meanwhile. A window that can never open blocks approval and expires on wall-clock time. Two more gates can park a plan without any policy: the cluster tier (chart clusterName, naming a ClusterRegistration) and the decision engine (chart decisionEngine.enabled). See Approval Workflow and Decision Engine.

10. Prometheus

Two separate integrations: Scraping. The operator exposes /metrics on port 8080 (chatcli_operator_*). Each Instance exposes /metrics on its metrics port (chatcli_grpc_*, chatcli_llm_*, chatcli_watcher_*, chatcli_server_*). Both are plain HTTP without authentication, so restrict who can reach them with NetworkPolicy (steps 1 and 3). The chart’s serviceMonitor.enabled covers the operator. For the Instance:
deploy/grafana/dashboards-configmap.yaml holds only two ServiceMonitors, not the dashboards. Its server ServiceMonitor selects app.kubernetes.io/name: chatcli and the port metrics, which both Instance Services (operator) and the server Helm chart carry, in any namespace; apply it, or keep the narrower Instance ServiceMonitor above.
Queries during analysis. With prometheusUrl set on the operator chart (environment variable PROMETHEUS_URL), the operator queries Prometheus for the incident’s time range and adds the results to the AI analysis: CPU, memory, restarts and network per pod, request and error rates, latency percentiles and HPA replicas. It calls /api/v1/query_range with no authentication, so the URL must reach an unauthenticated Prometheus HTTP API, usually the in-cluster Service. Container metrics come from cAdvisor and restarts from kube-state-metrics. Without it, analysis still runs, without these metrics.

11. Grafana dashboards

The repository ships four dashboards in deploy/grafana/. Their panels reference the datasource ${DS_PROMETHEUS}, which the Grafana import dialog resolves, but sidecar provisioning does not. For the sidecar, substitute your Prometheus datasource UID first (prometheus in kube-prometheus-stack; check Connections → Data sources in Grafana):
Or import each JSON file with Dashboards → New → Import and pick the datasource there. Every panel queries a metric the operator or the server exports; see Web Dashboard for what each dashboard shows.

12. Validate with a chaos drill

A drill proves the loop end to end: a failure is injected, the watcher sees it, an Issue is opened and labeled as a drill, and the workload recovers.
Run drills only against workloads with enough replicas. Nothing is protected by default: every guardrail below is opt-in, and no namespace is blocked unless you list it.
What to look for:
Limitations to know before you plan drills:
  • pod_kill and pod_failure work with the RBAC the chart ships. cpu_stress, memory_stress and disk_stress create a stress pod, which the chart’s RBAC allows; the pod passes the restricted Pod Security Standard (UID/GID 65534, non-root, RuntimeDefault seccomp, no privilege escalation, all capabilities dropped, read-only root filesystem with an emptyDir at /tmp, no ServiceAccount token). network_delay and network_loss are not supported: the experiment fails in Pending with a message that says nothing was injected.
  • requireApproval: true raises an ApprovalRequest chaos-<experiment> (requested action Custom, 30-minute timeout). The experiment runs once it is approved; a rejection or expiry fails the experiment.
  • schedule is not supported: an experiment that sets it fails in Pending and nothing runs. Create one ChaosExperiment per run.
  • recoveryTime is measured from the end of the injection to the first passing health check (checks run every 5 seconds); recoveryVerified is the pass/fail signal. Issues from the drill are left out of MTTD and MTTR.
See Chaos Engineering for every type, parameter and state.

13. Verify the installation

1

Operator and CRDs

2

Instance conditions

All five conditions must be True, as in step 3.
3

The alert bridge is connected

Expect Connected to Instance followed by Alert stream open on the leader. Connected to Instance alone proves nothing, because the connection is opened lazily. TLS and credential errors surface in ServerReachable and in the stream errors after it.
4

gRPC end to end, with TLS and the token

This relies on the localhost SAN from step 2.
5

REST API and dashboard

6

Metrics

In Prometheus, up{namespace="chatcli-system"} and up{namespace="chatcli"} are 1, and chatcli_operator_instance_ready and chatcli_server_info return series.
7

Policies are loaded

8

The pipeline reacts

Run the drill from step 12 and watch an Issue appear in production and in the dashboard.

14. Troubleshooting

Production checklist

  • Operator installed with a pinned --version, 17 CRDs present, replicaCount: 2
  • Operator NetworkPolicy enabled, with API and metrics ingress restricted
  • Instance Secrets created: LLM keys, token, TLS (tls.crt, tls.key, ca.crt), encryption key
  • Certificate valid for chatcli-prod.chatcli.svc.cluster.local (plus localhost for checks), with renewal in place
  • Instance has explicit resources, anti-affinity, and persistence consistent with replicas
  • All five Instance conditions True, and ServerReachable is Serving
  • Only one Instance with a watcher in the cluster
  • Instance NetworkPolicy and, if needed, a PodDisruptionBudget created by you
  • Dashboard keys in chatcli-operator-secrets, one per team and role, admin keys kept few
  • Dashboard published only over TLS, on its own host
  • NotificationPolicy with credentials in Secrets and a short deduplicationWindow
  • EscalationPolicy with its levels and, where a page must repeat, repeatIntervalMinutes
  • SLOs and ApprovalPolicies in the workloads’ namespaces
  • ApprovalPolicy with node actions and risky changes first, and a catch-all rule last
  • RBAC on approvalrequests limited to approvers
  • ServiceMonitors scraping the operator and the Instance
  • Grafana dashboards provisioned with the datasource UID substituted
  • A chaos drill run in dry-run first, then for real, with recovery verified
  • Operator values kept in a file (helm get values chatcli-operator -n chatcli-system -o yaml) for the next upgrade

Next steps

Security hardening

Credentials, TLS, audit and what the platform does not protect.

K8s Operator

Every Instance field, what the operator creates, and how to upgrade it.

Approval Workflow

Modes, change windows and approval requests.

REST API

Endpoints, roles and rate limits.