Skip to content

Feat/aiworks - #167

Open
nickottocode wants to merge 8 commits into
mainfrom
feat/aiworks
Open

Feat/aiworks#167
nickottocode wants to merge 8 commits into
mainfrom
feat/aiworks

Conversation

@nickottocode

Copy link
Copy Markdown
Contributor

Add deepgram-aiworks Helm chart

New 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

  • New chart with full template set: Deployment, Service, ConfigMap
    (renders config.toml from values), Secret (creds + optional TLS
    material), PVC, plus optional Ingress / HPA / NetworkPolicy /
    ServiceMonitor each gated on its own values flag.
  • Helm helm test post-install Pod that curls /api/health (branches
    on server.tls.enabled to use http vs https).
  • CI matrix at .github/workflows/aiworks-chart-ci.yml — runs
    helm lint + helm template + kubeconform across three values
    scenarios (default, files-mode, ingress + mTLS).
  • publish.yml extended so version bumps on either deepgram-self-hosted
    OR deepgram-aiworks trigger a release; handles the "new chart on
    first merge" edge case (no prior Chart.yaml in HEAD^).
  • Chart README with prerequisites, quick start, four example deployments
    (files+open mode, ingress, in-process TLS, postgres), upgrade /
    uninstall recipes, and a values reference table.

Quick-start example

helm repo add deepgram https://deepgram.github.io/self-hosted-resources
helm install aiworks deepgram/deepgram-aiworks \
  --set license.apiKey=<deepgram-license-uuid> \
  --set selfHosted.apiKey=<40+-char-shared-secret> \
  --set selfHosted.bootstrapSubject=$(uuidgen | tr A-Z a-z)

Connect clients via:

ws://<service-or-ingress>/api/repos/<any-uuid>/flows/<flow-slug>/execute
Authorization: Token <selfHosted.apiKey>

Architectural notes for reviewers

  • License-proxy is NOT bundled. The chart's license.serverUrl
    defaults to http://license-proxy.deepgram.svc.cluster.local:8080
    with https://license.deepgram.com as fallback. Operators reuse the
    license-proxy from an existing deepgram-self-hosted install or
    point at a standalone deployment.
  • ${VW_API_KEY} flows through pod env. Helm can't inline Secret
    values 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::var at
    startup — the corresponding backend change shipped in voiceworks
    commit 9302010. The chart README documents this contract in the
    values reference.
  • /api/health is used for all three probes (readiness / liveness /
    startup). It's unconditionally registered in both files and
    postgres storage modes, so probe config doesn't need to branch on
    storage.mode.
  • Security defaults: non-root UID 10001, readOnlyRootFilesystem,
    all caps dropped. Matches the voiceworks-self-hosted image's user.
  • updateStrategy.rollingUpdate.maxUnavailable: 0 by default —
    keeps at least one pod serving during rolling upgrades. Long-lived
    WebSocket sessions drain over terminationGracePeriodSeconds.

What's intentionally out of scope

  • Self-hosted frontend (Next.js editor). Backend-only for v1.
  • Bundled license-proxy. Operators bring their own (existing
    deepgram-self-hosted install or standalone).
  • ACME / Let's Encrypt in-process. Use an ingress controller for
    public-internet TLS termination.
  • SNI-based multi-cert. Single cert per listener.

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-hosted branch 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 lint clean against all three CI values files
    (default-values.yaml, files-mode-values.yaml, ingress-mtls-values.yaml)
  • helm template produces well-formed manifests for each scenario
  • CI workflow YAML parses; kubeconform step uses
    -ignore-missing-schemas to allow CRDs (ServiceMonitor)
  • First merge to main triggers chart release via publish.yml
    (verify after merge — the "new chart on first merge" branch is
    exercised here for the first time)
  • helm install against a real cluster pointing at an existing
    deepgram-self-hosted license-proxy
  • helm test aiworks returns success in both TLS-on and TLS-off
    configurations

Required follow-ups (out of this PR)

  • voiceworks feat/self-hosted branch must land before the chart is
    usable end-to-end (provides the backend image and the
    ${VW_API_KEY} env-var resolution this chart relies on).
  • developers.deepgram.com should get an "AIWorks (Self-Hosted)"
    section that links into the chart README and the in-product docs
    shipping in voiceworks.

nickottocode and others added 8 commits May 19, 2026 16:11
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>
@nickottocode
nickottocode requested review from a team and therealevanhenry as code owners May 20, 2026 16:04
@mattsloan-dg

Copy link
Copy Markdown

I will be taking a look at this!

@jkroll-deepgram jkroll-deepgram left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 and ci.yml fails the PR if a non-doc change to the chart skips the changelog. The new aiworks-chart-ci.yml doesn't enforce this. Either add a CHANGELOG.md + the equivalent CI check, or accept the inconsistency knowingly.
  • No helm-docs. ci.yml regenerates the deepgram-self-hosted README from values.yaml comments and fails on drift. The new chart's README is hand-written and unenforced — server.prefix, server.wsIdleTimeout, storage.postgres.dsnExistingSecret, storage.postgres.dsnExistingSecretKey are already missing from the values-reference table. Recommend adopting helm-docs for parity.
  • No chart-version-bump CI check. Existing CI fails a PR if appVersion bumped without version bumping. New CI has no equivalent — only publish.yml enforces 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-hosted landing first for ${VW_API_KEY} env resolution. Recommend either holding merge until that lands, or tagging this release in a way publish.yml won't pick up automatically.
  • The "first chart release" branch of publish.yml (empty OLD_VERSION ⇒ treated as bump) is exercised here for the first time. A dry-run with act or workflow_dispatch against 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 }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 clientCaBundleExistingSecret and require the CA bundle to live in the same TLS Secret as tls.crt/tls.key under the key ca.crt. Document the expected key names in the README.
  • Or add a second volume/volumeMount that pulls ca.crt from clientCaBundleExistingSecret and projects it next to tls.crt.

updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 for replicaCount: 1) and let operators tune up when they scale.
  • Expose accessModes in values.yaml so RWX-capable clusters can opt in.
  • Add a fail/required check 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-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 }}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 ingressFrom rule like - podSelector: {} (same namespace) and document the implication, or
  • Document loudly in the README that empty ingressFrom is 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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_KEY and VW_API_KEY are standard) inside secret.yaml and deployment.yaml.
  • Keep *ExistingSecretKey as a read-only knob that only applies when secrets.existingSecret is set.

Comment on lines +19 to +22
- port: http
interval: {{ .Values.serviceMonitor.interval }}
scrapeTimeout: {{ .Values.serviceMonitor.scrapeTimeout }}
path: /metrics

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/tls Secret. Must contain keys tls.crt, tls.key, and (when clientCaBundle is 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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants