Post

Productivity Dashboard: Infrastructure and Environments

Productivity Dashboard environments from k3d with a local LLM to AKS with Azure OpenAI: Helm charts, shared infra, Azure Files, and AI quota as the real capacity limit.

Productivity Dashboard: Infrastructure and Environments

The Productivity Dashboard runs the same charts on a laptop and in AKS, with one deliberate difference that shapes everything else: the AI. Locally the generators talk to a model in LM Studio; in production they talk to Azure OpenAI. Most environment differences are plumbing (storage driver, ingress, secrets), but this one changes the output, so it gets special treatment in how environments are tested and compared.

Development environment

k3d (k3s in Docker) with the productivity namespace, a local PVC productivity-data-pvc backed by the host filesystem, traefik ingress on summaries.local.dev, and LM Studio on the host reachable from pods at host.k3d.internal:1234.

1
2
3
4
5
task k3d-create
task productivity-build-all && task productivity-import-all
task productivity-deploy-local
task port-forward-productivity-ui     # http://127.0.0.1:9000
docker exec k3d-k8cron-cluster-server-0 ls /data      # browse summaries directly

Reading generated Markdown straight from the host is the fastest feedback loop for prompt work. More on k3d in k3d.

Test or QA environment

QA follows the repo’s three testing layers (local Python, k3d, AKS). The GitOps repository has development and production environments; the development AKS environment is where components are promoted first after a release, and where Azure OpenAI behaviour can be checked before production.

TODO: Does the development environment use its own Azure OpenAI deployment and synthetic or limited people data, or does it summarize real people too?

Staging environment

A full staging copy of production would summarize the same real people a second time, which doubles AI cost and personal-data processing for little gain. Two cheaper alternatives cover what staging would:

  • Prompt and model canaries: run a new prompt or model in development for a few people who volunteer, writing to a separate path, and compare with production output.
  • Golden-week regression (see Quality Assurance) on synthetic data before any release.

Production environment

ResourceProduction
ClusterAKS, ArgoCD-managed
Namespaceproductivity: CronJobs, UI Deployment, Redis, ConfigMap, Role
ImagesJFrog Artifactory
Ingressnginx, internal hostname, service port 80 → 9000
StorageAzure File Share mounted at /data through the shared PV/PVC
AIAzure OpenAI deployment
IdentityWorkload Identity on the productivity ServiceAccount
SecretsExternal Secrets from Azure Key Vault via a ClusterSecretStore

Infrastructure as code

Kubernetes resources are all Helm: a chart per collector and generator, the UI chart, and productivity-shared-infra for the ClusterSecretStore, ExternalSecrets (GitHub App, GitHub PAT, Jira, storage, JFrog registry, Grafana) and the shared volume. Promoted values live in the GitOps repository that ArgoCD watches; generator schedules live in the UI chart’s values.

TODO: How are the Azure resources (storage account and share, Azure OpenAI deployment, Key Vault, identities) provisioned: Terraform, portal, or a platform team?

Networking and DNS

  • One internal DNS name for the UI on the nginx ingress controller, with TLS.
  • Egress from jobs to GitHub, Jira Cloud and the Azure OpenAI endpoint; nothing inbound.
  • The UI needs the Kubernetes API (for refresh Jobs) and Redis inside the namespace.
  • Locally: traefik and summaries.local.dev, or a port-forward to 127.0.0.1:9000.

Compute, storage, and databases

WorkloadPattern
Collectors (3)Short-lived pods, once a day
Generators (4)Short-lived pods; mostly waiting on the AI API, so light on CPU
Refresh JobsOn demand, one target each
UIOne long-running pod (to become 2 to 3)
RedisIts own Deployment and Service in the chart

Storage is one Azure File Share: many small JSON and Markdown files, written by jobs and read by the UI. There’s no database; Redis is a cache.

Configuration and secret management

Per-component config.yaml (or src/config.py) holds defaults; Helm configYaml overrides them; the UI chart’s config block becomes the ConfigMap read by /api/config. People and teams are environment variables. Secrets come from Key Vault through External Secrets in AKS, and from kubectl create secret or existingSecret values locally. values.yaml and values-local.yaml must stay in step.

Environment parity

Aspectk3dAKSParity
ChartsSameSameFull
StorageHost-backed PVCAzure FilesSame paths
IngresstraefiknginxRarely matters
SecretsLocal secretsExternal SecretsSame names
AI modelLM Studio, local modelAzure OpenAIDifferent output

The last row is the important one. A prompt that works well on a small local model may behave differently on the production model, and the reverse. Local runs prove the plumbing and the rough format; accuracy has to be checked against the production model family, on synthetic data, before a prompt ships.

Capacity and quota planning

The binding limit is the Azure OpenAI deployment’s quota (tokens and requests per minute), followed by GitHub and Jira API rate limits. Each new team adds people × periods × (user, plus a share of team, epic and POD) summaries. Plan with:

  • tokens per summary type (measured from logs, without logging the content);
  • the generator window (04:00 to 05:00 UTC for users) divided by summaries per run;
  • MAX_PARALLEL_USERS set so the peak stays under the quota.

TODO: What quota does the Azure OpenAI deployment have, and how close to it does the 04:00 run get?

Checklist

  • k3d with LM Studio documented as the default dev loop
  • Development environment’s AI deployment and data scope decided
  • Canary path for prompt and model changes instead of a full staging copy
  • Azure resources defined as code
  • Prompts validated on the production model family before release
  • AI quota and generator window tracked as teams are added
  • values.yaml and values-local.yaml in sync

This post is licensed under CC BY 4.0 by the author.