Deployment Options (AKS)

How to change what runs at portal.example.com, from "instant, no redeploy" to "full image release". Each option below is independent — pick the lightest one that does the job. This page exists because several of the options have non-obvious constraints (a private API server, CI-only image builds, and a master key that must not be overwritten).

The environment, in one table

Fact Value Consequence
Cluster <aks-cluster> / rg <aks-resource-group> (Sweden Central)
API server private (…privatelink…azmk8s.io) kubectl/helm from outside the VNet cannot reach it — use az aks command invoke or an in-VNet runner/VPN
Key Vault CSI azureKeyvaultSecretsProvider add-on enabled — its identity clientId comes from az aks show … --query addonProfiles.azureKeyvaultSecretsProvider.identity.clientId secrets can come from Key Vault, keyless
Key Vault <key-vault> holds AzureFoundry-ApiKey
Images Live pods pull from ACR meshweaver.azurecr.io. The chart's default is ghcr.io/systemorph/memex-portal-ai:latest, which the AKS deploy script repoints to ACR Two CI channels build them, plus a manual path — see below
portal-ai base bakes @anthropic-ai/claude-code + @github/copilot CLIs the claude CLI is present in the running pod
AI model picker fed by ModelProvider / LanguageModel mesh nodes see Setting Up Model Providers

Option A — Live config via mesh nodes (no redeploy) ✅ used now

The model picker reads ModelProvider + LanguageModel mesh nodes. Create/patch them through the portal (MCP / Settings) and they take effect immediately — the data lives in Postgres, the running pod serves it, no deploy.

Best for: adding/curating models, fixing an empty picker, per-user keys. Limitation: these are instance nodes in a space/user partition — not the default catalog served to every partition (that's Option B). The encrypted key depends on the master key staying constant (see the caveat).

Option B — Default static catalog via Helm config (needs redeploy)

BuiltInLanguageModelProvider materialises a default catalog at Provider/{provider} + nested model children from config ({Section}:Models / :Endpoint) — imported into the top-level Provider partition on boot and served from the DB. The tier→model map now lives on the model NODES ("tier": "coding" — see Model Tiers); the ModelTier:Heavy/Standard/Light/Utility keys below are the deprecated shim, still read so an existing deployment keeps its mapping. The AKS overlay (deploy/aks/values.aks.yaml) sets:

AzureFoundry__Endpoint = https://<foundry-account>.services.ai.azure.com/models
AzureFoundry__Models__0/1/2 = DeepSeek-V4-Pro, DeepSeek-V3-0324, DeepSeek-V4-Flash
ModelTier__Heavy/Standard/Light/Utility = V4-Pro / V3-0324 / V4-Flash / V4-Flash
ClaudeCode__ConfigDirRoot = /mnt/users

The chart templates these in deploy/helm/templates/memex-portal/config.yaml (+ secrets.yaml); base values.yaml defaults them empty (neutral chart). Takes effect on the next helm upgrade (Option E), not before.

Best for: the org-wide default catalog + agent tier mapping. Anthropic/Claude is intentionally not wired here — Claude is per-user Claude Code (Option D).

Option C — Secrets via Key Vault + CSI (keyless, no committed keys)

The real AzureFoundry key lives in Key Vault, mounted by the CSI add-on and synced into a K8s Secret the portal reads via envFrom.

  1. Store the secret: az keyvault secret set --vault-name <key-vault> --name AzureFoundry-ApiKey --value <key>.
  2. Grant the add-on identity read access (once): az role assignment create --assignee <csi-addon-identity-clientId> --role "Key Vault Secrets User" --scope <key-vault resourceId> (or an access policy with secret get/list).
  3. Declare it under keyVaultSecrets: in the environment's values (vaultName, tenantId, identityClientId, and secrets: [{vaultSecret: AzureFoundry-ApiKey, key: AzureFoundry__ApiKey}]) — the chart renders the SecretProviderClass, the CSI volume, its mount and the envFrom, so steps 3–4 of the legacy path below are no longer hand-applied. See DeploymentAKS → "Key Vault secrets are DECLARED in values". (Legacy, hand-made shape — template at deploy/aks/envs/example/secretproviderclass.yaml; the real one is per-environment and git-ignored.)
  4. (Legacy only) Patch the portal Deployment to mount it + read the synced secret:
# strategic-merge patch on the portal container/pod
spec:
  template:
    spec:
      containers:
        - name: memex-portal
          envFrom:
            - secretRef: { name: memex-portal-ai-secrets }   # synced by the SPC
          volumeMounts:
            - name: kv-ai-secrets
              mountPath: /mnt/secrets-store
              readOnly: true
      volumes:
        - name: kv-ai-secrets
          csi:
            driver: secrets-store.csi.k8s.io
            readOnly: true
            volumeAttributes:
              secretProviderClass: memex-portal-ai-secrets

Best for: keeping the key out of git/values. Pairs with Option B (B says which models; C supplies the key).

Option D — Claude Code (per-user, Claude on your own subscription)

Claude is not a shared org key. Each user connects the co-hosted Claude Code CLI under their own account in Settings → Models → Connect: the portal runs claude setup-token under {ClaudeCode:ConfigDirRoot}/{userId}/.claude, captures the token, and stores an encrypted {user}/_Memex/ClaudeCode provider that injects Claude into that user's picker.

Option E — Code changes → new image → deploy

Anything in .cs (e.g. the Claude Code PTY fix, the static-catalog behaviour) only ships in a new image:

  1. Build + push. Three paths, and the normal one is the first:
    • Continuous (default): merge to mainmain-cd.yml builds and pushes 3.0.0-ci.<n> to ACR (meshweaver.azurecr.io). This is what the portals self-update onto. It also runs on an hourly reconcile schedule and workflow_dispatch.
    • Official release: push an annotated v*.*.* tag on a promoted, sealed commit → release.yml retags that set with the clean version in ACR and mirrors it to GHCR. Nothing is rebuilt (Release Process).
    • Manual (break-glass): dotnet publish -t:PublishContainer straight to ACR — see DeploymentAKS.md §1. 🚨 Only usable with the self-updater paused, because a non-SemVer tag is not a self-update candidate and the poller will roll the Deployment back off it.
  2. Roll the cluster to it — as a Roll (image) or HelmRelease deploy / Reconcile (config) Hosting/InstanceAction on the control instance (OperatingFromThePortal). The commands below are what the operator runs; typed by hand they are break-glass, server-side via az aks command invoke because the API server is private:
# config/manifest changes (works against the CURRENT image). The SecretProviderClass is
# per-environment and NOT in this repo — take it from the environment's own folder (template:
# deploy/aks/envs/example/secretproviderclass.yaml):
az aks command invoke -g <aks-resource-group> -n <aks-cluster> \
  --command "kubectl apply -f secretproviderclass.yaml && kubectl rollout restart deploy/memex-portal-deployment -n <ns>" \
  --file <env-dir>/secretproviderclass.yaml

# helm upgrade (uploads the chart + values to the in-cluster run pod, which has helm):
az aks command invoke -g <aks-resource-group> -n <aks-cluster> \
  --command "helm upgrade <release> ./helm -f ./helm/values.yaml -f values.aks.yaml -n <ns>" \
  --file deploy/helm --file deploy/aks/values.aks.yaml

az aks command invoke runs the command from a pod inside the cluster (kubectl + helm preinstalled) and attaches the --file paths — the standard way to operate a private AKS cluster without VNet line-of-sight.

🚨 The master-key caveat

ModelProvider.ApiKey values are encrypted with Ai:KeyProtection:MasterKey. The running deployment already has a master key set out-of-band (not in the chart, not in the KVs above). Therefore:

Quick chooser

You want to… Option Redeploy?
Add/curate models or fix an empty picker now A (mesh nodes) no
Make a model the org-wide default + agent tier B (Helm config) yes
Keep the key out of git C (Key Vault + CSI) yes
Let users use Claude on their own subscription D (Claude Code) code → E
Ship a .cs change E (merge to main → CI image → self-update, or command invoke) yes
Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.