- the operator in
chatcli-system, with its REST API, web dashboard, metrics and an optional NetworkPolicy; - a ChatCLI server (
Instance) inchatcli, 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) andopenssl.- 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
chatcliCLI 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.1. Install the operator
Write a values file. Every key below exists in the chart’s schema, and the schema rejects unknown keys.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.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.
- cert-manager (recommended)
- openssl (self-signed)
An internal CA issuer writes
tls.crt, tls.key and ca.crt into the Secret, and renews the certificate. Each renewal rolls the Instance pods.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
- A Deployment
chatcli-prodwhose pods run as non-root with a read-only root filesystem, drop all capabilities, and meet therestrictedPod Security Standard. Startup, readiness and liveness probes callGET /healthzon the metrics port. - A Service
chatcli-prodwith portsgrpcandmetrics. Withreplicas > 1it 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).
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.
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.
True and ServerReachable reads Serving:
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.
4. Link source repositories (optional)
ASourceRepository 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).
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:
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’ssecretRef 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.
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 reachesEscalated, 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, withescalation-notified-atfor the last page). EachtimeoutMinutesmoves it one level up, until the last level, where it stays.chatcli_operator_escalation_level_reachedgoes up by one for every level reached. repeatIntervalMinutesre-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 recordsacknowledgedAtandacknowledgedBy(the caller’s API key role) instatus.activeEscalations. - Snooze (
POST /api/v1/incidents/<name>/snoozewith a positiveduration, such as"30m") holds every notification of the Issue exceptResolved, and holds the escalation. A level page that falls inside the snooze is markedescalation-pending-notifyand 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.signalType: slo_violation, which the slo-violations rule from step 5 routes.
SLA
AnIncidentSLA 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.
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:
/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.9. Approvals
AnApprovalPolicy 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.
autowithoutautoApproveConditionsmeans the policy does not gate the plan. Anautorule withautoApproveConditionsparks the plan and approves it automatically only when every condition holds (maxSeverityis 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 underlow-risk-auto.defaultModeis informational:kubectl get apshows 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.DrainNodecordons the node and evicts its pods through thepolicy/v1Eviction API with a 30-second grace period, so PodDisruptionBudgets are honored. Evictions refused with429or a server error are retried every 5 seconds until the optionaltimeoutparam (default2m, at most10m); 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 EventApprovalGateUnavailableis 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.
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.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:
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 indeploy/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):
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.pod_killandpod_failurework with the RBAC the chart ships.cpu_stress,memory_stressanddisk_stresscreate a stress pod, which the chart’s RBAC allows; the pod passes therestrictedPod Security Standard (UID/GID 65534, non-root,RuntimeDefaultseccomp, no privilege escalation, all capabilities dropped, read-only root filesystem with anemptyDirat/tmp, no ServiceAccount token).network_delayandnetwork_lossare not supported: the experiment fails inPendingwith a message that says nothing was injected.requireApproval: trueraises an ApprovalRequestchaos-<experiment>(requested actionCustom, 30-minute timeout). The experiment runs once it is approved; a rejection or expiry fails the experiment.scheduleis not supported: an experiment that sets it fails inPendingand nothing runs. Create one ChaosExperiment per run.recoveryTimeis measured from the end of the injection to the first passing health check (checks run every 5 seconds);recoveryVerifiedis the pass/fail signal. Issues from the drill are left out of MTTD and MTTR.
13. Verify the installation
1
Operator and CRDs
2
Instance conditions
True, as in step 3.3
The alert bridge is connected
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
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(pluslocalhostfor checks), with renewal in place - Instance has explicit
resources, anti-affinity, and persistence consistent withreplicas - All five Instance conditions
True, andServerReachableisServing - 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
approvalrequestslimited 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.