From 3daf615dd2e3c2a84930d2186fa0423bb8a8baaf Mon Sep 17 00:00:00 2001 From: Will Anderson Date: Sun, 3 May 2026 10:45:18 -0500 Subject: [PATCH] add neuron-web phase-1 deploy on Legion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stand up the elb-built native El landing server on Legion k3s alongside the existing Cloud Run prod marketing site. Image is built on gitea-runner-1 from elb-emitted .c files + native cc and pushed to registry.neuralplatform.ai/neuron-web:dev-dfe41234. Deployment exposes web-stage.neuralplatform.ai via Traefik + Cloudflare tunnel. The point is to have a working local instance we can iterate against while we redesign the CI/CD pipeline; nothing here replaces the Cloud Run deploy yet. * k8s/neuron-technologies/web/ — namespace, deployment, service, ingress, registry-pull-secret + externalsecret pulling anthropic/supabase keys from Vault * apps/neuron-web.yaml — Argo CD Application * dns-neuralplatform.tf, main.tf — CNAME + tunnel ingress for web-stage.neuralplatform.ai --- servers/legion/apps/neuron-web.yaml | 26 ++++++ servers/legion/dns-neuralplatform.tf | 12 +++ .../neuron-technologies/web/deployment.yaml | 91 +++++++++++++++++++ .../web/externalsecret.yaml | 79 ++++++++++++++++ .../k8s/neuron-technologies/web/ingress.yaml | 25 +++++ .../web/kustomization.yaml | 9 ++ .../neuron-technologies/web/namespace.yaml | 7 ++ .../web/registry-pull-secret.yaml | 30 ++++++ .../k8s/neuron-technologies/web/service.yaml | 15 +++ servers/legion/main.tf | 11 +++ 10 files changed, 305 insertions(+) create mode 100644 servers/legion/apps/neuron-web.yaml create mode 100644 servers/legion/k8s/neuron-technologies/web/deployment.yaml create mode 100644 servers/legion/k8s/neuron-technologies/web/externalsecret.yaml create mode 100644 servers/legion/k8s/neuron-technologies/web/ingress.yaml create mode 100644 servers/legion/k8s/neuron-technologies/web/kustomization.yaml create mode 100644 servers/legion/k8s/neuron-technologies/web/namespace.yaml create mode 100644 servers/legion/k8s/neuron-technologies/web/registry-pull-secret.yaml create mode 100644 servers/legion/k8s/neuron-technologies/web/service.yaml diff --git a/servers/legion/apps/neuron-web.yaml b/servers/legion/apps/neuron-web.yaml new file mode 100644 index 0000000..c9e5de6 --- /dev/null +++ b/servers/legion/apps/neuron-web.yaml @@ -0,0 +1,26 @@ +# neuron-web — El landing page server (Phase 1 deploy on Legion) +# +# Sibling to neuron-marketing (Next.js prod) and neuron-stage (Next.js stage). +# This deployment runs the elb-built native El binary at +# web-stage.neuralplatform.ai. Coexists with the Cloud Run prod marketing +# site; nothing here replaces that yet. +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: neuron-web + namespace: argocd +spec: + project: default + source: + repoURL: http://gitea.git.svc.cluster.local:3000/will/infrastructure.git + targetRevision: main + path: servers/legion/k8s/neuron-technologies/web + destination: + server: https://kubernetes.default.svc + namespace: neuron-web + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/servers/legion/dns-neuralplatform.tf b/servers/legion/dns-neuralplatform.tf index 9d2e070..56e2745 100644 --- a/servers/legion/dns-neuralplatform.tf +++ b/servers/legion/dns-neuralplatform.tf @@ -11,3 +11,15 @@ resource "cloudflare_record" "np_vpn" { proxied = true ttl = 1 } + +# web-stage.neuralplatform.ai — Phase 1 Legion deploy of neuron-web (the El +# native landing server). Coexists with the Cloud Run prod marketing site +# at neurontechnologies.ai; this lets us iterate on the native build. +resource "cloudflare_record" "np_web_stage" { + zone_id = local.zone_neuralplatform_ai + name = "web-stage" + type = "CNAME" + content = "${var.cloudflare_tunnel_id}.cfargotunnel.com" + proxied = true + ttl = 1 +} diff --git a/servers/legion/k8s/neuron-technologies/web/deployment.yaml b/servers/legion/k8s/neuron-technologies/web/deployment.yaml new file mode 100644 index 0000000..4a7580d --- /dev/null +++ b/servers/legion/k8s/neuron-technologies/web/deployment.yaml @@ -0,0 +1,91 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: neuron-web + namespace: neuron-web + labels: + app: neuron-web + env: stage +spec: + replicas: 1 + selector: + matchLabels: + app: neuron-web + strategy: + type: RollingUpdate + rollingUpdate: + maxUnavailable: 0 + maxSurge: 1 + template: + metadata: + labels: + app: neuron-web + env: stage + spec: + imagePullSecrets: + - name: registry-pull-secret + containers: + - name: neuron-web + image: registry.neuralplatform.ai/neuron-web:dev-dfe41234 + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + env: + - name: NODE_ENV + value: production + - name: PORT + value: "8080" + - name: NEURON_PORT + value: "7772" + - name: LANDING_ROOT + value: /srv/landing + # Public Cloud Run env that the El runtime reads at startup. + - name: NEURON_LLM_0_FORMAT + value: anthropic + - name: NEURON_LLM_0_MODEL + value: claude-sonnet-4-5 + - name: NEURON_LLM_0_URL + value: https://api.anthropic.com/v1/messages + - name: GCS_SHARE_BUCKET + value: neuron-shares-prod + - name: NEURON_ORIGIN + value: https://web-stage.neuralplatform.ai + envFrom: + - secretRef: + name: neuron-web-secrets + # The El runtime fs_writes index.html, about.html, terms.html, + # enterprise-terms.html into LANDING_ROOT at startup. The image + # ships a baseline /srv/landing layout (assets, llms.txt) but + # the HTML files are runtime-generated. Mount a writable + # emptyDir over /srv/landing so the runtime can write, while + # keeping the image's static layout via subPath copies — but + # since the image already chowns /srv/landing to the landing + # user, no overlay is needed for normal operation. The + # readOnlyRootFilesystem flag is intentionally NOT set so + # /srv/landing stays writable. + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 512Mi + livenessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + failureThreshold: 3 + readinessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] diff --git a/servers/legion/k8s/neuron-technologies/web/externalsecret.yaml b/servers/legion/k8s/neuron-technologies/web/externalsecret.yaml new file mode 100644 index 0000000..029dd61 --- /dev/null +++ b/servers/legion/k8s/neuron-technologies/web/externalsecret.yaml @@ -0,0 +1,79 @@ +# neuron-web-secrets — runtime secrets pulled from Vault by ESO. +# +# Phase 1 deployment of neuron-web on Legion. Only ANTHROPIC_API_KEY and +# SUPABASE_ANON_KEY are critical for the page to render and the chat +# widget to talk to Claude. Stripe + Resend + DocuSeal are pulled from +# the existing marketing-test path so checkout flows can be exercised in +# stage; webhook + DocuSeal tokens are present but the public webhook +# routes still go to the Cloud Run prod URL until we cut over. +# +# Vault paths used: +# secret/neuron-technologies/anthropic api_key +# secret/neuron-technologies/supabase anon_key, service_role_key, project_url +# secret/neuron-technologies/marketing-test stripe_*, etc. +# secret/neuron-technologies/notifications resend_api_key +# +# To add a new secret without changing the ExternalSecret manifest: +# vault kv patch secret/neuron-technologies/ key="value" +# then add a new data: entry below. +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: neuron-web-secrets + namespace: neuron-web +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: neuron-web-secrets + creationPolicy: Owner + data: + # Critical — chat widget + page render + - secretKey: ANTHROPIC_API_KEY + remoteRef: + key: secret/data/neuron-technologies/anthropic + property: api_key + - secretKey: NEURON_LLM_0_KEY + remoteRef: + key: secret/data/neuron-technologies/anthropic + property: api_key + - secretKey: SUPABASE_ANON_KEY + remoteRef: + key: secret/data/neuron-technologies/supabase + property: anon_key + - secretKey: SUPABASE_SERVICE_KEY + remoteRef: + key: secret/data/neuron-technologies/supabase + property: service_role_key + - secretKey: SUPABASE_URL + remoteRef: + key: secret/data/neuron-technologies/supabase + property: project_url + # Stripe — test/stage keys + - secretKey: STRIPE_PUBLISHABLE_KEY + remoteRef: + key: secret/data/neuron-technologies/marketing-test + property: next_public_stripe_publishable_key + - secretKey: STRIPE_SECRET_KEY + remoteRef: + key: secret/data/neuron-technologies/marketing-test + property: stripe_secret_key + - secretKey: STRIPE_WEBHOOK_SECRET + remoteRef: + key: secret/data/neuron-technologies/marketing-test + property: stripe_webhook_secret + - secretKey: STRIPE_PRICE_PROFESSIONAL + remoteRef: + key: secret/data/neuron-technologies/marketing-test + property: stripe_price_professional + - secretKey: STRIPE_PRICE_FOUNDING + remoteRef: + key: secret/data/neuron-technologies/marketing-test + property: stripe_price_founding + # Notifications + - secretKey: RESEND_API_KEY + remoteRef: + key: secret/data/neuron-technologies/notifications + property: resend_api_key diff --git a/servers/legion/k8s/neuron-technologies/web/ingress.yaml b/servers/legion/k8s/neuron-technologies/web/ingress.yaml new file mode 100644 index 0000000..cb7ba75 --- /dev/null +++ b/servers/legion/k8s/neuron-technologies/web/ingress.yaml @@ -0,0 +1,25 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: neuron-web + namespace: neuron-web + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: traefik + tls: + - hosts: + - web-stage.neuralplatform.ai + secretName: neuron-web-tls + rules: + - host: web-stage.neuralplatform.ai + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: neuron-web + port: + number: 8080 diff --git a/servers/legion/k8s/neuron-technologies/web/kustomization.yaml b/servers/legion/k8s/neuron-technologies/web/kustomization.yaml new file mode 100644 index 0000000..06aea8c --- /dev/null +++ b/servers/legion/k8s/neuron-technologies/web/kustomization.yaml @@ -0,0 +1,9 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - registry-pull-secret.yaml + - externalsecret.yaml + - service.yaml + - deployment.yaml + - ingress.yaml diff --git a/servers/legion/k8s/neuron-technologies/web/namespace.yaml b/servers/legion/k8s/neuron-technologies/web/namespace.yaml new file mode 100644 index 0000000..974db31 --- /dev/null +++ b/servers/legion/k8s/neuron-technologies/web/namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: neuron-web + labels: + managed-by: argo-cd + tier: web diff --git a/servers/legion/k8s/neuron-technologies/web/registry-pull-secret.yaml b/servers/legion/k8s/neuron-technologies/web/registry-pull-secret.yaml new file mode 100644 index 0000000..3e76f7c --- /dev/null +++ b/servers/legion/k8s/neuron-technologies/web/registry-pull-secret.yaml @@ -0,0 +1,30 @@ +# registry-pull-secret — credentials for registry.neuralplatform.ai +# pulled from Vault by ESO. Lets the deployment fetch +# registry.neuralplatform.ai/neuron-web:* without baking creds into the manifest. +apiVersion: external-secrets.io/v1beta1 +kind: ExternalSecret +metadata: + name: registry-pull-secret + namespace: neuron-web +spec: + refreshInterval: 1h + secretStoreRef: + name: vault + kind: ClusterSecretStore + target: + name: registry-pull-secret + creationPolicy: Owner + template: + type: kubernetes.io/dockerconfigjson + data: + .dockerconfigjson: | + {"auths":{"registry.neuralplatform.ai":{"username":"{{ .registry_user }}","password":"{{ .registry_docker_token }}","auth":"{{ list .registry_user .registry_docker_token | join ":" | b64enc }}"}}} + data: + - secretKey: registry_user + remoteRef: + key: secret/data/mudcraft + property: registry_user + - secretKey: registry_docker_token + remoteRef: + key: secret/data/mudcraft + property: registry_docker_token diff --git a/servers/legion/k8s/neuron-technologies/web/service.yaml b/servers/legion/k8s/neuron-technologies/web/service.yaml new file mode 100644 index 0000000..c398887 --- /dev/null +++ b/servers/legion/k8s/neuron-technologies/web/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: neuron-web + namespace: neuron-web + labels: + app: neuron-web +spec: + selector: + app: neuron-web + ports: + - name: http + port: 8080 + targetPort: 8080 + type: ClusterIP diff --git a/servers/legion/main.tf b/servers/legion/main.tf index cad20a3..532ffd9 100644 --- a/servers/legion/main.tf +++ b/servers/legion/main.tf @@ -265,6 +265,17 @@ resource "cloudflare_zero_trust_tunnel_cloudflared_config" "legion" { service = "http://headscale.headscale.svc.cluster.local:8080" } + # web-stage.neuralplatform.ai — Phase 1 Legion deploy of neuron-web + # (native El landing server). Routes via Traefik to the neuron-web + # service in the neuron-web namespace. + ingress_rule { + hostname = "web-stage.neuralplatform.ai" + service = "https://traefik.kube-system.svc:443" + origin_request { + no_tls_verify = true + } + } + # Catch-all — must be last ingress_rule { service = "http_status:404"