Migrate k8s config from Terraform to Argo CD + ESO

Phase 1: Install External Secrets Operator via Argo CD app
- apps/external-secrets.yaml — ESO Helm chart install
- apps/external-secrets-config.yaml — ClusterSecretStore deployment
- k8s/external-secrets/cluster-secret-store.yaml — Vault backend using vault-token Secret

Phase 2: Create k8s manifests for all services
- k8s/neuron/ — PVC, ConfigMap, ExternalSecrets (neuron-secrets, cloudflared-secret), Ingress
- k8s/gitea/ — PVC, ConfigMap (custom CSS), ExternalSecret (gitea-db), Ingress
- k8s/github-runner/ — ExternalSecret (github-runner-secret)
- k8s/gitea-runner/ — ExternalSecret (gitea-runner-secret)
- k8s/monitoring/ — ExternalSecrets (grafana, slack), Alloy OTLP service+middleware, datasources ConfigMap, Ingress
- k8s/postgres/ — ExternalSecret (postgres-passwords)
- k8s/vault/ — ExternalSecret (vault-gcp-sa from Vault)
- k8s/adguard/ — PVCs, ConfigMap, Certificate, Ingress, ddclient Deployment+ExternalSecret
- k8s/ollama/ — PVC, Ingress
- k8s/headscale/ — PVC
- k8s/packages/ — PVCs, ConfigMap, Ingresses
- k8s/registry/ — PVC, Ingresses
- k8s/backup/ — CronJob, ExternalSecret (backup-credentials)

New Argo CD apps for Helm releases:
- apps/kube-prometheus-stack.yaml, loki.yaml, tempo.yaml, alloy.yaml
- apps/postgres.yaml, redis.yaml, vault.yaml

New Argo CD apps for k8s config paths:
- apps/neuron-config, gitea-config, ci-config, gitea-runner-config
- apps/monitoring-config, adguard-config, ollama-config, headscale-config
- apps/packages-config, registry-config, postgres-config, vault-config, backup

Phase 3: Strip Terraform to infrastructure-only
- All kubernetes_* and helm_release resources removed from service .tf files
- Each service .tf now contains only kubernetes_namespace (bootstrap dependency)
- variables.tf stripped to only cloudflare_api_key, cloudflare_email, gitea_api_token
- namespaces.tf gains external-secrets namespace
- ingress.tf, backup.tf, ddclient.tf emptied (resources in k8s/)
- cert-manager.tf, argocd.tf, traefik.tf unchanged (bootstrap)
This commit is contained in:
Will Anderson
2026-03-25 10:29:14 -05:00
parent dd0f8a49a3
commit 4d0cfb1bbf
74 changed files with 1895 additions and 1726 deletions
+3 -167
View File
@@ -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
+20
View File
@@ -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
+82
View File
@@ -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
+20
View File
@@ -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
+41
View File
@@ -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
@@ -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
+23
View File
@@ -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
+20
View File
@@ -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
+20
View File
@@ -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
@@ -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
+44
View File
@@ -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
@@ -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
+20
View File
@@ -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
+20
View File
@@ -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
+20
View File
@@ -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
+20
View File
@@ -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
+41
View File
@@ -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
+32
View File
@@ -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
+20
View File
@@ -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
+31
View File
@@ -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
+20
View File
@@ -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
+89
View File
@@ -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
+2 -137
View File
@@ -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/
+3 -73
View File
@@ -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
+2 -14
View File
@@ -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/
+2 -176
View File
@@ -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
<link rel="stylesheet" href="/assets/css/custom.css">
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]
}
+2 -12
View File
@@ -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/
+3 -23
View File
@@ -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"
}
}
}
}
+4 -243
View File
@@ -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:<token>)
# 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/<service>/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
@@ -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
+59
View File
@@ -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
@@ -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
@@ -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
+25
View File
@@ -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
+24
View File
@@ -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
+95
View File
@@ -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: {}
@@ -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
@@ -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
@@ -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
+104
View File
@@ -0,0 +1,104 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: gitea-custom-css
namespace: git
data:
header.tmpl: |
<link rel="stylesheet" href="/assets/css/custom.css">
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;
}
@@ -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
+25
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-data
namespace: git
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
@@ -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
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: headscale-data
namespace: headscale
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
@@ -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
@@ -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
@@ -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 <token> | cut -d: -f2)"
data:
- secretKey: users
remoteRef:
key: secret/data/alloy
property: htpasswd_entry
@@ -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
@@ -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
@@ -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
+27
View File
@@ -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
@@ -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
+25
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: neuron-data
namespace: neuron
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 5Gi
+25
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: ollama-models
namespace: ollama
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
@@ -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
+54
View File
@@ -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
+24
View File
@@ -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
@@ -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
+58
View File
@@ -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
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: registry-data
namespace: registry
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 50Gi
@@ -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
+4 -374
View File
@@ -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:<ALLOY_PUSH_TOKEN>)
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]
}
+10
View File
@@ -17,3 +17,13 @@ resource "kubernetes_namespace" "ci" {
}
}
}
resource "kubernetes_namespace" "external_secrets" {
metadata {
name = "external-secrets"
labels = {
managed-by = "terraform"
tier = "infrastructure"
}
}
}
+2 -125
View File
@@ -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]
}
+4 -19
View File
@@ -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
+3 -68
View File
@@ -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
+3 -34
View File
@@ -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/
+3 -34
View File
@@ -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
+4 -19
View File
@@ -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
+1 -109
View File
@@ -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
}
+5 -99
View File
@@ -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,
]
}