Feat/aiworks - #167
Conversation
New chart parallel to deepgram-self-hosted. Backend-only (no frontend) for v1; backed by voiceworks-self-hosted image. Independent versioning. License-proxy referenced by URL — not bundled. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ConfigMap renders the full config.toml from values, including [license],
[server], [storage], [deepgram], and [self_hosted] blocks. Credentials
Secret holds DEEPGRAM_API_KEY and VW_API_KEY; a separate
kubernetes.io/tls Secret holds inline TLS material when
server.tls.enabled and no existingSecret is provided.
Relies on voiceworks's ${VAR} resolution on [self_hosted].api_key
(landed in voiceworks commit 9302010).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Deployment runs voiceworks-self-hosted as non-root UID 10001 with readOnlyRootFilesystem, drops all capabilities, mounts the ConfigMap at /etc/voiceworks/config.toml and TLS material at /etc/voiceworks/tls. Three probes (readiness/liveness/startup) hit /api/health on http/https based on tls.enabled. Config-change pod restart is forced via checksum annotation on the ConfigMap. Service is ClusterIP by default; Ingress (next task) handles external exposure. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…viceMonitor Each optional resource is gated on a values flag; defaults render none of them. Files-mode persistence is on by default (PVC included unless existingClaim is set or persistence is disabled). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…econform) helm-test Pod curls /api/health on the Service post-install (30s timeout with 2s polling). CI workflow runs helm lint + helm template + kubeconform across three values files (default, files-mode, ingress+mTLS). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds prerequisites, quick start, examples for the four most common deployment shapes (files+open mode, ingress, in-process TLS, postgres), upgrade and uninstall recipes, and a hand-curated values reference table covering the most commonly-tuned knobs. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When TLS is enabled the health check now curls https:// with -k so the smoke test passes regardless of whether the in-process cert is trusted by the curlimages/curl trust store. Plain http:// is used when TLS is off. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The version-bump detection step previously hardcoded deepgram-self-hosted and gated the whole job on it, so deepgram-aiworks was never released. The step now checks both charts via a shared helper function and proceeds when either has a bumped version. The helper handles the "chart is new" edge case (no Chart.yaml in HEAD^) by treating a missing prior version as a bump. chart-releaser-action already auto-discovers all charts/ subdirs, so no changes to that step are needed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
I will be taking a look at this! |
jkroll-deepgram
left a comment
There was a problem hiding this comment.
Solid bones — sensible security defaults (non-root 10001, read-only rootfs, drop ALL caps), checksum-pinned configmap restart, three meaningful CI scenarios, decent values-reference docs. Inline comments cover the specific bugs / footguns I'd want addressed; this top-level note is for the cross-cutting convention gaps with deepgram-self-hosted's CI.
Missing repo conventions:
- No
CHANGELOG.md. The existing chart has one andci.ymlfails the PR if a non-doc change to the chart skips the changelog. The newaiworks-chart-ci.ymldoesn't enforce this. Either add aCHANGELOG.md+ the equivalent CI check, or accept the inconsistency knowingly. - No
helm-docs.ci.ymlregenerates thedeepgram-self-hostedREADME fromvalues.yamlcomments and fails on drift. The new chart's README is hand-written and unenforced —server.prefix,server.wsIdleTimeout,storage.postgres.dsnExistingSecret,storage.postgres.dsnExistingSecretKeyare already missing from the values-reference table. Recommend adoptinghelm-docsfor parity. - No chart-version-bump CI check. Existing CI fails a PR if
appVersionbumped withoutversionbumping. New CI has no equivalent — onlypublish.ymlenforces it at release time. Cheap to add and prevents a "merged but didn't publish" surprise.
Risks worth flagging (already acknowledged in the PR body but restating for the record):
- Chart depends on voiceworks
feat/self-hostedlanding first for${VW_API_KEY}env resolution. Recommend either holding merge until that lands, or tagging this release in a waypublish.ymlwon't pick up automatically. - The "first chart release" branch of
publish.yml(emptyOLD_VERSION⇒ treated as bump) is exercised here for the first time. A dry-run withactorworkflow_dispatchagainst a throwaway repo would derisk it.
| {{- if .Values.server.tls.enabled }} | ||
| cert = "/etc/voiceworks/tls/tls.crt" | ||
| key = "/etc/voiceworks/tls/tls.key" | ||
| {{- if or .Values.server.tls.clientCaBundle .Values.server.tls.clientCaBundleExistingSecret }} |
There was a problem hiding this comment.
clientCaBundleExistingSecret is honored here but nowhere else in the chart — secret.yaml only writes ca.crt from the inline clientCaBundle, and deployment.yaml only mounts one TLS volume (the chart-managed or server.tls.existingSecret-referenced Secret). If an operator sets clientCaBundleExistingSecret, config.toml will reference /etc/voiceworks/tls/ca.crt but no such file is mounted, and voiceworks will fail to start.
Two ways out:
- Drop
clientCaBundleExistingSecretand require the CA bundle to live in the same TLS Secret astls.crt/tls.keyunder the keyca.crt. Document the expected key names in the README. - Or add a second volume/volumeMount that pulls
ca.crtfromclientCaBundleExistingSecretand projects it next totls.crt.
| updateStrategy: | ||
| type: RollingUpdate | ||
| rollingUpdate: | ||
| maxUnavailable: 0 |
There was a problem hiding this comment.
Heads up: this default interacts badly with the other defaults on this chart. With replicaCount: 1 (line 17), storage.files.persistence.enabled: true (line 46), and pvc.yaml hardcoding accessModes: ["ReadWriteOnce"], a rolling update has to schedule the new pod before terminating the old one — but the new pod can't mount the RWO volume while the old one holds it. The roll stalls indefinitely.
Same shape blocks HPA: scaling.auto.maxReplicas: 5 + files mode + RWO can't scale past 1.
Options:
- Default
maxUnavailable: 1(correct forreplicaCount: 1) and let operators tune up when they scale. - Expose
accessModesinvalues.yamlso RWX-capable clusters can opt in. - Add a
fail/requiredcheck in templates when the combination is incompatible (replicas > 1+ files + RWO).
| curl -sL https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz | tar -xz -C /usr/local/bin | ||
|
|
||
| - name: kubeconform render | ||
| run: kubeconform -strict -summary -kubernetes-version 1.28.0 -ignore-missing-schemas /tmp/rendered.yaml |
There was a problem hiding this comment.
-ignore-missing-schemas silently accepts any kind whose schema isn't in the bundle, including kinds introduced by a typo (e.g., kind: Deployement). The only kind in this chart that needs the escape hatch is ServiceMonitor (from the prometheus-operator CRDs).
Tighter options:
-skip monitoring.coreos.com/v1/ServiceMonitor— skip just that one kind.-schema-location 'default' -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'— load community CRD schemas so ServiceMonitor (and others) get validated.
| {{- if .Values.networkPolicy.egressTo }} | ||
| - Egress | ||
| {{- end }} | ||
| {{- with .Values.networkPolicy.ingressFrom }} |
There was a problem hiding this comment.
If an operator sets networkPolicy.enabled: true but leaves ingressFrom: [] (the default in values.yaml), the {{- with ...}} block elides the entire ingress: clause. The rendered Policy then declares policyTypes: [Ingress] with no rules — k8s reads that as "deny all ingress to selected pods."
The Service stops accepting any traffic, including from kube-proxy / probes. Failure mode looks like "everything is down" with no obvious cause.
Fixes:
{{- required "networkPolicy.ingressFrom must be set when networkPolicy.enabled" .Values.networkPolicy.ingressFrom }}to fail at template time, or- Default to an
ingressFromrule like- podSelector: {}(same namespace) and document the implication, or - Document loudly in the README that empty
ingressFromis default-deny.
| # -- Alternative: reference an existing Secret rather than letting the chart create one. | ||
| apiKeyExistingSecret: "" | ||
| # -- Key inside apiKeyExistingSecret that holds the license UUID. | ||
| apiKeyExistingSecretKey: "DEEPGRAM_API_KEY" |
There was a problem hiding this comment.
This field controls two unrelated things: the key the chart reads inside secrets.existingSecret (the BYO Secret path) and the key the chart writes in its own credentials Secret (secret.yaml). If an operator changes it on an existing release with secrets.existingSecret: "", the chart-created Secret renames its key — but deployment.yaml's env ref also follows the rename, so it superficially keeps working until something else (a sidecar, an in-place patch, an external reader) trips over the new key name.
Suggest splitting the responsibility:
- Hardcode the chart-written key names (
DEEPGRAM_API_KEYandVW_API_KEYare standard) insidesecret.yamlanddeployment.yaml. - Keep
*ExistingSecretKeyas a read-only knob that only applies whensecrets.existingSecretis set.
| - port: http | ||
| interval: {{ .Values.serviceMonitor.interval }} | ||
| scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }} | ||
| path: /metrics |
There was a problem hiding this comment.
This scrapes /metrics on the same Service port that handles /api/health and the WebSocket execute endpoint. Worth verifying voiceworks actually exposes Prometheus metrics on the main listener — many backends route them onto a separate :9090 (or :3001, etc.) admin port that isn't in the Service or container spec.
If it's a separate port, this ServiceMonitor will scrape voiceworks's app router and either 404 (Prom marks the target down) or, worse, return HTML that breaks the parser. If it's the same port, leaving a comment in values.yaml to that effect would save the next operator a debugging session.
| | `server.tls.enabled` | bool | `false` | Terminate TLS in voiceworks itself | | ||
| | `server.tls.cert` | string | `""` | PEM cert chain (inline; required when `tls.enabled` and no `existingSecret`) | | ||
| | `server.tls.key` | string | `""` | PEM private key (inline) | | ||
| | `server.tls.existingSecret` | string | `""` | Reference an existing `kubernetes.io/tls` Secret | |
There was a problem hiding this comment.
The chart mounts this Secret at /etc/voiceworks/tls and references tls.crt / tls.key / ca.crt from config.toml. Operators bringing their own Secret need to know which keys to put in it. Suggest tightening this row to something like:
Reference an existing
kubernetes.io/tlsSecret. Must contain keystls.crt,tls.key, and (whenclientCaBundleis set)ca.crt.
Same note belongs on secrets.existingSecret (line 197) — that one needs the keys named by license.apiKeyExistingSecretKey and selfHosted.apiKeyExistingSecretKey (default DEEPGRAM_API_KEY and VW_API_KEY).
Add
deepgram-aiworksHelm chartNew chart at
charts/deepgram-aiworks/for deploying VoiceWorks(AIWorks) self-hosted on Kubernetes. Parallel to
deepgram-self-hosted,independently versioned, backend-only (no frontend) for v1.
Summary
(renders
config.tomlfrom values), Secret (creds + optional TLSmaterial), PVC, plus optional Ingress / HPA / NetworkPolicy /
ServiceMonitor each gated on its own values flag.
helm testpost-install Pod that curls/api/health(brancheson
server.tls.enabledto use http vs https)..github/workflows/aiworks-chart-ci.yml— runshelm lint+helm template+kubeconformacross three valuesscenarios (default, files-mode, ingress + mTLS).
publish.ymlextended so version bumps on eitherdeepgram-self-hostedOR
deepgram-aiworkstrigger a release; handles the "new chart onfirst merge" edge case (no prior
Chart.yamlinHEAD^).(files+open mode, ingress, in-process TLS, postgres), upgrade /
uninstall recipes, and a values reference table.
Quick-start example
Connect clients via:
Architectural notes for reviewers
license.serverUrldefaults to
http://license-proxy.deepgram.svc.cluster.local:8080with
https://license.deepgram.comas fallback. Operators reuse thelicense-proxy from an existing
deepgram-self-hostedinstall orpoint at a standalone deployment.
${VW_API_KEY}flows through pod env. Helm can't inline Secretvalues at template time, so the ConfigMap renders
[self_hosted].api_key = "${VW_API_KEY}"as a literal placeholder.Voiceworks's config loader resolves it from
std::env::varatstartup — the corresponding backend change shipped in voiceworks
commit
9302010. The chart README documents this contract in thevalues reference.
/api/healthis used for all three probes (readiness / liveness /startup). It's unconditionally registered in both
filesandpostgresstorage modes, so probe config doesn't need to branch onstorage.mode.readOnlyRootFilesystem,all caps dropped. Matches the
voiceworks-self-hostedimage's user.updateStrategy.rollingUpdate.maxUnavailable: 0by default —keeps at least one pod serving during rolling upgrades. Long-lived
WebSocket sessions drain over
terminationGracePeriodSeconds.What's intentionally out of scope
deepgram-self-hostedinstall or standalone).public-internet TLS termination.
Companion voiceworks work
This PR is the chart half of a four-plan effort to make AIWorks
production-ready for self-hosted. The matching backend changes ship
on the
feat/self-hostedbranch of github.com/deepgram/voiceworks —that branch will land separately.
Design spec lives at
voiceworks/docs/superpowers/specs/2026-05-18-aiworks-self-hosted-production-readiness-design.md.Test plan
helm lintclean against all three CI values files(default-values.yaml, files-mode-values.yaml, ingress-mtls-values.yaml)
helm templateproduces well-formed manifests for each scenariokubeconformstep uses-ignore-missing-schemasto allow CRDs (ServiceMonitor)maintriggers chart release viapublish.yml(verify after merge — the "new chart on first merge" branch is
exercised here for the first time)
helm installagainst a real cluster pointing at an existingdeepgram-self-hostedlicense-proxyhelm test aiworksreturns success in both TLS-on and TLS-offconfigurations
Required follow-ups (out of this PR)
feat/self-hostedbranch must land before the chart isusable end-to-end (provides the backend image and the
${VW_API_KEY}env-var resolution this chart relies on).developers.deepgram.comshould get an "AIWorks (Self-Hosted)"section that links into the chart README and the in-product docs
shipping in voiceworks.