diff --git a/servers/legion/adguard.tf b/servers/legion/adguard.tf
index 6bf9c09..a19a83c 100644
--- a/servers/legion/adguard.tf
+++ b/servers/legion/adguard.tf
@@ -1,4 +1,6 @@
-# AdGuard Home — household DNS with ad blocking, local resolution, DoH via dns.nook.family, and DoT via dot.nook.family:853
+# AdGuard Home — namespace only
+# PVCs, ConfigMap, Certificate, Ingress, ddclient managed by Argo CD + ESO
+# See: apps/adguard.yaml, apps/adguard-config.yaml, k8s/adguard/
resource "kubernetes_namespace" "dns" {
metadata {
@@ -9,169 +11,3 @@ resource "kubernetes_namespace" "dns" {
}
}
}
-
-resource "kubernetes_persistent_volume_claim" "adguard_config" {
- wait_until_bound = false
- metadata {
- name = "adguard-config"
- namespace = kubernetes_namespace.dns.metadata[0].name
- }
- spec {
- access_modes = ["ReadWriteOnce"]
- resources {
- requests = {
- storage = "1Gi"
- }
- }
- }
-}
-
-resource "kubernetes_persistent_volume_claim" "adguard_data" {
- wait_until_bound = false
- metadata {
- name = "adguard-data"
- namespace = kubernetes_namespace.dns.metadata[0].name
- }
- spec {
- access_modes = ["ReadWriteOnce"]
- resources {
- requests = {
- storage = "5Gi"
- }
- }
- }
-}
-
-# Desired AdGuard settings — applied by init container on every pod start.
-# To change DNS upstreams, filter lists, or rewrites: edit here, terraform apply, then push apps/adguard.yaml.
-resource "kubernetes_config_map" "adguard_defaults" {
- metadata {
- name = "adguard-defaults"
- namespace = kubernetes_namespace.dns.metadata[0].name
- }
-
- data = {
- "AdGuardHome.yaml" = <<-EOT
- dns:
- bind_hosts:
- - 0.0.0.0
- port: 53
- upstream_dns:
- - https://dns.cloudflare.com/dns-query
- - https://dns.google/dns-query
- bootstrap_dns:
- - 1.1.1.1
- - 8.8.8.8
- - 9.9.9.10
- - 149.112.112.10
- upstream_mode: load_balance
- cache_enabled: true
- cache_size: 4194304
- filters:
- - enabled: true
- url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
- name: AdGuard DNS filter
- id: 1
- - enabled: true
- url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt
- name: AdAway Default Blocklist
- id: 2
- - enabled: true
- url: https://big.oisd.nl/domainswild
- name: OISD Big
- id: 3
- - enabled: true
- url: https://easylist.to/easylist/easylist.txt
- name: EasyList
- id: 4
- - enabled: true
- url: https://easylist.to/easylist/easyprivacy.txt
- name: EasyPrivacy
- id: 5
- tls:
- enabled: true
- server_name: dot.nook.family
- port_dns_over_tls: 853
- certificate_path: /etc/adguard/tls/tls.crt
- private_key_path: /etc/adguard/tls/tls.key
- allow_unencrypted_doh: true
- filtering:
- filtering_enabled: true
- rewrites:
- - domain: '*.nook.family'
- answer: ${var.legion_ip}
- enabled: true
- - domain: '*.neuralplatform.ai'
- answer: ${var.legion_ip}
- enabled: true
- EOT
- }
-}
-
-# TLS certificate for DoT — issued via cert-manager DNS-01 (Cloudflare)
-resource "kubernetes_manifest" "adguard_dot_cert" {
- manifest = {
- apiVersion = "cert-manager.io/v1"
- kind = "Certificate"
- metadata = {
- name = "adguard-dot-tls"
- namespace = kubernetes_namespace.dns.metadata[0].name
- }
- spec = {
- secretName = "adguard-dot-tls"
- issuerRef = {
- name = "letsencrypt-prod"
- kind = "ClusterIssuer"
- }
- dnsNames = ["dot.nook.family"]
- }
- }
-
- depends_on = [kubernetes_manifest.cluster_issuer_prod]
-}
-
-# Deployment + Service managed by Argo CD — see servers/legion/apps/adguard.yaml
-
-resource "kubernetes_ingress_v1" "adguard" {
- metadata {
- name = "adguard"
- namespace = kubernetes_namespace.dns.metadata[0].name
- annotations = {
- "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
- "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
- }
- }
-
- spec {
- tls {
- hosts = ["dns.nook.family"]
- secret_name = "adguard-tls"
- }
-
- rule {
- host = "dns.nook.family"
- http {
- path {
- path = "/"
- path_type = "Prefix"
- backend {
- service {
- name = "adguard-ui"
- port {
- number = 3000
- }
- }
- }
- }
- }
- }
- }
-
- depends_on = [helm_release.cert_manager]
-}
-
-# Host-level bootstrap notes (run bootstrap.sh once on new Legion installs):
-# - systemd-resolved disabled (conflicts with AdGuard on port 53)
-# - k3s config.yaml includes tls-san for LAN IP
-# - /etc/resolv.conf set to 1.1.1.1 fallback
-# See: servers/legion/bootstrap.sh
diff --git a/servers/legion/apps/adguard-config.yaml b/servers/legion/apps/adguard-config.yaml
new file mode 100644
index 0000000..60ae69a
--- /dev/null
+++ b/servers/legion/apps/adguard-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: adguard-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/adguard
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: dns
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/alloy.yaml b/servers/legion/apps/alloy.yaml
new file mode 100644
index 0000000..77dbfc0
--- /dev/null
+++ b/servers/legion/apps/alloy.yaml
@@ -0,0 +1,82 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: alloy
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: https://grafana.github.io/helm-charts
+ chart: alloy
+ targetRevision: "*"
+ helm:
+ values: |
+ controller:
+ type: daemonset
+ alloy:
+ configMap:
+ content: |
+ discovery.kubernetes "pods" {
+ role = "pod"
+ }
+
+ discovery.relabel "pods" {
+ targets = discovery.kubernetes.pods.targets
+ rule {
+ source_labels = ["__meta_kubernetes_namespace"]
+ target_label = "namespace"
+ }
+ rule {
+ source_labels = ["__meta_kubernetes_pod_name"]
+ target_label = "pod"
+ }
+ rule {
+ source_labels = ["__meta_kubernetes_pod_container_name"]
+ target_label = "container"
+ }
+ rule {
+ source_labels = ["__meta_kubernetes_pod_label_app"]
+ target_label = "app"
+ }
+ }
+
+ loki.source.kubernetes "pods" {
+ targets = discovery.relabel.pods.output
+ forward_to = [loki.write.default.receiver]
+ }
+
+ loki.write "default" {
+ endpoint {
+ url = "http://loki-gateway.monitoring.svc.cluster.local/loki/api/v1/push"
+ tenant_id = "legion"
+ }
+ }
+
+ otelcol.receiver.otlp "default" {
+ grpc { endpoint = "0.0.0.0:4317" }
+ http { endpoint = "0.0.0.0:4318" }
+ output {
+ traces = [otelcol.exporter.otlp.tempo.input]
+ logs = [otelcol.exporter.loki.pantheon.input]
+ }
+ }
+
+ otelcol.exporter.otlp "tempo" {
+ client {
+ endpoint = "tempo.monitoring.svc.cluster.local:4317"
+ tls { insecure = true }
+ }
+ }
+
+ otelcol.exporter.loki "pantheon" {
+ forward_to = [loki.write.default.receiver]
+ }
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: monitoring
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/backup.yaml b/servers/legion/apps/backup.yaml
new file mode 100644
index 0000000..9ad29e6
--- /dev/null
+++ b/servers/legion/apps/backup.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: backup
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/backup
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: git
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/ci-config.yaml b/servers/legion/apps/ci-config.yaml
new file mode 100644
index 0000000..1877d37
--- /dev/null
+++ b/servers/legion/apps/ci-config.yaml
@@ -0,0 +1,41 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: ci-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/github-runner
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: ci
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
+---
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: gitea-runner-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/gitea-runner
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: ci
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/external-secrets-config.yaml b/servers/legion/apps/external-secrets-config.yaml
new file mode 100644
index 0000000..6a2ff49
--- /dev/null
+++ b/servers/legion/apps/external-secrets-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: external-secrets-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/external-secrets
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: external-secrets
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/external-secrets.yaml b/servers/legion/apps/external-secrets.yaml
new file mode 100644
index 0000000..ff21cab
--- /dev/null
+++ b/servers/legion/apps/external-secrets.yaml
@@ -0,0 +1,23 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: external-secrets
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: https://charts.external-secrets.io
+ chart: external-secrets
+ targetRevision: 0.10.7
+ helm:
+ values: |
+ installCRDs: true
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: external-secrets
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/gitea-config.yaml b/servers/legion/apps/gitea-config.yaml
new file mode 100644
index 0000000..b042f7a
--- /dev/null
+++ b/servers/legion/apps/gitea-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: gitea-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/gitea
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: git
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/headscale-config.yaml b/servers/legion/apps/headscale-config.yaml
new file mode 100644
index 0000000..c89d971
--- /dev/null
+++ b/servers/legion/apps/headscale-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: headscale-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/headscale
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: headscale
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/kube-prometheus-stack.yaml b/servers/legion/apps/kube-prometheus-stack.yaml
new file mode 100644
index 0000000..7458ef5
--- /dev/null
+++ b/servers/legion/apps/kube-prometheus-stack.yaml
@@ -0,0 +1,108 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: kube-prometheus-stack
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: https://prometheus-community.github.io/helm-charts
+ chart: kube-prometheus-stack
+ targetRevision: "*"
+ helm:
+ values: |
+ grafana:
+ adminPassword:
+ valueFrom:
+ secretKeyRef:
+ name: grafana-admin-secret
+ key: admin-password
+ persistence:
+ enabled: true
+ size: 5Gi
+ sidecar:
+ datasources:
+ defaultDatasourceEnabled: false
+ initChownData:
+ enabled: false
+ ingress:
+ enabled: true
+ ingressClassName: traefik
+ hosts:
+ - grafana.neuralplatform.ai
+ tls:
+ - secretName: grafana-tls
+ hosts:
+ - grafana.neuralplatform.ai
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+ prometheus:
+ prometheusSpec:
+ storageSpec:
+ volumeClaimTemplate:
+ spec:
+ resources:
+ requests:
+ storage: 20Gi
+ kubeEtcd:
+ enabled: false
+ kubeControllerManager:
+ enabled: false
+ kubeScheduler:
+ enabled: false
+ kubeProxy:
+ enabled: false
+ alertmanager:
+ config:
+ global:
+ resolve_timeout: 5m
+ inhibit_rules:
+ - source_matchers: [severity="critical"]
+ target_matchers: [severity=~"warning|info"]
+ equal: [namespace, alertname]
+ - source_matchers: [severity="warning"]
+ target_matchers: [severity="info"]
+ equal: [namespace, alertname]
+ receivers:
+ - name: "null"
+ - name: slack-alerts
+ slack_configs:
+ - channel: "#infrastructure-alerts"
+ api_url: "https://slack.com/api/chat.postMessage"
+ http_config:
+ authorization:
+ type: Bearer
+ credentials_file: /etc/alertmanager/secrets/slack-bot-token/bot_token
+ send_resolved: true
+ title: '[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} ({{ .CommonLabels.namespace }})'
+ text: |
+ {{ range .Alerts }}
+ *{{ if .Annotations.summary }}{{ .Annotations.summary }}{{ else }}{{ .Labels.alertname }}{{ end }}*{{ if .Labels.severity }} · {{ .Labels.severity }}{{ end }}
+ {{ if .Annotations.description }}{{ .Annotations.description }}{{ end }}
+ {{ end }}
+ footer: "Legion · neuralplatform.ai"
+ route:
+ group_by: [namespace, alertname]
+ group_wait: 30s
+ group_interval: 5m
+ repeat_interval: 4h
+ receiver: slack-alerts
+ routes:
+ - matchers: [alertname="Watchdog"]
+ receiver: "null"
+ - matchers: [severity="info"]
+ receiver: "null"
+ alertmanagerSpec:
+ secrets:
+ - slack-bot-token
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: monitoring
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
+ - ServerSideApply=true
diff --git a/servers/legion/apps/loki.yaml b/servers/legion/apps/loki.yaml
new file mode 100644
index 0000000..9105f7b
--- /dev/null
+++ b/servers/legion/apps/loki.yaml
@@ -0,0 +1,44 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: loki
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: https://grafana.github.io/helm-charts
+ chart: loki
+ targetRevision: "*"
+ helm:
+ values: |
+ loki:
+ commonConfig:
+ replication_factor: 1
+ storage:
+ type: filesystem
+ useTestSchema: true
+ singleBinary:
+ replicas: 1
+ persistence:
+ enabled: true
+ size: 10Gi
+ deploymentMode: SingleBinary
+ backend:
+ replicas: 0
+ read:
+ replicas: 0
+ write:
+ replicas: 0
+ chunksCache:
+ enabled: false
+ resultsCache:
+ enabled: false
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: monitoring
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/monitoring-config.yaml b/servers/legion/apps/monitoring-config.yaml
new file mode 100644
index 0000000..f5853c7
--- /dev/null
+++ b/servers/legion/apps/monitoring-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: monitoring-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/monitoring
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: monitoring
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/neuron-config.yaml b/servers/legion/apps/neuron-config.yaml
new file mode 100644
index 0000000..ef04a57
--- /dev/null
+++ b/servers/legion/apps/neuron-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: neuron-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/neuron
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: neuron
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/ollama-config.yaml b/servers/legion/apps/ollama-config.yaml
new file mode 100644
index 0000000..76565b8
--- /dev/null
+++ b/servers/legion/apps/ollama-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: ollama-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/ollama
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: ollama
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/packages-config.yaml b/servers/legion/apps/packages-config.yaml
new file mode 100644
index 0000000..f753266
--- /dev/null
+++ b/servers/legion/apps/packages-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: packages-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/packages
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: packages
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/postgres-config.yaml b/servers/legion/apps/postgres-config.yaml
new file mode 100644
index 0000000..0a60ba0
--- /dev/null
+++ b/servers/legion/apps/postgres-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: postgres-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/postgres
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: platform
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/postgres.yaml b/servers/legion/apps/postgres.yaml
new file mode 100644
index 0000000..2480f96
--- /dev/null
+++ b/servers/legion/apps/postgres.yaml
@@ -0,0 +1,41 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: postgres
+ namespace: argocd
+spec:
+ project: default
+ sources:
+ # Helm chart from Docker Hub OCI registry
+ - repoURL: registry-1.docker.io/bitnamicharts
+ chart: postgresql
+ targetRevision: "*"
+ helm:
+ values: |
+ auth:
+ existingSecret: postgres-passwords
+ secretKeys:
+ adminPasswordKey: postgres-password
+ primary:
+ persistence:
+ size: 20Gi
+ resources:
+ requests:
+ memory: 256Mi
+ cpu: 100m
+ initdb:
+ scripts:
+ create-kong-db.sql: |
+ CREATE DATABASE kong;
+ CREATE USER kong WITH PASSWORD '$(KONG_DB_PASSWORD)';
+ GRANT ALL PRIVILEGES ON DATABASE kong TO kong;
+ ALTER DATABASE kong OWNER TO kong;
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: platform
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/redis.yaml b/servers/legion/apps/redis.yaml
new file mode 100644
index 0000000..d56c5e9
--- /dev/null
+++ b/servers/legion/apps/redis.yaml
@@ -0,0 +1,32 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: redis
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: registry-1.docker.io/bitnamicharts
+ chart: redis
+ targetRevision: "*"
+ helm:
+ values: |
+ auth:
+ enabled: false
+ architecture: standalone
+ master:
+ persistence:
+ size: 5Gi
+ resources:
+ requests:
+ memory: 128Mi
+ cpu: 50m
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: platform
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/registry-config.yaml b/servers/legion/apps/registry-config.yaml
new file mode 100644
index 0000000..b5e30f8
--- /dev/null
+++ b/servers/legion/apps/registry-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: registry-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/registry
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: registry
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/tempo.yaml b/servers/legion/apps/tempo.yaml
new file mode 100644
index 0000000..6c86318
--- /dev/null
+++ b/servers/legion/apps/tempo.yaml
@@ -0,0 +1,31 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: tempo
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: https://grafana.github.io/helm-charts
+ chart: tempo
+ targetRevision: "*"
+ helm:
+ values: |
+ tempo:
+ storage:
+ trace:
+ backend: local
+ local:
+ path: /var/tempo
+ persistence:
+ enabled: true
+ size: 10Gi
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: monitoring
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/vault-config.yaml b/servers/legion/apps/vault-config.yaml
new file mode 100644
index 0000000..76476e2
--- /dev/null
+++ b/servers/legion/apps/vault-config.yaml
@@ -0,0 +1,20 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: vault-config
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: http://10.43.1.53:3000/will/infrastructure.git
+ targetRevision: main
+ path: servers/legion/k8s/vault
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: vault
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/apps/vault.yaml b/servers/legion/apps/vault.yaml
new file mode 100644
index 0000000..ba36912
--- /dev/null
+++ b/servers/legion/apps/vault.yaml
@@ -0,0 +1,89 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: vault
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: https://helm.releases.hashicorp.com
+ chart: vault
+ targetRevision: "0.29.1"
+ helm:
+ values: |
+ server:
+ image:
+ repository: hashicorp/vault
+ tag: "1.18.3"
+
+ extraEnvironmentVars:
+ GOOGLE_APPLICATION_CREDENTIALS: /vault/gcp/sa.json
+
+ volumes:
+ - name: gcp-sa
+ secret:
+ secretName: vault-gcp-sa
+
+ volumeMounts:
+ - name: gcp-sa
+ mountPath: /vault/gcp
+ readOnly: true
+
+ ha:
+ enabled: false
+
+ dataStorage:
+ enabled: true
+ size: 10Gi
+
+ standalone:
+ enabled: true
+ config: |
+ ui = true
+
+ listener "tcp" {
+ tls_disable = 1
+ address = "[::]:8200"
+ cluster_address = "[::]:8201"
+ }
+
+ storage "file" {
+ path = "/vault/data"
+ }
+
+ seal "gcpckms" {
+ project = "neuron-785695"
+ region = "global"
+ key_ring = "vault"
+ crypto_key = "vault-unseal"
+ }
+
+ ingress:
+ enabled: true
+ ingressClassName: traefik
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+ hosts:
+ - host: vault.neuralplatform.ai
+ paths:
+ - /
+ tls:
+ - secretName: vault-tls
+ hosts:
+ - vault.neuralplatform.ai
+
+ ui:
+ enabled: true
+
+ injector:
+ enabled: false
+ destination:
+ server: https://kubernetes.default.svc
+ namespace: vault
+ syncPolicy:
+ automated:
+ prune: true
+ selfHeal: true
+ syncOptions:
+ - CreateNamespace=true
diff --git a/servers/legion/backup.tf b/servers/legion/backup.tf
index 4250d1c..07f8b89 100644
--- a/servers/legion/backup.tf
+++ b/servers/legion/backup.tf
@@ -1,137 +1,2 @@
-# Legion backup — restic to Cloudflare R2
-# Dumps all Postgres databases + Gitea data nightly at 2am
-# Retains 30 daily / 12 weekly / 6 monthly snapshots
-
-resource "kubernetes_secret" "backup_credentials" {
- metadata {
- name = "backup-credentials"
- namespace = "git"
- }
-
- data = {
- RESTIC_PASSWORD = "gitea-restic-2026"
- AWS_ACCESS_KEY_ID = var.r2_access_key_id
- AWS_SECRET_ACCESS_KEY = var.r2_secret_access_key
- RESTIC_REPOSITORY = "s3:${var.r2_endpoint}/legion-gitea-backup"
- PGPASSWORD = var.postgres_password
- }
-}
-
-resource "kubernetes_cron_job_v1" "gitea_backup" {
- metadata {
- name = "gitea-backup"
- namespace = "git"
- }
-
- spec {
- schedule = "0 2 * * *"
- concurrency_policy = "Forbid"
- failed_jobs_history_limit = 3
- successful_jobs_history_limit = 3
-
- job_template {
- metadata {}
- spec {
- backoff_limit = 2
- template {
- metadata {}
- spec {
- restart_policy = "OnFailure"
-
- # Dump all Postgres databases before restic runs
- init_container {
- name = "pg-dump"
- image = "postgres:18-alpine"
-
- command = ["/bin/sh", "-c",
- "pg_dumpall -h postgres-postgresql.platform.svc.cluster.local -U postgres --no-role-passwords > /dump/all-databases.sql && echo 'DB dump complete, size:' $(du -sh /dump/all-databases.sql)"
- ]
-
- env_from {
- secret_ref {
- name = kubernetes_secret.backup_credentials.metadata[0].name
- }
- }
-
- volume_mount {
- name = "dump"
- mount_path = "/dump"
- }
-
- resources {
- requests = { memory = "128Mi", cpu = "100m" }
- limits = { memory = "256Mi" }
- }
- }
-
- container {
- name = "backup"
- image = "restic/restic:latest"
-
- command = ["/bin/sh", "-c", <<-EOT
- set -e
-
- # Init repo if first run
- restic snapshots 2>/dev/null || restic init
-
- # Back up Gitea data + full DB dump
- echo "Running restic backup..."
- restic backup \
- /gitea-data \
- /dump/all-databases.sql \
- --tag legion \
- --host legion
-
- # Prune old snapshots
- restic forget \
- --keep-daily 30 \
- --keep-weekly 12 \
- --keep-monthly 6 \
- --prune
-
- echo "Backup complete."
- restic snapshots --latest 3
- EOT
- ]
-
- env_from {
- secret_ref {
- name = kubernetes_secret.backup_credentials.metadata[0].name
- }
- }
-
- volume_mount {
- name = "gitea-data"
- mount_path = "/gitea-data"
- read_only = true
- }
-
- volume_mount {
- name = "dump"
- mount_path = "/dump"
- }
-
- resources {
- requests = { memory = "256Mi", cpu = "100m" }
- limits = { memory = "512Mi" }
- }
- }
-
- volume {
- name = "gitea-data"
- persistent_volume_claim {
- claim_name = "gitea-data"
- read_only = true
- }
- }
-
- volume {
- name = "dump"
- empty_dir {}
- }
- }
- }
- }
- }
- }
-}
+# Legion backup — CronJob and secret managed by Argo CD + ESO
+# See: apps/backup.yaml, k8s/backup/
diff --git a/servers/legion/ddclient.tf b/servers/legion/ddclient.tf
index 7814b06..cfabf35 100644
--- a/servers/legion/ddclient.tf
+++ b/servers/legion/ddclient.tf
@@ -1,73 +1,3 @@
-# ddclient — dynamic DNS daemon that keeps dot.nook.family pointing at the home public IP
-# Updates Cloudflare A record every 5 minutes; required for DoT (port 853) which can't use Cloudflare proxy
-
-resource "kubernetes_secret" "ddclient_config" {
- metadata {
- name = "ddclient-config"
- namespace = kubernetes_namespace.dns.metadata[0].name
- }
-
- data = {
- "ddclient.conf" = <<-EOT
- daemon=300
- syslog=yes
- pid=/var/run/ddclient/ddclient.pid
- use=web, web=https://api.ipify.org
-
- protocol=cloudflare
- zone=nook.family
- ttl=120
- login=${var.cloudflare_email}
- password=${var.cloudflare_api_key}
- dot.nook.family
- EOT
- }
-}
-
-resource "kubernetes_deployment" "ddclient" {
- metadata {
- name = "ddclient"
- namespace = kubernetes_namespace.dns.metadata[0].name
- labels = { app = "ddclient" }
- }
-
- spec {
- replicas = 1
- selector {
- match_labels = { app = "ddclient" }
- }
- template {
- metadata {
- labels = { app = "ddclient" }
- }
- spec {
- container {
- name = "ddclient"
- image = "ghcr.io/ddclient/ddclient:latest"
- command = ["ddclient", "-file", "/etc/ddclient/ddclient.conf", "-daemon", "300", "-noquiet", "-foreground"]
-
- volume_mount {
- name = "config"
- mount_path = "/etc/ddclient/ddclient.conf"
- sub_path = "ddclient.conf"
- read_only = true
- }
-
- resources {
- requests = { memory = "32Mi", cpu = "10m" }
- limits = { memory = "64Mi" }
- }
- }
-
- volume {
- name = "config"
- secret {
- secret_name = kubernetes_secret.ddclient_config.metadata[0].name
- }
- }
- }
- }
- }
-
- depends_on = [kubernetes_namespace.dns]
-}
+# ddclient — dynamic DNS daemon
+# Deployment and config secret managed by Argo CD + ESO
+# See: apps/adguard-config.yaml, k8s/adguard/ddclient-*.yaml
diff --git a/servers/legion/gitea-runner.tf b/servers/legion/gitea-runner.tf
index a6f60a7..9473a76 100644
--- a/servers/legion/gitea-runner.tf
+++ b/servers/legion/gitea-runner.tf
@@ -1,14 +1,2 @@
-# Gitea act-runner — executes Gitea Actions CI jobs on Legion
-# Registered at the instance level so it can serve all repos/orgs
-
-resource "kubernetes_secret" "gitea_runner_secret" {
- metadata {
- name = "gitea-runner-secret"
- namespace = kubernetes_namespace.ci.metadata[0].name
- }
-
- data = {
- GITEA_INSTANCE_URL = "https://git.neuralplatform.ai"
- GITEA_RUNNER_REGISTRATION_TOKEN = var.gitea_runner_token
- }
-}
+# Gitea act-runner — secrets managed by Argo CD + ESO
+# See: apps/gitea-runner.yaml, apps/ci-config.yaml, k8s/gitea-runner/
diff --git a/servers/legion/gitea.tf b/servers/legion/gitea.tf
index 5dcf535..797ca8c 100644
--- a/servers/legion/gitea.tf
+++ b/servers/legion/gitea.tf
@@ -1,117 +1,6 @@
# Gitea — local git server for private/sensitive infrastructure repos
-# Accessible at git.neuralplatform.ai (internal only, real TLS)
-
-resource "kubernetes_config_map" "gitea_custom_css" {
- metadata {
- name = "gitea-custom-css"
- namespace = kubernetes_namespace.git.metadata[0].name
- }
-
- data = {
- "header.tmpl" = <<-TMPL
-
- TMPL
-
-
- "custom.css" = <<-CSS
- /* ── Typography & base ── */
- :root {
- --color-primary: #6366f1;
- --color-primary-dark: #4f46e5;
- --color-secondary: #8b5cf6;
- --color-bg: #0f0f17;
- --color-surface: #16161f;
- --color-surface-2: #1e1e2e;
- --color-border: #2a2a3d;
- --color-text: #e2e2f0;
- --color-text-muted: #8888aa;
- --color-green: #22d3a5;
- --radius: 10px;
- }
-
- body {
- background: var(--color-bg) !important;
- color: var(--color-text) !important;
- font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
- }
-
- /* ── Top navbar ── */
- .navbar, #navbar {
- background: var(--color-surface) !important;
- border-bottom: 1px solid var(--color-border) !important;
- box-shadow: 0 1px 12px rgba(0,0,0,0.4) !important;
- }
-
- .navbar .brand svg, .navbar .brand img { filter: brightness(1.2); }
-
- /* ── Sidebar & panels ── */
- .repository, .ui.container, .ui.segment,
- .ui.card, .ui.cards > .card {
- background: var(--color-surface) !important;
- border: 1px solid var(--color-border) !important;
- border-radius: var(--radius) !important;
- }
-
- /* ── Buttons ── */
- .ui.primary.button, .ui.green.button {
- background: var(--color-primary) !important;
- border: none !important;
- border-radius: 8px !important;
- }
- .ui.primary.button:hover { background: var(--color-primary-dark) !important; }
-
- /* ── Inputs ── */
- input, textarea, select,
- .ui.input > input, .ui.dropdown {
- background: var(--color-surface-2) !important;
- border: 1px solid var(--color-border) !important;
- color: var(--color-text) !important;
- border-radius: 8px !important;
- }
-
- /* ── Sign-in page ── */
- .user.signin .ui.segment,
- .user.signup .ui.segment {
- background: var(--color-surface) !important;
- border: 1px solid var(--color-border) !important;
- border-radius: 16px !important;
- box-shadow: 0 8px 32px rgba(0,0,0,0.5) !important;
- padding: 2.5rem !important;
- }
-
- /* ── Repo file tree ── */
- .repository.file.list .file-list {
- background: var(--color-surface) !important;
- border-radius: var(--radius) !important;
- border: 1px solid var(--color-border) !important;
- }
-
- .repository.file.list .file-list tr:hover td {
- background: var(--color-surface-2) !important;
- }
-
- /* ── Labels & badges ── */
- .ui.label { border-radius: 6px !important; }
-
- /* ── Dashboard activity feed ── */
- .feeds .news { border-bottom: 1px solid var(--color-border) !important; }
-
- /* ── Code blocks ── */
- pre, code {
- background: var(--color-surface-2) !important;
- border: 1px solid var(--color-border) !important;
- border-radius: 6px !important;
- }
-
- /* ── Muted footer ── */
- #footer {
- background: var(--color-surface) !important;
- border-top: 1px solid var(--color-border) !important;
- color: var(--color-text-muted) !important;
- }
- CSS
- }
-}
+# Namespace only — all other resources managed by Argo CD + ESO
+# See: apps/gitea.yaml, apps/gitea-config.yaml, k8s/gitea/
resource "kubernetes_namespace" "git" {
metadata {
@@ -122,66 +11,3 @@ resource "kubernetes_namespace" "git" {
}
}
}
-
-resource "kubernetes_persistent_volume_claim" "gitea_data" {
- wait_until_bound = false
- metadata {
- name = "gitea-data"
- namespace = kubernetes_namespace.git.metadata[0].name
- }
- spec {
- access_modes = ["ReadWriteOnce"]
- resources {
- requests = {
- storage = "20Gi"
- }
- }
- }
-}
-
-resource "kubernetes_secret" "gitea_db_secret" {
- metadata {
- name = "gitea-db"
- namespace = kubernetes_namespace.git.metadata[0].name
- }
- data = {
- password = var.gitea_db_password
- }
-}
-
-# Deployment + Service managed by Argo CD — see servers/legion/apps/gitea.yaml
-
-resource "kubernetes_ingress_v1" "gitea" {
- metadata {
- name = "gitea"
- namespace = kubernetes_namespace.git.metadata[0].name
- annotations = {
- "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
- "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
- }
- }
-
- spec {
- tls {
- hosts = [var.gitea_domain]
- secret_name = "gitea-tls"
- }
- rule {
- host = var.gitea_domain
- http {
- path {
- path = "/"
- path_type = "Prefix"
- backend {
- service {
- name = "gitea"
- port { number = 3000 }
- }
- }
- }
- }
- }
- }
-
- depends_on = [helm_release.cert_manager]
-}
diff --git a/servers/legion/github-runner.tf b/servers/legion/github-runner.tf
index 1a85d40..47de7ed 100644
--- a/servers/legion/github-runner.tf
+++ b/servers/legion/github-runner.tf
@@ -1,12 +1,2 @@
-# GitHub Actions Self-Hosted Runner — org-level, serves all repos
-# Deployment managed by Argo CD — see servers/legion/apps/github-runner.yaml
-
-resource "kubernetes_secret" "github_runner_secret" {
- metadata {
- name = "github-runner-secret"
- namespace = kubernetes_namespace.ci.metadata[0].name
- }
- data = {
- ACCESS_TOKEN = var.github_classic_pat
- }
-}
+# GitHub Actions Self-Hosted Runner — secrets managed by Argo CD + ESO
+# See: apps/github-runner.yaml, apps/ci-config.yaml, k8s/github-runner/
diff --git a/servers/legion/headscale.tf b/servers/legion/headscale.tf
index fe2bf7f..a882fd0 100644
--- a/servers/legion/headscale.tf
+++ b/servers/legion/headscale.tf
@@ -1,7 +1,6 @@
-# Headscale — self-hosted Tailscale control plane
-# Coordination endpoint: https://headscale.neuralplatform.ai (Cloudflare tunnel)
-# Clients: Tailscale app on Mac/iOS/Android, pointed at custom server
-# Subnet router: tailscaled on Legion host, advertises k3s + LAN CIDRs
+# Headscale — namespace only
+# PVC, Deployment, Service managed by Argo CD
+# See: apps/headscale.yaml, apps/headscale-config.yaml, k8s/headscale/
resource "kubernetes_namespace" "headscale" {
metadata {
@@ -12,22 +11,3 @@ resource "kubernetes_namespace" "headscale" {
}
}
}
-
-resource "kubernetes_persistent_volume_claim" "headscale_data" {
- metadata {
- name = "headscale-data"
- namespace = kubernetes_namespace.headscale.metadata[0].name
- }
-
- # wait_until_bound = false because local-path only provisions on pod schedule
- wait_until_bound = false
-
- spec {
- access_modes = ["ReadWriteOnce"]
- resources {
- requests = {
- storage = "1Gi"
- }
- }
- }
-}
diff --git a/servers/legion/ingress.tf b/servers/legion/ingress.tf
index 480be40..13511b9 100644
--- a/servers/legion/ingress.tf
+++ b/servers/legion/ingress.tf
@@ -1,243 +1,4 @@
-# Traefik ingress routes for all services — TLS via cert-manager + Cloudflare DNS-01
-# Kong and open-webui ingresses moved to inactive/ — restore when needed
-
-resource "kubernetes_ingress_v1" "ollama" {
- metadata {
- name = "ollama"
- namespace = kubernetes_namespace.ollama.metadata[0].name
- annotations = {
- "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
- "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
- }
- }
-
- spec {
- tls {
- hosts = ["ollama.${var.infra_domain}"]
- secret_name = "ollama-tls"
- }
- rule {
- host = "ollama.${var.infra_domain}"
- http {
- path {
- path = "/"
- path_type = "Prefix"
- backend {
- service {
- name = "ollama"
- port { number = 11434 }
- }
- }
- }
- }
- }
- }
-
- depends_on = [helm_release.cert_manager]
-}
-
-
-resource "kubernetes_ingress_v1" "registry" {
- metadata {
- name = "registry"
- namespace = kubernetes_namespace.registry.metadata[0].name
- annotations = {
- "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
- "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
- "traefik.ingress.kubernetes.io/router.middlewares" = "registry-chunked@kubernetescrd"
- "nginx.ingress.kubernetes.io/proxy-body-size" = "0"
- "nginx.ingress.kubernetes.io/proxy-read-timeout" = "600"
- "nginx.ingress.kubernetes.io/proxy-send-timeout" = "600"
- }
- }
-
- spec {
- tls {
- hosts = ["registry.${var.infra_domain}"]
- secret_name = "registry-tls"
- }
- rule {
- host = "registry.${var.infra_domain}"
- http {
- path {
- path = "/"
- path_type = "Prefix"
- backend {
- service {
- name = "registry"
- port { number = 5000 }
- }
- }
- }
- }
- }
- }
-
- depends_on = [helm_release.cert_manager]
-}
-
-resource "kubernetes_ingress_v1" "registry_ui" {
- metadata {
- name = "registry-ui"
- namespace = kubernetes_namespace.registry.metadata[0].name
- annotations = {
- "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
- "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
- }
- }
-
- spec {
- tls {
- hosts = ["docker.neuralplatform.ai"]
- secret_name = "docker-ui-tls"
- }
- rule {
- host = "docker.neuralplatform.ai"
- http {
- path {
- path = "/"
- path_type = "Prefix"
- backend {
- service {
- name = "registry-ui"
- port { number = 80 }
- }
- }
- }
- }
- }
- }
-
- depends_on = [helm_release.cert_manager]
-}
-
-# npm registry — Verdaccio (proxies npmjs.org, caches locally)
-resource "kubernetes_ingress_v1" "npm_registry" {
- metadata {
- name = "npm-registry"
- namespace = kubernetes_namespace.packages.metadata[0].name
- annotations = {
- "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
- "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
- }
- }
-
- spec {
- tls {
- hosts = ["npm.neuralplatform.ai"]
- secret_name = "npm-registry-tls"
- }
- rule {
- host = "npm.neuralplatform.ai"
- http {
- path {
- path = "/"
- path_type = "Prefix"
- backend {
- service {
- name = "verdaccio"
- port { number = 4873 }
- }
- }
- }
- }
- }
- }
-
- depends_on = [helm_release.cert_manager]
-}
-
-# Alloy OTLP HTTP ingress — external log/trace ingestion from RunPod pods
-# Auth: HTTP Basic Auth (username: pantheon, password: ALLOY_PUSH_TOKEN from Vault)
-# Pods set: Authorization: Basic base64(pantheon:)
-# Traefik middleware strips auth before forwarding to Alloy.
-resource "kubernetes_manifest" "alloy_otlp_basicauth_middleware" {
- manifest = {
- apiVersion = "traefik.io/v1alpha1"
- kind = "Middleware"
- metadata = {
- name = "alloy-otlp-auth"
- namespace = "monitoring"
- }
- spec = {
- basicAuth = {
- secret = "alloy-otlp-auth"
- }
- }
- }
- depends_on = [kubernetes_secret.alloy_otlp_auth]
-}
-
-resource "kubernetes_ingress_v1" "alloy_otlp" {
- metadata {
- name = "alloy-otlp"
- namespace = "monitoring"
- annotations = {
- "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
- "traefik.ingress.kubernetes.io/router.middlewares" = "monitoring-alloy-otlp-auth@kubernetescrd"
- "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
- }
- }
- spec {
- tls {
- hosts = ["alloy.${var.infra_domain}"]
- secret_name = "alloy-otlp-tls"
- }
- rule {
- host = "alloy.${var.infra_domain}"
- http {
- path {
- path = "/"
- path_type = "Prefix"
- backend {
- service {
- name = "alloy-otlp"
- port { number = 4318 }
- }
- }
- }
- }
- }
- }
- depends_on = [
- kubernetes_manifest.alloy_otlp_basicauth_middleware,
- kubernetes_service.alloy_otlp,
- helm_release.cert_manager,
- ]
-}
-
-# PyPI registry — devpi (proxies pypi.org, caches locally)
-resource "kubernetes_ingress_v1" "pypi_registry" {
- metadata {
- name = "pypi-registry"
- namespace = kubernetes_namespace.packages.metadata[0].name
- annotations = {
- "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
- "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
- }
- }
-
- spec {
- tls {
- hosts = ["pypi.neuralplatform.ai"]
- secret_name = "pypi-registry-tls"
- }
- rule {
- host = "pypi.neuralplatform.ai"
- http {
- path {
- path = "/"
- path_type = "Prefix"
- backend {
- service {
- name = "devpi"
- port { number = 3141 }
- }
- }
- }
- }
- }
- }
-
- depends_on = [helm_release.cert_manager]
-}
+# Ingresses — all moved to k8s//ingress.yaml, managed by Argo CD
+# See: k8s/neuron/ingress.yaml, k8s/gitea/ingress.yaml, k8s/ollama/ingress.yaml,
+# k8s/registry/ingress.yaml, k8s/packages/ingress.yaml,
+# k8s/monitoring/ingress.yaml, k8s/adguard/ingress.yaml
diff --git a/servers/legion/k8s/adguard/certificate.yaml b/servers/legion/k8s/adguard/certificate.yaml
new file mode 100644
index 0000000..fd69de2
--- /dev/null
+++ b/servers/legion/k8s/adguard/certificate.yaml
@@ -0,0 +1,15 @@
+---
+# TLS certificate for DoT (DNS-over-TLS) on dot.nook.family:853
+# Issued via cert-manager DNS-01 challenge (Cloudflare)
+apiVersion: cert-manager.io/v1
+kind: Certificate
+metadata:
+ name: adguard-dot-tls
+ namespace: dns
+spec:
+ secretName: adguard-dot-tls
+ issuerRef:
+ name: letsencrypt-prod
+ kind: ClusterIssuer
+ dnsNames:
+ - dot.nook.family
diff --git a/servers/legion/k8s/adguard/configmap.yaml b/servers/legion/k8s/adguard/configmap.yaml
new file mode 100644
index 0000000..359f04d
--- /dev/null
+++ b/servers/legion/k8s/adguard/configmap.yaml
@@ -0,0 +1,59 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: adguard-defaults
+ namespace: dns
+data:
+ AdGuardHome.yaml: |
+ dns:
+ bind_hosts:
+ - 0.0.0.0
+ port: 53
+ upstream_dns:
+ - https://dns.cloudflare.com/dns-query
+ - https://dns.google/dns-query
+ bootstrap_dns:
+ - 1.1.1.1
+ - 8.8.8.8
+ - 9.9.9.10
+ - 149.112.112.10
+ upstream_mode: load_balance
+ cache_enabled: true
+ cache_size: 4194304
+ filters:
+ - enabled: true
+ url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt
+ name: AdGuard DNS filter
+ id: 1
+ - enabled: true
+ url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt
+ name: AdAway Default Blocklist
+ id: 2
+ - enabled: true
+ url: https://big.oisd.nl/domainswild
+ name: OISD Big
+ id: 3
+ - enabled: true
+ url: https://easylist.to/easylist/easylist.txt
+ name: EasyList
+ id: 4
+ - enabled: true
+ url: https://easylist.to/easylist/easyprivacy.txt
+ name: EasyPrivacy
+ id: 5
+ tls:
+ enabled: true
+ server_name: dot.nook.family
+ port_dns_over_tls: 853
+ certificate_path: /etc/adguard/tls/tls.crt
+ private_key_path: /etc/adguard/tls/tls.key
+ allow_unencrypted_doh: true
+ filtering:
+ filtering_enabled: true
+ rewrites:
+ - domain: '*.nook.family'
+ answer: 192.168.68.77
+ enabled: true
+ - domain: '*.neuralplatform.ai'
+ answer: 192.168.68.77
+ enabled: true
diff --git a/servers/legion/k8s/adguard/ddclient-deployment.yaml b/servers/legion/k8s/adguard/ddclient-deployment.yaml
new file mode 100644
index 0000000..dbdcc34
--- /dev/null
+++ b/servers/legion/k8s/adguard/ddclient-deployment.yaml
@@ -0,0 +1,39 @@
+---
+# ddclient — dynamic DNS daemon keeping dot.nook.family pointed at home public IP
+# Updates Cloudflare A record every 5 minutes (required for DoT port 853, no CF proxy)
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: ddclient
+ namespace: dns
+ labels:
+ app: ddclient
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: ddclient
+ template:
+ metadata:
+ labels:
+ app: ddclient
+ spec:
+ containers:
+ - name: ddclient
+ image: ghcr.io/ddclient/ddclient:latest
+ command: ["ddclient", "-file", "/etc/ddclient/ddclient.conf", "-daemon", "300", "-noquiet", "-foreground"]
+ volumeMounts:
+ - name: config
+ mountPath: /etc/ddclient/ddclient.conf
+ subPath: ddclient.conf
+ readOnly: true
+ resources:
+ requests:
+ memory: 32Mi
+ cpu: 10m
+ limits:
+ memory: 64Mi
+ volumes:
+ - name: config
+ secret:
+ secretName: ddclient-config
diff --git a/servers/legion/k8s/adguard/ddclient-external-secret.yaml b/servers/legion/k8s/adguard/ddclient-external-secret.yaml
new file mode 100644
index 0000000..6e3939a
--- /dev/null
+++ b/servers/legion/k8s/adguard/ddclient-external-secret.yaml
@@ -0,0 +1,39 @@
+---
+# ddclient-config — Cloudflare credentials for dynamic DNS updates
+# cloudflare_api_key and cloudflare_email stored in Vault at secret/cloudflare
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: ddclient-config
+ namespace: dns
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: ddclient-config
+ creationPolicy: Owner
+ template:
+ data:
+ ddclient.conf: |
+ daemon=300
+ syslog=yes
+ pid=/var/run/ddclient/ddclient.pid
+ use=web, web=https://api.ipify.org
+
+ protocol=cloudflare
+ zone=nook.family
+ ttl=120
+ login={{ .cloudflare_email }}
+ password={{ .cloudflare_api_key }}
+ dot.nook.family
+ data:
+ - secretKey: cloudflare_api_key
+ remoteRef:
+ key: secret/data/cloudflare
+ property: api_key
+ - secretKey: cloudflare_email
+ remoteRef:
+ key: secret/data/cloudflare
+ property: email
diff --git a/servers/legion/k8s/adguard/ingress.yaml b/servers/legion/k8s/adguard/ingress.yaml
new file mode 100644
index 0000000..3873b91
--- /dev/null
+++ b/servers/legion/k8s/adguard/ingress.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: adguard
+ namespace: dns
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+spec:
+ ingressClassName: traefik
+ tls:
+ - hosts:
+ - dns.nook.family
+ secretName: adguard-tls
+ rules:
+ - host: dns.nook.family
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: adguard-ui
+ port:
+ number: 3000
diff --git a/servers/legion/k8s/adguard/pvc.yaml b/servers/legion/k8s/adguard/pvc.yaml
new file mode 100644
index 0000000..5a24387
--- /dev/null
+++ b/servers/legion/k8s/adguard/pvc.yaml
@@ -0,0 +1,24 @@
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: adguard-config
+ namespace: dns
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 1Gi
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: adguard-data
+ namespace: dns
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 5Gi
diff --git a/servers/legion/k8s/backup/cronjob.yaml b/servers/legion/k8s/backup/cronjob.yaml
new file mode 100644
index 0000000..283d943
--- /dev/null
+++ b/servers/legion/k8s/backup/cronjob.yaml
@@ -0,0 +1,95 @@
+---
+# Nightly backup — dumps Postgres + Gitea data to Cloudflare R2 via restic
+# Schedule: 2am daily. Retains 30 daily / 12 weekly / 6 monthly snapshots.
+apiVersion: batch/v1
+kind: CronJob
+metadata:
+ name: gitea-backup
+ namespace: git
+spec:
+ schedule: "0 2 * * *"
+ concurrencyPolicy: Forbid
+ failedJobsHistoryLimit: 3
+ successfulJobsHistoryLimit: 3
+ jobTemplate:
+ spec:
+ backoffLimit: 2
+ template:
+ spec:
+ restartPolicy: OnFailure
+ initContainers:
+ - name: pg-dump
+ image: postgres:18-alpine
+ command:
+ - /bin/sh
+ - -c
+ - >-
+ pg_dumpall
+ -h postgres-postgresql.platform.svc.cluster.local
+ -U postgres
+ --no-role-passwords
+ > /dump/all-databases.sql
+ && echo 'DB dump complete, size:' $(du -sh /dump/all-databases.sql)
+ envFrom:
+ - secretRef:
+ name: backup-credentials
+ volumeMounts:
+ - name: dump
+ mountPath: /dump
+ resources:
+ requests:
+ memory: 128Mi
+ cpu: 100m
+ limits:
+ memory: 256Mi
+ containers:
+ - name: backup
+ image: restic/restic:latest
+ command:
+ - /bin/sh
+ - -c
+ - |
+ set -e
+
+ # Init repo if first run
+ restic snapshots 2>/dev/null || restic init
+
+ # Back up Gitea data + full DB dump
+ echo "Running restic backup..."
+ restic backup \
+ /gitea-data \
+ /dump/all-databases.sql \
+ --tag legion \
+ --host legion
+
+ # Prune old snapshots
+ restic forget \
+ --keep-daily 30 \
+ --keep-weekly 12 \
+ --keep-monthly 6 \
+ --prune
+
+ echo "Backup complete."
+ restic snapshots --latest 3
+ envFrom:
+ - secretRef:
+ name: backup-credentials
+ volumeMounts:
+ - name: gitea-data
+ mountPath: /gitea-data
+ readOnly: true
+ - name: dump
+ mountPath: /dump
+ resources:
+ requests:
+ memory: 256Mi
+ cpu: 100m
+ limits:
+ memory: 512Mi
+ volumes:
+ - name: gitea-data
+ persistentVolumeClaim:
+ claimName: gitea-data
+ readOnly: true
+ - name: dump
+ emptyDir: {}
diff --git a/servers/legion/k8s/backup/external-secrets.yaml b/servers/legion/k8s/backup/external-secrets.yaml
new file mode 100644
index 0000000..7c443eb
--- /dev/null
+++ b/servers/legion/k8s/backup/external-secrets.yaml
@@ -0,0 +1,35 @@
+---
+# backup-credentials — R2 access keys + restic password + Postgres password for nightly backup
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: backup-credentials
+ namespace: git
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: backup-credentials
+ creationPolicy: Owner
+ template:
+ data:
+ RESTIC_PASSWORD: "gitea-restic-2026"
+ AWS_ACCESS_KEY_ID: "{{ .r2_access_key_id }}"
+ AWS_SECRET_ACCESS_KEY: "{{ .r2_secret_access_key }}"
+ RESTIC_REPOSITORY: "s3:https://651161e0a3d321561b4c90b5bcd5f15b.r2.cloudflarestorage.com/legion-gitea-backup"
+ PGPASSWORD: "{{ .postgres_password }}"
+ data:
+ - secretKey: r2_access_key_id
+ remoteRef:
+ key: secret/data/r2
+ property: access_key_id
+ - secretKey: r2_secret_access_key
+ remoteRef:
+ key: secret/data/r2
+ property: secret_access_key
+ - secretKey: postgres_password
+ remoteRef:
+ key: secret/data/legion-db
+ property: postgres_password
diff --git a/servers/legion/k8s/external-secrets/cluster-secret-store.yaml b/servers/legion/k8s/external-secrets/cluster-secret-store.yaml
new file mode 100644
index 0000000..4c16fdf
--- /dev/null
+++ b/servers/legion/k8s/external-secrets/cluster-secret-store.yaml
@@ -0,0 +1,23 @@
+---
+# ClusterSecretStore — connects ESO to Vault using the root token Secret.
+# The vault-token Secret (name: vault-token, key: token) must be created
+# via kubectl as a one-time bootstrap step before this store can sync:
+#
+# kubectl create secret generic vault-token \
+# --namespace external-secrets \
+# --from-literal=token=$(cat ~/Secrets/tokens/vault-root-token)
+apiVersion: external-secrets.io/v1beta1
+kind: ClusterSecretStore
+metadata:
+ name: vault
+spec:
+ provider:
+ vault:
+ server: "https://vault.neuralplatform.ai"
+ path: "secret"
+ version: "v2"
+ auth:
+ tokenSecretRef:
+ name: vault-token
+ namespace: external-secrets
+ key: token
diff --git a/servers/legion/k8s/gitea-runner/external-secrets.yaml b/servers/legion/k8s/gitea-runner/external-secrets.yaml
new file mode 100644
index 0000000..a5a9a39
--- /dev/null
+++ b/servers/legion/k8s/gitea-runner/external-secrets.yaml
@@ -0,0 +1,24 @@
+---
+# gitea-runner-secret — Gitea act-runner registration token
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: gitea-runner-secret
+ namespace: ci
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: gitea-runner-secret
+ creationPolicy: Owner
+ data:
+ - secretKey: GITEA_INSTANCE_URL
+ remoteRef:
+ key: secret/data/gitea
+ property: instance_url
+ - secretKey: GITEA_RUNNER_REGISTRATION_TOKEN
+ remoteRef:
+ key: secret/data/gitea
+ property: runner_token
diff --git a/servers/legion/k8s/gitea/configmap.yaml b/servers/legion/k8s/gitea/configmap.yaml
new file mode 100644
index 0000000..518170d
--- /dev/null
+++ b/servers/legion/k8s/gitea/configmap.yaml
@@ -0,0 +1,104 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: gitea-custom-css
+ namespace: git
+data:
+ header.tmpl: |
+
+ custom.css: |
+ /* ── Typography & base ── */
+ :root {
+ --color-primary: #6366f1;
+ --color-primary-dark: #4f46e5;
+ --color-secondary: #8b5cf6;
+ --color-bg: #0f0f17;
+ --color-surface: #16161f;
+ --color-surface-2: #1e1e2e;
+ --color-border: #2a2a3d;
+ --color-text: #e2e2f0;
+ --color-text-muted: #8888aa;
+ --color-green: #22d3a5;
+ --radius: 10px;
+ }
+
+ body {
+ background: var(--color-bg) !important;
+ color: var(--color-text) !important;
+ font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif !important;
+ }
+
+ /* ── Top navbar ── */
+ .navbar, #navbar {
+ background: var(--color-surface) !important;
+ border-bottom: 1px solid var(--color-border) !important;
+ box-shadow: 0 1px 12px rgba(0,0,0,0.4) !important;
+ }
+
+ .navbar .brand svg, .navbar .brand img { filter: brightness(1.2); }
+
+ /* ── Sidebar & panels ── */
+ .repository, .ui.container, .ui.segment,
+ .ui.card, .ui.cards > .card {
+ background: var(--color-surface) !important;
+ border: 1px solid var(--color-border) !important;
+ border-radius: var(--radius) !important;
+ }
+
+ /* ── Buttons ── */
+ .ui.primary.button, .ui.green.button {
+ background: var(--color-primary) !important;
+ border: none !important;
+ border-radius: 8px !important;
+ }
+ .ui.primary.button:hover { background: var(--color-primary-dark) !important; }
+
+ /* ── Inputs ── */
+ input, textarea, select,
+ .ui.input > input, .ui.dropdown {
+ background: var(--color-surface-2) !important;
+ border: 1px solid var(--color-border) !important;
+ color: var(--color-text) !important;
+ border-radius: 8px !important;
+ }
+
+ /* ── Sign-in page ── */
+ .user.signin .ui.segment,
+ .user.signup .ui.segment {
+ background: var(--color-surface) !important;
+ border: 1px solid var(--color-border) !important;
+ border-radius: 16px !important;
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5) !important;
+ padding: 2.5rem !important;
+ }
+
+ /* ── Repo file tree ── */
+ .repository.file.list .file-list {
+ background: var(--color-surface) !important;
+ border-radius: var(--radius) !important;
+ border: 1px solid var(--color-border) !important;
+ }
+
+ .repository.file.list .file-list tr:hover td {
+ background: var(--color-surface-2) !important;
+ }
+
+ /* ── Labels & badges ── */
+ .ui.label { border-radius: 6px !important; }
+
+ /* ── Dashboard activity feed ── */
+ .feeds .news { border-bottom: 1px solid var(--color-border) !important; }
+
+ /* ── Code blocks ── */
+ pre, code {
+ background: var(--color-surface-2) !important;
+ border: 1px solid var(--color-border) !important;
+ border-radius: 6px !important;
+ }
+
+ /* ── Muted footer ── */
+ #footer {
+ background: var(--color-surface) !important;
+ border-top: 1px solid var(--color-border) !important;
+ color: var(--color-text-muted) !important;
+ }
diff --git a/servers/legion/k8s/gitea/external-secrets.yaml b/servers/legion/k8s/gitea/external-secrets.yaml
new file mode 100644
index 0000000..a239bdb
--- /dev/null
+++ b/servers/legion/k8s/gitea/external-secrets.yaml
@@ -0,0 +1,20 @@
+---
+# gitea-db — password used by the Gitea pod to connect to Postgres
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: gitea-db
+ namespace: git
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: gitea-db
+ creationPolicy: Owner
+ data:
+ - secretKey: password
+ remoteRef:
+ key: secret/data/gitea
+ property: db_password
diff --git a/servers/legion/k8s/gitea/ingress.yaml b/servers/legion/k8s/gitea/ingress.yaml
new file mode 100644
index 0000000..dddfda7
--- /dev/null
+++ b/servers/legion/k8s/gitea/ingress.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: gitea
+ namespace: git
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+spec:
+ ingressClassName: traefik
+ tls:
+ - hosts:
+ - git.neuralplatform.ai
+ secretName: gitea-tls
+ rules:
+ - host: git.neuralplatform.ai
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: gitea
+ port:
+ number: 3000
diff --git a/servers/legion/k8s/gitea/pvc.yaml b/servers/legion/k8s/gitea/pvc.yaml
new file mode 100644
index 0000000..3de652c
--- /dev/null
+++ b/servers/legion/k8s/gitea/pvc.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: gitea-data
+ namespace: git
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 20Gi
diff --git a/servers/legion/k8s/github-runner/external-secrets.yaml b/servers/legion/k8s/github-runner/external-secrets.yaml
new file mode 100644
index 0000000..e924bf0
--- /dev/null
+++ b/servers/legion/k8s/github-runner/external-secrets.yaml
@@ -0,0 +1,20 @@
+---
+# github-runner-secret — GitHub classic PAT for runner registration
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: github-runner-secret
+ namespace: ci
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: github-runner-secret
+ creationPolicy: Owner
+ data:
+ - secretKey: ACCESS_TOKEN
+ remoteRef:
+ key: secret/data/github
+ property: classic_pat
diff --git a/servers/legion/k8s/headscale/pvc.yaml b/servers/legion/k8s/headscale/pvc.yaml
new file mode 100644
index 0000000..d6c1e43
--- /dev/null
+++ b/servers/legion/k8s/headscale/pvc.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: headscale-data
+ namespace: headscale
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 1Gi
diff --git a/servers/legion/k8s/monitoring/alloy-otlp-middleware.yaml b/servers/legion/k8s/monitoring/alloy-otlp-middleware.yaml
new file mode 100644
index 0000000..a517209
--- /dev/null
+++ b/servers/legion/k8s/monitoring/alloy-otlp-middleware.yaml
@@ -0,0 +1,11 @@
+---
+# Traefik BasicAuth middleware — strips auth before forwarding to Alloy
+# Secret alloy-otlp-auth is synced from Vault via ExternalSecret (see external-secrets.yaml)
+apiVersion: traefik.io/v1alpha1
+kind: Middleware
+metadata:
+ name: alloy-otlp-auth
+ namespace: monitoring
+spec:
+ basicAuth:
+ secret: alloy-otlp-auth
diff --git a/servers/legion/k8s/monitoring/alloy-otlp-service.yaml b/servers/legion/k8s/monitoring/alloy-otlp-service.yaml
new file mode 100644
index 0000000..f9283dd
--- /dev/null
+++ b/servers/legion/k8s/monitoring/alloy-otlp-service.yaml
@@ -0,0 +1,18 @@
+---
+# Expose Alloy's OTLP HTTP port (4318) for external log/trace ingestion from RunPod pods
+apiVersion: v1
+kind: Service
+metadata:
+ name: alloy-otlp
+ namespace: monitoring
+ labels:
+ app: alloy-otlp
+spec:
+ selector:
+ app.kubernetes.io/name: alloy
+ ports:
+ - name: otlp-http
+ port: 4318
+ targetPort: 4318
+ protocol: TCP
+ type: ClusterIP
diff --git a/servers/legion/k8s/monitoring/external-secrets.yaml b/servers/legion/k8s/monitoring/external-secrets.yaml
new file mode 100644
index 0000000..ab689fc
--- /dev/null
+++ b/servers/legion/k8s/monitoring/external-secrets.yaml
@@ -0,0 +1,23 @@
+---
+# alloy-otlp-auth — bcrypt htpasswd entry for Traefik BasicAuth middleware
+# Username: pantheon, password: alloy push token from Vault secret/alloy
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: alloy-otlp-auth
+ namespace: monitoring
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: alloy-otlp-auth
+ creationPolicy: Owner
+ # ESO does not bcrypt — the htpasswd entry must be pre-hashed in Vault.
+ # Store via: vault kv patch secret/alloy htpasswd_entry="pantheon:$(htpasswd -nbB pantheon | cut -d: -f2)"
+ data:
+ - secretKey: users
+ remoteRef:
+ key: secret/data/alloy
+ property: htpasswd_entry
diff --git a/servers/legion/k8s/monitoring/grafana-datasources.yaml b/servers/legion/k8s/monitoring/grafana-datasources.yaml
new file mode 100644
index 0000000..948de89
--- /dev/null
+++ b/servers/legion/k8s/monitoring/grafana-datasources.yaml
@@ -0,0 +1,37 @@
+---
+# Grafana extra datasources — Loki and Tempo with cross-linking
+# Discovered by Grafana sidecar via label grafana_datasource=1
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: grafana-extra-datasources
+ namespace: monitoring
+ labels:
+ grafana_datasource: "1"
+data:
+ datasources.yaml: |
+ apiVersion: 1
+ datasources:
+ - name: Loki
+ type: loki
+ uid: loki
+ url: http://loki-gateway.monitoring.svc.cluster.local
+ access: proxy
+ jsonData:
+ httpHeaderName1: X-Scope-OrgID
+ secureJsonData:
+ httpHeaderValue1: legion
+ - name: Tempo
+ type: tempo
+ uid: tempo
+ url: http://tempo.monitoring.svc.cluster.local:3100
+ access: proxy
+ jsonData:
+ lokiSearch:
+ datasourceUid: loki
+ tracesToLogsV2:
+ datasourceUid: loki
+ filterByTraceID: true
+ filterBySpanID: false
+ tracesToMetrics:
+ datasourceUid: prometheus
diff --git a/servers/legion/k8s/monitoring/grafana-external-secrets.yaml b/servers/legion/k8s/monitoring/grafana-external-secrets.yaml
new file mode 100644
index 0000000..b52d810
--- /dev/null
+++ b/servers/legion/k8s/monitoring/grafana-external-secrets.yaml
@@ -0,0 +1,40 @@
+---
+# grafana-admin-secret — Grafana admin password from Vault
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: grafana-admin-secret
+ namespace: monitoring
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: grafana-admin-secret
+ creationPolicy: Owner
+ data:
+ - secretKey: admin-password
+ remoteRef:
+ key: secret/data/grafana
+ property: admin_password
+---
+# slack-bot-token — Slack bot token for Alertmanager notifications
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: slack-bot-token
+ namespace: monitoring
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: slack-bot-token
+ creationPolicy: Owner
+ data:
+ - secretKey: bot_token
+ remoteRef:
+ key: secret/data/slack
+ property: bot_token
diff --git a/servers/legion/k8s/monitoring/ingress.yaml b/servers/legion/k8s/monitoring/ingress.yaml
new file mode 100644
index 0000000..1af264a
--- /dev/null
+++ b/servers/legion/k8s/monitoring/ingress.yaml
@@ -0,0 +1,29 @@
+---
+# Alloy OTLP HTTP ingress — external log/trace ingestion from RunPod pods
+# Auth: HTTP Basic Auth (username: pantheon, password: ALLOY_PUSH_TOKEN)
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: alloy-otlp
+ namespace: monitoring
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ traefik.ingress.kubernetes.io/router.middlewares: monitoring-alloy-otlp-auth@kubernetescrd
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+spec:
+ ingressClassName: traefik
+ tls:
+ - hosts:
+ - alloy.neuralplatform.ai
+ secretName: alloy-otlp-tls
+ rules:
+ - host: alloy.neuralplatform.ai
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: alloy-otlp
+ port:
+ number: 4318
diff --git a/servers/legion/k8s/neuron/configmap.yaml b/servers/legion/k8s/neuron/configmap.yaml
new file mode 100644
index 0000000..a1f7cb0
--- /dev/null
+++ b/servers/legion/k8s/neuron/configmap.yaml
@@ -0,0 +1,27 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: neuron-config
+ namespace: neuron
+data:
+ MCP_PORT: "8001"
+ MCP_HOST: "0.0.0.0"
+ WEBHOOK_PORT: "3847"
+ NEURON_DB_PATH: "/data/neuron.db"
+ AXON_DB_PATH: "/data/axon.db"
+ TUNNEL_NAME: "neural-platform"
+ default.yaml: |
+ runtime:
+ name: neuron
+ version: 0.1.0
+ storage:
+ db_path: /data/neuron.db
+ cache:
+ max_l1_size: 1000
+ gc:
+ gen0_threshold: 3
+ gen1_threshold: 5
+ knowledge:
+ base_path: /data/knowledge
+ synapse:
+ path: /data/synapse
diff --git a/servers/legion/k8s/neuron/external-secrets.yaml b/servers/legion/k8s/neuron/external-secrets.yaml
new file mode 100644
index 0000000..c9a70e1
--- /dev/null
+++ b/servers/legion/k8s/neuron/external-secrets.yaml
@@ -0,0 +1,52 @@
+---
+# neuron-secrets — GITHUB_WEBHOOK_SECRET, GITEA_WEBHOOK_SECRET, SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: neuron-secrets
+ namespace: neuron
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: neuron-secrets
+ creationPolicy: Owner
+ data:
+ - secretKey: GITHUB_WEBHOOK_SECRET
+ remoteRef:
+ key: secret/data/github
+ property: webhook_secret
+ - secretKey: GITEA_WEBHOOK_SECRET
+ remoteRef:
+ key: secret/data/gitea
+ property: api_token
+ - secretKey: SLACK_BOT_TOKEN
+ remoteRef:
+ key: secret/data/slack
+ property: bot_token
+ - secretKey: SLACK_SIGNING_SECRET
+ remoteRef:
+ key: secret/data/slack
+ property: signing_secret
+---
+# cloudflared-secret — TUNNEL_TOKEN for the Cloudflare tunnel
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: cloudflared-secret
+ namespace: neuron
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: cloudflared-secret
+ creationPolicy: Owner
+ data:
+ - secretKey: TUNNEL_TOKEN
+ remoteRef:
+ key: secret/data/cloudflare-tunnel
+ property: token
diff --git a/servers/legion/k8s/neuron/ingress.yaml b/servers/legion/k8s/neuron/ingress.yaml
new file mode 100644
index 0000000..aac1b56
--- /dev/null
+++ b/servers/legion/k8s/neuron/ingress.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: neuron
+ namespace: neuron
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+spec:
+ ingressClassName: traefik
+ tls:
+ - secretName: neuron-tls
+ hosts:
+ - neuron.neuralplatform.ai
+ rules:
+ - host: neuron.neuralplatform.ai
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: neuron
+ port:
+ number: 8001
diff --git a/servers/legion/k8s/neuron/pvc.yaml b/servers/legion/k8s/neuron/pvc.yaml
new file mode 100644
index 0000000..5926d0e
--- /dev/null
+++ b/servers/legion/k8s/neuron/pvc.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: neuron-data
+ namespace: neuron
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 5Gi
diff --git a/servers/legion/k8s/ollama/ingress.yaml b/servers/legion/k8s/ollama/ingress.yaml
new file mode 100644
index 0000000..500d0bc
--- /dev/null
+++ b/servers/legion/k8s/ollama/ingress.yaml
@@ -0,0 +1,25 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: ollama
+ namespace: ollama
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+spec:
+ ingressClassName: traefik
+ tls:
+ - hosts:
+ - ollama.neuralplatform.ai
+ secretName: ollama-tls
+ rules:
+ - host: ollama.neuralplatform.ai
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: ollama
+ port:
+ number: 11434
diff --git a/servers/legion/k8s/ollama/pvc.yaml b/servers/legion/k8s/ollama/pvc.yaml
new file mode 100644
index 0000000..1493199
--- /dev/null
+++ b/servers/legion/k8s/ollama/pvc.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: ollama-models
+ namespace: ollama
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 50Gi
diff --git a/servers/legion/k8s/packages/configmap.yaml b/servers/legion/k8s/packages/configmap.yaml
new file mode 100644
index 0000000..bea3ed8
--- /dev/null
+++ b/servers/legion/k8s/packages/configmap.yaml
@@ -0,0 +1,32 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: verdaccio-config
+ namespace: packages
+data:
+ config.yaml: |
+ storage: /verdaccio/storage
+ auth:
+ htpasswd:
+ file: /verdaccio/storage/htpasswd
+ max_users: 1
+ uplinks:
+ npmjs:
+ url: https://registry.npmjs.org/
+ cache: true
+ packages:
+ '@harmonic-framework/*':
+ access: $authenticated
+ publish: $authenticated
+ unpublish: $authenticated
+ '**':
+ access: $all
+ publish: $authenticated
+ proxy: npmjs
+ listen: 0.0.0.0:4873
+ server:
+ keepAliveTimeout: 60
+ log:
+ type: stdout
+ format: pretty
+ level: http
diff --git a/servers/legion/k8s/packages/ingress.yaml b/servers/legion/k8s/packages/ingress.yaml
new file mode 100644
index 0000000..6ef4738
--- /dev/null
+++ b/servers/legion/k8s/packages/ingress.yaml
@@ -0,0 +1,54 @@
+---
+# npm registry — Verdaccio
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: npm-registry
+ namespace: packages
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+spec:
+ ingressClassName: traefik
+ tls:
+ - hosts:
+ - npm.neuralplatform.ai
+ secretName: npm-registry-tls
+ rules:
+ - host: npm.neuralplatform.ai
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: verdaccio
+ port:
+ number: 4873
+---
+# PyPI registry — devpi
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: pypi-registry
+ namespace: packages
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+spec:
+ ingressClassName: traefik
+ tls:
+ - hosts:
+ - pypi.neuralplatform.ai
+ secretName: pypi-registry-tls
+ rules:
+ - host: pypi.neuralplatform.ai
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: devpi
+ port:
+ number: 3141
diff --git a/servers/legion/k8s/packages/pvc.yaml b/servers/legion/k8s/packages/pvc.yaml
new file mode 100644
index 0000000..f9c2ea0
--- /dev/null
+++ b/servers/legion/k8s/packages/pvc.yaml
@@ -0,0 +1,24 @@
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: verdaccio-data
+ namespace: packages
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 10Gi
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: devpi-data
+ namespace: packages
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 10Gi
diff --git a/servers/legion/k8s/postgres/external-secrets.yaml b/servers/legion/k8s/postgres/external-secrets.yaml
new file mode 100644
index 0000000..e8c7829
--- /dev/null
+++ b/servers/legion/k8s/postgres/external-secrets.yaml
@@ -0,0 +1,25 @@
+---
+# postgres-passwords — Postgres superuser + Kong DB passwords from Vault
+# These are passed to the Bitnami postgresql chart via the app's Helm values.
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: postgres-passwords
+ namespace: platform
+spec:
+ refreshInterval: 1h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: postgres-passwords
+ creationPolicy: Owner
+ data:
+ - secretKey: postgres-password
+ remoteRef:
+ key: secret/data/legion-db
+ property: postgres_password
+ - secretKey: kong-db-password
+ remoteRef:
+ key: secret/data/legion-db
+ property: kong_db_password
diff --git a/servers/legion/k8s/registry/ingress.yaml b/servers/legion/k8s/registry/ingress.yaml
new file mode 100644
index 0000000..fdb782e
--- /dev/null
+++ b/servers/legion/k8s/registry/ingress.yaml
@@ -0,0 +1,58 @@
+---
+# Docker registry ingress with chunked upload middleware
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: registry
+ namespace: registry
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+ traefik.ingress.kubernetes.io/router.middlewares: registry-chunked@kubernetescrd
+ nginx.ingress.kubernetes.io/proxy-body-size: "0"
+ nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
+ nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
+spec:
+ ingressClassName: traefik
+ tls:
+ - hosts:
+ - registry.neuralplatform.ai
+ secretName: registry-tls
+ rules:
+ - host: registry.neuralplatform.ai
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: registry
+ port:
+ number: 5000
+---
+# Docker registry UI
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: registry-ui
+ namespace: registry
+ annotations:
+ traefik.ingress.kubernetes.io/router.entrypoints: websecure
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+spec:
+ ingressClassName: traefik
+ tls:
+ - hosts:
+ - docker.neuralplatform.ai
+ secretName: docker-ui-tls
+ rules:
+ - host: docker.neuralplatform.ai
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: registry-ui
+ port:
+ number: 80
diff --git a/servers/legion/k8s/registry/pvc.yaml b/servers/legion/k8s/registry/pvc.yaml
new file mode 100644
index 0000000..91941e6
--- /dev/null
+++ b/servers/legion/k8s/registry/pvc.yaml
@@ -0,0 +1,11 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: registry-data
+ namespace: registry
+spec:
+ accessModes:
+ - ReadWriteOnce
+ resources:
+ requests:
+ storage: 50Gi
diff --git a/servers/legion/k8s/vault/gcp-sa-external-secret.yaml b/servers/legion/k8s/vault/gcp-sa-external-secret.yaml
new file mode 100644
index 0000000..8f9eec0
--- /dev/null
+++ b/servers/legion/k8s/vault/gcp-sa-external-secret.yaml
@@ -0,0 +1,22 @@
+---
+# vault-gcp-sa — GCP service account JSON for KMS auto-unseal
+# The SA JSON is stored in Vault at secret/data/vault-gcp-sa
+# Store it: vault kv put secret/vault-gcp-sa sa_json=@vault-gcp-sa.json
+apiVersion: external-secrets.io/v1beta1
+kind: ExternalSecret
+metadata:
+ name: vault-gcp-sa
+ namespace: vault
+spec:
+ refreshInterval: 24h
+ secretStoreRef:
+ name: vault
+ kind: ClusterSecretStore
+ target:
+ name: vault-gcp-sa
+ creationPolicy: Owner
+ data:
+ - secretKey: sa.json
+ remoteRef:
+ key: secret/data/vault-gcp-sa
+ property: sa_json
diff --git a/servers/legion/monitoring.tf b/servers/legion/monitoring.tf
index 5db4f83..1787997 100644
--- a/servers/legion/monitoring.tf
+++ b/servers/legion/monitoring.tf
@@ -1,8 +1,7 @@
-# Monitoring stack — Prometheus, Grafana, Loki, Tempo, Alloy, Alertmanager
-# Grafana UI: https://grafana.neuralplatform.ai (admin / var.grafana_admin_password)
-#
-# TODO: Migrate Tempo to tempo-distributed once R2 is wired as trace storage backend
-# TODO: Replace loki.useTestSchema=true with proper schema_config for production
+# Monitoring stack — Prometheus, Grafana, Loki, Tempo, Alloy
+# Namespace only — all Helm releases and config managed by Argo CD + ESO
+# See: apps/kube-prometheus-stack.yaml, apps/loki.yaml, apps/tempo.yaml, apps/alloy.yaml
+# See: apps/monitoring-config.yaml, k8s/monitoring/
resource "kubernetes_namespace" "monitoring" {
metadata {
@@ -13,372 +12,3 @@ resource "kubernetes_namespace" "monitoring" {
}
}
}
-
-# kube-prometheus-stack — Prometheus, Grafana, Alertmanager, node exporters
-resource "helm_release" "kube_prometheus_stack" {
- name = "kube-prometheus-stack"
- namespace = kubernetes_namespace.monitoring.metadata[0].name
- repository = "https://prometheus-community.github.io/helm-charts"
- chart = "kube-prometheus-stack"
- timeout = 600
-
- # Grafana
- set {
- name = "grafana.adminPassword"
- value = var.grafana_admin_password
- }
- set {
- name = "grafana.persistence.enabled"
- value = "true"
- }
- set {
- name = "grafana.persistence.size"
- value = "5Gi"
- }
- set {
- name = "grafana.sidecar.datasources.defaultDatasourceEnabled"
- value = "false"
- }
- set {
- name = "grafana.initChownData.enabled"
- value = "false"
- }
- set {
- name = "grafana.ingress.enabled"
- value = "true"
- }
- set {
- name = "grafana.ingress.ingressClassName"
- value = "traefik"
- }
- set {
- name = "grafana.ingress.hosts[0]"
- value = "grafana.${var.infra_domain}"
- }
- set {
- name = "grafana.ingress.tls[0].secretName"
- value = "grafana-tls"
- }
- set {
- name = "grafana.ingress.tls[0].hosts[0]"
- value = "grafana.${var.infra_domain}"
- }
- set {
- name = "grafana.ingress.annotations.traefik\\.ingress\\.kubernetes\\.io/router\\.entrypoints"
- value = "websecure"
- }
- set {
- name = "grafana.ingress.annotations.cert-manager\\.io/cluster-issuer"
- value = "letsencrypt-prod"
- }
-
- # Prometheus persistent storage
- set {
- name = "prometheus.prometheusSpec.storageSpec.volumeClaimTemplate.spec.resources.requests.storage"
- value = "20Gi"
- }
-
- # Disable k3s-incompatible scrapers
- set {
- name = "kubeEtcd.enabled"
- value = "false"
- }
- set {
- name = "kubeControllerManager.enabled"
- value = "false"
- }
- set {
- name = "kubeScheduler.enabled"
- value = "false"
- }
- set {
- name = "kubeProxy.enabled"
- value = "false"
- }
-
- # Alertmanager — route alerts to #infrastructure-alerts in Slack
- values = [<<-EOT
- alertmanager:
- config:
- global:
- resolve_timeout: 5m
- inhibit_rules:
- - source_matchers: [severity="critical"]
- target_matchers: [severity=~"warning|info"]
- equal: [namespace, alertname]
- - source_matchers: [severity="warning"]
- target_matchers: [severity="info"]
- equal: [namespace, alertname]
- receivers:
- - name: "null"
- - name: slack-alerts
- slack_configs:
- - channel: "#infrastructure-alerts"
- api_url: "https://slack.com/api/chat.postMessage"
- http_config:
- authorization:
- type: Bearer
- credentials: "${var.slack_bot_token}"
- send_resolved: true
- title: '[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .CommonLabels.alertname }} ({{ .CommonLabels.namespace }})'
- text: |
- {{ range .Alerts }}
- *{{ if .Annotations.summary }}{{ .Annotations.summary }}{{ else }}{{ .Labels.alertname }}{{ end }}*{{ if .Labels.severity }} · {{ .Labels.severity }}{{ end }}
- {{ if .Annotations.description }}{{ .Annotations.description }}{{ end }}
- {{ end }}
- footer: "Legion · neuralplatform.ai"
- route:
- group_by: [namespace, alertname]
- group_wait: 30s
- group_interval: 5m
- repeat_interval: 4h
- receiver: slack-alerts
- routes:
- - matchers: [alertname="Watchdog"]
- receiver: "null"
- - matchers: [severity="info"]
- receiver: "null"
- EOT
- ]
-
- depends_on = [helm_release.cert_manager]
-}
-
-# Grafana extra datasources — Loki and Tempo wired up with cross-linking
-resource "kubernetes_config_map" "grafana_extra_datasources" {
- metadata {
- name = "grafana-extra-datasources"
- namespace = kubernetes_namespace.monitoring.metadata[0].name
- labels = { grafana_datasource = "1" }
- }
-
- data = {
- "datasources.yaml" = <<-EOT
- apiVersion: 1
- datasources:
- - name: Loki
- type: loki
- uid: loki
- url: http://loki-gateway.monitoring.svc.cluster.local
- access: proxy
- jsonData:
- httpHeaderName1: X-Scope-OrgID
- secureJsonData:
- httpHeaderValue1: legion
- - name: Tempo
- type: tempo
- uid: tempo
- url: http://tempo.monitoring.svc.cluster.local:3100
- access: proxy
- jsonData:
- lokiSearch:
- datasourceUid: loki
- tracesToLogsV2:
- datasourceUid: loki
- filterByTraceID: true
- filterBySpanID: false
- tracesToMetrics:
- datasourceUid: prometheus
- EOT
- }
-
- depends_on = [helm_release.kube_prometheus_stack]
-}
-
-# Loki — log aggregation (single binary mode)
-# TODO: Replace useTestSchema with proper schema_config before storing long-term logs
-resource "helm_release" "loki" {
- name = "loki"
- namespace = kubernetes_namespace.monitoring.metadata[0].name
- repository = "https://grafana.github.io/helm-charts"
- chart = "loki"
- timeout = 300
-
- set {
- name = "loki.commonConfig.replication_factor"
- value = "1"
- }
- set {
- name = "loki.storage.type"
- value = "filesystem"
- }
- set {
- name = "loki.useTestSchema"
- value = "true"
- }
- set {
- name = "singleBinary.replicas"
- value = "1"
- }
- set {
- name = "singleBinary.persistence.enabled"
- value = "true"
- }
- set {
- name = "singleBinary.persistence.size"
- value = "10Gi"
- }
- set {
- name = "deploymentMode"
- value = "SingleBinary"
- }
- set {
- name = "backend.replicas"
- value = "0"
- }
- set {
- name = "read.replicas"
- value = "0"
- }
- set {
- name = "write.replicas"
- value = "0"
- }
- set {
- name = "chunksCache.enabled"
- value = "false"
- }
- set {
- name = "resultsCache.enabled"
- value = "false"
- }
-}
-
-# Tempo — distributed tracing backend (local storage)
-# TODO: Migrate to tempo-distributed with R2 storage once R2 trace bucket is created
-resource "helm_release" "tempo" {
- name = "tempo"
- namespace = kubernetes_namespace.monitoring.metadata[0].name
- repository = "https://grafana.github.io/helm-charts"
- chart = "tempo"
- timeout = 300
-
- set {
- name = "tempo.storage.trace.backend"
- value = "local"
- }
- set {
- name = "tempo.storage.trace.local.path"
- value = "/var/tempo"
- }
- set {
- name = "persistence.enabled"
- value = "true"
- }
- set {
- name = "persistence.size"
- value = "10Gi"
- }
-}
-
-# Alloy — log shipping + OTLP trace forwarding (replaces deprecated Promtail)
-resource "helm_release" "alloy" {
- name = "alloy"
- namespace = kubernetes_namespace.monitoring.metadata[0].name
- repository = "https://grafana.github.io/helm-charts"
- chart = "alloy"
- timeout = 300
-
- set {
- name = "controller.type"
- value = "daemonset"
- }
-
- values = [<<-EOT
- alloy:
- configMap:
- content: |
- discovery.kubernetes "pods" {
- role = "pod"
- }
-
- discovery.relabel "pods" {
- targets = discovery.kubernetes.pods.targets
- rule {
- source_labels = ["__meta_kubernetes_namespace"]
- target_label = "namespace"
- }
- rule {
- source_labels = ["__meta_kubernetes_pod_name"]
- target_label = "pod"
- }
- rule {
- source_labels = ["__meta_kubernetes_pod_container_name"]
- target_label = "container"
- }
- rule {
- source_labels = ["__meta_kubernetes_pod_label_app"]
- target_label = "app"
- }
- }
-
- loki.source.kubernetes "pods" {
- targets = discovery.relabel.pods.output
- forward_to = [loki.write.default.receiver]
- }
-
- loki.write "default" {
- endpoint {
- url = "http://loki-gateway.monitoring.svc.cluster.local/loki/api/v1/push"
- tenant_id = "legion"
- }
- }
-
- otelcol.receiver.otlp "default" {
- grpc { endpoint = "0.0.0.0:4317" }
- http { endpoint = "0.0.0.0:4318" }
- output {
- traces = [otelcol.exporter.otlp.tempo.input]
- logs = [otelcol.exporter.loki.pantheon.input]
- }
- }
-
- otelcol.exporter.otlp "tempo" {
- client {
- endpoint = "tempo.monitoring.svc.cluster.local:4317"
- tls { insecure = true }
- }
- }
-
- otelcol.exporter.loki "pantheon" {
- forward_to = [loki.write.default.receiver]
- }
- EOT
- ]
-
- depends_on = [helm_release.loki, helm_release.tempo]
-}
-
-# Expose Alloy's OTLP HTTP port (4318) for external log/trace ingestion from RunPod pods
-resource "kubernetes_service" "alloy_otlp" {
- metadata {
- name = "alloy-otlp"
- namespace = kubernetes_namespace.monitoring.metadata[0].name
- labels = { app = "alloy-otlp" }
- }
- spec {
- selector = { "app.kubernetes.io/name" = "alloy" }
- port {
- name = "otlp-http"
- port = 4318
- target_port = 4318
- protocol = "TCP"
- }
- type = "ClusterIP"
- }
- depends_on = [helm_release.alloy]
-}
-
-# BasicAuth secret for Traefik middleware — pods authenticate with
-# Authorization: Basic base64(pantheon:)
-resource "kubernetes_secret" "alloy_otlp_auth" {
- metadata {
- name = "alloy-otlp-auth"
- namespace = kubernetes_namespace.monitoring.metadata[0].name
- }
- data = {
- # htpasswd entry — bcrypt hash of the push token, username "pantheon"
- users = "pantheon:${bcrypt(var.alloy_push_token)}"
- }
- depends_on = [kubernetes_namespace.monitoring]
-}
diff --git a/servers/legion/namespaces.tf b/servers/legion/namespaces.tf
index 3e885c5..ff6169b 100644
--- a/servers/legion/namespaces.tf
+++ b/servers/legion/namespaces.tf
@@ -17,3 +17,13 @@ resource "kubernetes_namespace" "ci" {
}
}
}
+
+resource "kubernetes_namespace" "external_secrets" {
+ metadata {
+ name = "external-secrets"
+ labels = {
+ managed-by = "terraform"
+ tier = "infrastructure"
+ }
+ }
+}
diff --git a/servers/legion/neuron.tf b/servers/legion/neuron.tf
index 6369a07..d45ac76 100644
--- a/servers/legion/neuron.tf
+++ b/servers/legion/neuron.tf
@@ -1,12 +1,6 @@
# Neuron — MCP server + Axon webhook hub
-# MCP: https://neuron.nook.family (internal, Traefik)
-# Webhooks: https://axon.neuralplatform.ai (external, Cloudflare tunnel)
-#
-# Image: registry.nook.family/neural-platform/neuron:latest
-# Built by GitHub Actions runner on Legion — see neural-platform/neuron ci.yml
-#
-# TODO: Migrate SQLite → Postgres once neuron db migration is ready
-# TODO: Wire Gmail OAuth credentials via k8s Secret (currently unauthenticated)
+# Namespace only — all other resources managed by Argo CD + ESO
+# See: apps/neuron.yaml, apps/neuron-config.yaml, k8s/neuron/
resource "kubernetes_namespace" "neuron" {
metadata {
@@ -17,120 +11,3 @@ resource "kubernetes_namespace" "neuron" {
}
}
}
-
-resource "kubernetes_persistent_volume_claim" "neuron_data" {
- metadata {
- name = "neuron-data"
- namespace = kubernetes_namespace.neuron.metadata[0].name
- }
-
- spec {
- access_modes = ["ReadWriteOnce"]
- resources {
- requests = {
- storage = "5Gi"
- }
- }
- }
-}
-
-resource "kubernetes_config_map" "neuron_config" {
- metadata {
- name = "neuron-config"
- namespace = kubernetes_namespace.neuron.metadata[0].name
- }
-
- data = {
- MCP_PORT = "8001"
- MCP_HOST = "0.0.0.0"
- WEBHOOK_PORT = "3847"
- NEURON_DB_PATH = "/data/neuron.db"
- AXON_DB_PATH = "/data/axon.db"
- TUNNEL_NAME = "neural-platform"
-
- "default.yaml" = <<-EOT
- runtime:
- name: neuron
- version: 0.1.0
- storage:
- db_path: /data/neuron.db
- cache:
- max_l1_size: 1000
- gc:
- gen0_threshold: 3
- gen1_threshold: 5
- knowledge:
- base_path: /data/knowledge
- synapse:
- path: /data/synapse
- EOT
- }
-}
-
-resource "kubernetes_secret" "neuron_secrets" {
- metadata {
- name = "neuron-secrets"
- namespace = kubernetes_namespace.neuron.metadata[0].name
- }
-
- data = {
- GITHUB_WEBHOOK_SECRET = var.github_webhook_secret
- GITEA_WEBHOOK_SECRET = var.gitea_webhook_secret
- SLACK_BOT_TOKEN = var.slack_bot_token
- SLACK_SIGNING_SECRET = var.slack_signing_secret
- }
-}
-
-# Deployment + Service + cloudflared managed by Argo CD — see servers/legion/apps/neuron.yaml
-
-resource "kubernetes_secret" "cloudflared_secret" {
- metadata {
- name = "cloudflared-secret"
- namespace = kubernetes_namespace.neuron.metadata[0].name
- }
- data = {
- TUNNEL_TOKEN = var.cloudflared_tunnel_token
- }
-}
-
-# Internal MCP ingress — accessible within nook.family network
-resource "kubernetes_ingress_v1" "neuron" {
- metadata {
- name = "neuron"
- namespace = kubernetes_namespace.neuron.metadata[0].name
- annotations = {
- "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure"
- "cert-manager.io/cluster-issuer" = "letsencrypt-prod"
- }
- }
-
- spec {
- ingress_class_name = "traefik"
-
- tls {
- secret_name = "neuron-tls"
- hosts = ["neuron.${var.infra_domain}"]
- }
-
- rule {
- host = "neuron.${var.infra_domain}"
- http {
- path {
- path = "/"
- path_type = "Prefix"
- backend {
- service {
- name = "neuron"
- port {
- number = 8001
- }
- }
- }
- }
- }
- }
- }
-
- depends_on = [helm_release.cert_manager]
-}
-
diff --git a/servers/legion/ollama.tf b/servers/legion/ollama.tf
index 16bc408..530c1a8 100644
--- a/servers/legion/ollama.tf
+++ b/servers/legion/ollama.tf
@@ -1,4 +1,7 @@
-# Ollama — local LLM inference with GPU
+# Ollama — namespace only
+# PVC, Deployment, Service, Ingress managed by Argo CD
+# See: apps/ollama.yaml, apps/ollama-config.yaml, k8s/ollama/
+
resource "kubernetes_namespace" "ollama" {
metadata {
name = "ollama"
@@ -8,21 +11,3 @@ resource "kubernetes_namespace" "ollama" {
}
}
}
-
-resource "kubernetes_persistent_volume_claim" "ollama_models" {
- metadata {
- name = "ollama-models"
- namespace = kubernetes_namespace.ollama.metadata[0].name
- }
-
- spec {
- access_modes = ["ReadWriteOnce"]
- resources {
- requests = {
- storage = "50Gi"
- }
- }
- }
-}
-
-# Deployment + Service managed by Argo CD — see servers/legion/apps/ollama.yaml
diff --git a/servers/legion/packages.tf b/servers/legion/packages.tf
index 715d4e1..2e1231f 100644
--- a/servers/legion/packages.tf
+++ b/servers/legion/packages.tf
@@ -1,5 +1,6 @@
-# Package registries — Verdaccio (npm) and devpi (PyPI)
-# Both proxy to public registries and cache locally on Legion
+# Package registries — namespace only
+# PVCs, ConfigMap, Ingresses managed by Argo CD
+# See: apps/packages.yaml, apps/packages-config.yaml, k8s/packages/
resource "kubernetes_namespace" "packages" {
metadata {
@@ -10,69 +11,3 @@ resource "kubernetes_namespace" "packages" {
}
}
}
-
-resource "kubernetes_persistent_volume_claim" "verdaccio_data" {
- metadata {
- name = "verdaccio-data"
- namespace = kubernetes_namespace.packages.metadata[0].name
- }
- spec {
- access_modes = ["ReadWriteOnce"]
- resources {
- requests = { storage = "10Gi" }
- }
- }
-}
-
-resource "kubernetes_persistent_volume_claim" "devpi_data" {
- metadata {
- name = "devpi-data"
- namespace = kubernetes_namespace.packages.metadata[0].name
- }
- spec {
- access_modes = ["ReadWriteOnce"]
- resources {
- requests = { storage = "10Gi" }
- }
- }
-}
-
-# Verdaccio — npm proxy/cache
-resource "kubernetes_config_map" "verdaccio_config" {
- metadata {
- name = "verdaccio-config"
- namespace = kubernetes_namespace.packages.metadata[0].name
- }
-
- data = {
- "config.yaml" = <<-EOT
- storage: /verdaccio/storage
- auth:
- htpasswd:
- file: /verdaccio/storage/htpasswd
- max_users: 1
- uplinks:
- npmjs:
- url: https://registry.npmjs.org/
- cache: true
- packages:
- '@harmonic-framework/*':
- access: $authenticated
- publish: $authenticated
- unpublish: $authenticated
- '**':
- access: $all
- publish: $authenticated
- proxy: npmjs
- listen: 0.0.0.0:4873
- server:
- keepAliveTimeout: 60
- log:
- type: stdout
- format: pretty
- level: http
- EOT
- }
-}
-
-# Deployments + Services managed by Argo CD — see servers/legion/apps/packages.yaml
diff --git a/servers/legion/postgres.tf b/servers/legion/postgres.tf
index 09b5507..a1a4938 100644
--- a/servers/legion/postgres.tf
+++ b/servers/legion/postgres.tf
@@ -1,34 +1,3 @@
-# Shared Postgres — Kong, app databases
-resource "helm_release" "postgres" {
- name = "postgres"
- namespace = kubernetes_namespace.platform.metadata[0].name
- repository = "oci://registry-1.docker.io/bitnamicharts"
- chart = "postgresql"
-
- timeout = 600
-
- set {
- name = "auth.postgresPassword"
- value = var.postgres_password
- }
-
- set {
- name = "primary.persistence.size"
- value = "20Gi"
- }
-
- set {
- name = "primary.resources.requests.memory"
- value = "256Mi"
- }
-
- set {
- name = "primary.resources.requests.cpu"
- value = "100m"
- }
-
- set {
- name = "primary.initdb.scripts.create-kong-db\\.sql"
- value = "CREATE DATABASE kong; CREATE USER kong WITH PASSWORD '${var.kong_db_password}'; GRANT ALL PRIVILEGES ON DATABASE kong TO kong; ALTER DATABASE kong OWNER TO kong;"
- }
-}
+# Shared Postgres — namespace only
+# Helm release and secrets managed by Argo CD + ESO
+# See: apps/postgres.yaml, apps/postgres-config.yaml, k8s/postgres/
diff --git a/servers/legion/redis.tf b/servers/legion/redis.tf
index f8308b6..cdf6f1a 100644
--- a/servers/legion/redis.tf
+++ b/servers/legion/redis.tf
@@ -1,34 +1,3 @@
-# Shared Redis — caching, session store, pub/sub
-resource "helm_release" "redis" {
- name = "redis"
- namespace = kubernetes_namespace.platform.metadata[0].name
- repository = "oci://registry-1.docker.io/bitnamicharts"
- chart = "redis"
-
- timeout = 600
-
- set {
- name = "auth.enabled"
- value = "false"
- }
-
- set {
- name = "architecture"
- value = "standalone"
- }
-
- set {
- name = "master.persistence.size"
- value = "5Gi"
- }
-
- set {
- name = "master.resources.requests.memory"
- value = "128Mi"
- }
-
- set {
- name = "master.resources.requests.cpu"
- value = "50m"
- }
-}
+# Shared Redis — namespace only
+# Helm release managed by Argo CD
+# See: apps/redis.yaml
diff --git a/servers/legion/registry.tf b/servers/legion/registry.tf
index c812c50..5a6da69 100644
--- a/servers/legion/registry.tf
+++ b/servers/legion/registry.tf
@@ -1,4 +1,7 @@
-# Local Docker Registry — push images locally, no DockerHub
+# Local Docker Registry — namespace only
+# PVC, Deployment, Service, Ingresses managed by Argo CD
+# See: apps/registry.yaml, apps/registry-config.yaml, k8s/registry/
+
resource "kubernetes_namespace" "registry" {
metadata {
name = "registry"
@@ -8,21 +11,3 @@ resource "kubernetes_namespace" "registry" {
}
}
}
-
-resource "kubernetes_persistent_volume_claim" "registry_data" {
- metadata {
- name = "registry-data"
- namespace = kubernetes_namespace.registry.metadata[0].name
- }
-
- spec {
- access_modes = ["ReadWriteOnce"]
- resources {
- requests = {
- storage = "50Gi"
- }
- }
- }
-}
-
-# Deployments + Services managed by Argo CD — see servers/legion/apps/registry.yaml
diff --git a/servers/legion/variables.tf b/servers/legion/variables.tf
index 6c5531f..a3ab42d 100644
--- a/servers/legion/variables.tf
+++ b/servers/legion/variables.tf
@@ -11,7 +11,7 @@ variable "legion_ip" {
}
variable "domain_suffix" {
- description = "Internal-only domain (nook.family) — not exposed via Cloudflare tunnel. Use infra_domain for public services."
+ description = "Internal-only domain (nook.family)"
type = string
default = "nook.family"
}
@@ -34,116 +34,8 @@ variable "cloudflare_email" {
default = "andersonwilliam85@gmail.com"
}
-variable "gitea_domain" {
- description = "Domain for Gitea git server"
- type = string
- default = "git.neuralplatform.ai"
-}
-
-variable "gitea_admin_password" {
- description = "Gitea admin user password"
- type = string
- sensitive = true
-}
-
-variable "gitea_db_password" {
- description = "Gitea database password"
- type = string
- sensitive = true
-}
-
-variable "postgres_password" {
- description = "Postgres superuser password"
- type = string
- sensitive = true
-}
-
-variable "kong_db_password" {
- description = "Kong database password"
- type = string
- sensitive = true
-}
-
-variable "grafana_admin_password" {
- description = "Grafana admin password"
- type = string
- sensitive = true
-}
-
-variable "github_classic_pat" {
- description = "GitHub classic PAT with admin:org scope for runner registration"
- type = string
- sensitive = true
-}
-
-variable "github_org" {
- description = "GitHub organization"
- type = string
- default = "harmonic-framework"
-}
-
-variable "r2_endpoint" {
- description = "Cloudflare R2 S3-compatible endpoint"
- type = string
- default = "https://651161e0a3d321561b4c90b5bcd5f15b.r2.cloudflarestorage.com"
-}
-
-variable "r2_access_key_id" {
- description = "Cloudflare R2 access key ID"
- type = string
- sensitive = true
-}
-
-variable "r2_secret_access_key" {
- description = "Cloudflare R2 secret access key"
- type = string
- sensitive = true
-}
-
-variable "cloudflared_tunnel_token" {
- description = "Cloudflare tunnel token for neural-platform tunnel"
- type = string
- sensitive = true
-}
-
-variable "slack_bot_token" {
- description = "Slack bot token for Neuron bot"
- type = string
- sensitive = true
-}
-
-variable "slack_signing_secret" {
- description = "Slack signing secret for webhook verification"
- type = string
- sensitive = true
-}
-
-variable "github_webhook_secret" {
- description = "GitHub webhook secret for Axon event ingestion"
- type = string
- sensitive = true
-}
-
-variable "gitea_webhook_secret" {
- description = "Gitea webhook secret for Axon event ingestion"
- type = string
- sensitive = true
-}
-
variable "gitea_api_token" {
description = "Gitea API token for Argo CD repo access"
type = string
sensitive = true
}
-
-variable "gitea_runner_token" {
- description = "Gitea act-runner registration token"
- type = string
- sensitive = true
-}
-
-variable "alloy_push_token" {
- description = "Bearer token required to push OTLP logs/traces to alloy.neuralplatform.ai"
- type = string
- sensitive = true
-}
diff --git a/servers/legion/vault.tf b/servers/legion/vault.tf
index 6049c41..9836a59 100644
--- a/servers/legion/vault.tf
+++ b/servers/legion/vault.tf
@@ -1,6 +1,8 @@
-# Vault — secrets management with GCP KMS auto-unseal
-# UI: https://vault.neuralplatform.ai
-# Auto-unseal: GCP KMS projects/neuron-785695/locations/global/keyRings/vault/cryptoKeys/vault-unseal
+# Vault — namespace only
+# Helm release and GCP SA secret managed by Argo CD + ESO
+# See: apps/vault.yaml, apps/vault-config.yaml, k8s/vault/
+# Note: vault-gcp-sa.json must be stored in Vault at secret/vault-gcp-sa (sa_json field)
+# before the ESO ExternalSecret can sync it.
resource "kubernetes_namespace" "vault" {
metadata {
@@ -11,99 +13,3 @@ resource "kubernetes_namespace" "vault" {
}
}
}
-
-resource "kubernetes_secret" "vault_gcp_sa" {
- metadata {
- name = "vault-gcp-sa"
- namespace = kubernetes_namespace.vault.metadata[0].name
- }
-
- data = {
- "sa.json" = file("${path.module}/vault-gcp-sa.json")
- }
-}
-
-resource "helm_release" "vault" {
- name = "vault"
- namespace = kubernetes_namespace.vault.metadata[0].name
- repository = "https://helm.releases.hashicorp.com"
- chart = "vault"
- version = "0.29.1"
- timeout = 300
-
- values = [<<-EOT
- server:
- image:
- repository: hashicorp/vault
- tag: "1.18.3"
-
- extraEnvironmentVars:
- GOOGLE_APPLICATION_CREDENTIALS: /vault/gcp/sa.json
-
- volumes:
- - name: gcp-sa
- secret:
- secretName: vault-gcp-sa
-
- volumeMounts:
- - name: gcp-sa
- mountPath: /vault/gcp
- readOnly: true
-
- ha:
- enabled: false
-
- dataStorage:
- enabled: true
- size: 10Gi
-
- standalone:
- enabled: true
- config: |
- ui = true
-
- listener "tcp" {
- tls_disable = 1
- address = "[::]:8200"
- cluster_address = "[::]:8201"
- }
-
- storage "file" {
- path = "/vault/data"
- }
-
- seal "gcpckms" {
- project = "neuron-785695"
- region = "global"
- key_ring = "vault"
- crypto_key = "vault-unseal"
- }
-
- ingress:
- enabled: true
- ingressClassName: traefik
- annotations:
- traefik.ingress.kubernetes.io/router.entrypoints: websecure
- cert-manager.io/cluster-issuer: letsencrypt-prod
- hosts:
- - host: vault.${var.infra_domain}
- paths:
- - /
- tls:
- - secretName: vault-tls
- hosts:
- - vault.${var.infra_domain}
-
- ui:
- enabled: true
-
- injector:
- enabled: false
- EOT
- ]
-
- depends_on = [
- kubernetes_secret.vault_gcp_sa,
- helm_release.cert_manager,
- ]
-}