Initial commit — migrate legion Terraform from infrastructure repo
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
|||||||
|
# Terraform
|
||||||
|
**/.terraform/
|
||||||
|
**/*.tfstate
|
||||||
|
**/*.tfstate.backup
|
||||||
|
**/tfplan
|
||||||
|
**/.terraform.lock.hcl
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
**/*.json
|
||||||
|
**/*.env
|
||||||
|
**/.envrc
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# servers
|
||||||
|
|
||||||
|
Infrastructure-as-code for all servers. Currently: Legion (Ubuntu 24.04, k3s).
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
- `legion/` — Legion k3s cluster (Terraform)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd legion
|
||||||
|
direnv allow # loads credentials from ~/Secrets/credentials/infrastructure.env
|
||||||
|
terraform plan
|
||||||
|
terraform apply
|
||||||
|
```
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
.terraform/
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.backup
|
||||||
|
.terraform.lock.hcl
|
||||||
|
terraform.tfvars
|
||||||
|
.envrc
|
||||||
|
vault-gcp-sa.json
|
||||||
@@ -0,0 +1,271 @@
|
|||||||
|
# AdGuard Home — household DNS with ad blocking and local nook.family resolution
|
||||||
|
|
||||||
|
resource "kubernetes_namespace" "dns" {
|
||||||
|
metadata {
|
||||||
|
name = "dns"
|
||||||
|
labels = {
|
||||||
|
managed-by = "terraform"
|
||||||
|
tier = "infrastructure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_deployment" "adguard" {
|
||||||
|
metadata {
|
||||||
|
name = "adguard"
|
||||||
|
namespace = kubernetes_namespace.dns.metadata[0].name
|
||||||
|
labels = {
|
||||||
|
app = "adguard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
|
||||||
|
strategy {
|
||||||
|
type = "Recreate"
|
||||||
|
}
|
||||||
|
|
||||||
|
selector {
|
||||||
|
match_labels = {
|
||||||
|
app = "adguard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = {
|
||||||
|
app = "adguard"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
# hostNetwork gives AdGuard direct access to port 53 on the host IP
|
||||||
|
host_network = true
|
||||||
|
dns_policy = "ClusterFirstWithHostNet"
|
||||||
|
|
||||||
|
# Ensure bind_hosts is always 0.0.0.0 regardless of what was written to the PVC
|
||||||
|
init_container {
|
||||||
|
name = "fix-bind-hosts"
|
||||||
|
image = "python:3.12-alpine"
|
||||||
|
command = ["python3", "-c"]
|
||||||
|
args = [<<-EOT
|
||||||
|
import re, os
|
||||||
|
config = "/opt/adguardhome/conf/AdGuardHome.yaml"
|
||||||
|
if os.path.exists(config):
|
||||||
|
with open(config) as f:
|
||||||
|
content = f.read()
|
||||||
|
# Replace any bind_hosts list with 0.0.0.0
|
||||||
|
content = re.sub(
|
||||||
|
r'(bind_hosts:\n)((?:[ \t]*- .*\n)*)',
|
||||||
|
'bind_hosts:\n - 0.0.0.0\n',
|
||||||
|
content
|
||||||
|
)
|
||||||
|
with open(config, "w") as f:
|
||||||
|
f.write(content)
|
||||||
|
print("bind_hosts set to 0.0.0.0")
|
||||||
|
else:
|
||||||
|
print("No config yet — AdGuard will create it on first run")
|
||||||
|
EOT
|
||||||
|
]
|
||||||
|
volume_mount {
|
||||||
|
name = "config"
|
||||||
|
mount_path = "/opt/adguardhome/conf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container {
|
||||||
|
name = "adguard"
|
||||||
|
image = "adguard/adguardhome:latest"
|
||||||
|
|
||||||
|
port {
|
||||||
|
name = "dns-tcp"
|
||||||
|
container_port = 53
|
||||||
|
protocol = "TCP"
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
name = "dns-udp"
|
||||||
|
container_port = 53
|
||||||
|
protocol = "UDP"
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
name = "http"
|
||||||
|
container_port = 3000
|
||||||
|
protocol = "TCP"
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "config"
|
||||||
|
mount_path = "/opt/adguardhome/conf"
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "data"
|
||||||
|
mount_path = "/opt/adguardhome/work"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
memory = "128Mi"
|
||||||
|
cpu = "100m"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "512Mi"
|
||||||
|
cpu = "500m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "config"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.adguard_config.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "data"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.adguard_data.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# UI accessible via Traefik ingress (port 3000 → ingress)
|
||||||
|
resource "kubernetes_service" "adguard_ui" {
|
||||||
|
metadata {
|
||||||
|
name = "adguard-ui"
|
||||||
|
namespace = kubernetes_namespace.dns.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
selector = {
|
||||||
|
app = "adguard"
|
||||||
|
}
|
||||||
|
|
||||||
|
port {
|
||||||
|
name = "http"
|
||||||
|
port = 3000
|
||||||
|
target_port = 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
type = "ClusterIP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.${var.domain_suffix}"]
|
||||||
|
secret_name = "adguard-tls"
|
||||||
|
}
|
||||||
|
|
||||||
|
rule {
|
||||||
|
host = "dns.${var.domain_suffix}"
|
||||||
|
http {
|
||||||
|
path {
|
||||||
|
path = "/"
|
||||||
|
path_type = "Prefix"
|
||||||
|
backend {
|
||||||
|
service {
|
||||||
|
name = kubernetes_service.adguard_ui.metadata[0].name
|
||||||
|
port {
|
||||||
|
number = 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [helm_release.cert_manager]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add new LAN IP to k3s TLS SANs so the API cert is valid after network change
|
||||||
|
resource "null_resource" "k3s_tls_san" {
|
||||||
|
triggers = {
|
||||||
|
legion_ip = var.legion_ip
|
||||||
|
}
|
||||||
|
|
||||||
|
connection {
|
||||||
|
type = "ssh"
|
||||||
|
host = var.legion_ip
|
||||||
|
user = "will"
|
||||||
|
private_key = file("~/.ssh/id_ed25519")
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "remote-exec" {
|
||||||
|
inline = [
|
||||||
|
"sudo mkdir -p /etc/rancher/k3s",
|
||||||
|
"echo 'tls-san:' | sudo tee /etc/rancher/k3s/config.yaml",
|
||||||
|
"echo ' - ${var.legion_ip}' | sudo tee -a /etc/rancher/k3s/config.yaml",
|
||||||
|
"echo ' - 127.0.0.1' | sudo tee -a /etc/rancher/k3s/config.yaml",
|
||||||
|
"sudo k3s certificate rotate",
|
||||||
|
"sudo systemctl restart k3s",
|
||||||
|
"sleep 20",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# systemd-resolved occupies port 53 on the host — must be disabled for AdGuard to bind
|
||||||
|
resource "null_resource" "disable_systemd_resolved" {
|
||||||
|
connection {
|
||||||
|
type = "ssh"
|
||||||
|
host = var.legion_ip
|
||||||
|
user = "will"
|
||||||
|
private_key = file("~/.ssh/id_ed25519")
|
||||||
|
}
|
||||||
|
|
||||||
|
provisioner "remote-exec" {
|
||||||
|
inline = [
|
||||||
|
"sudo systemctl disable --now systemd-resolved || true",
|
||||||
|
"sudo rm -f /etc/resolv.conf",
|
||||||
|
"echo 'nameserver 1.1.1.1' | sudo tee /etc/resolv.conf",
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
# 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"
|
||||||
|
POSTGRES_PASSWORD = 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 {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
# cert-manager — TLS certificate management via Let's Encrypt + Cloudflare DNS-01
|
||||||
|
|
||||||
|
resource "helm_release" "cert_manager" {
|
||||||
|
name = "cert-manager"
|
||||||
|
repository = "https://charts.jetstack.io"
|
||||||
|
chart = "cert-manager"
|
||||||
|
version = "v1.17.1"
|
||||||
|
namespace = "cert-manager"
|
||||||
|
create_namespace = true
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "crds.enabled"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_secret" "cloudflare_api_key" {
|
||||||
|
metadata {
|
||||||
|
name = "cloudflare-api-key"
|
||||||
|
namespace = "cert-manager"
|
||||||
|
}
|
||||||
|
|
||||||
|
data = {
|
||||||
|
api-key = var.cloudflare_api_key
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [helm_release.cert_manager]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_manifest" "cluster_issuer_prod" {
|
||||||
|
manifest = {
|
||||||
|
apiVersion = "cert-manager.io/v1"
|
||||||
|
kind = "ClusterIssuer"
|
||||||
|
metadata = {
|
||||||
|
name = "letsencrypt-prod"
|
||||||
|
}
|
||||||
|
spec = {
|
||||||
|
acme = {
|
||||||
|
server = "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
|
email = var.cloudflare_email
|
||||||
|
privateKeySecretRef = {
|
||||||
|
name = "letsencrypt-prod"
|
||||||
|
}
|
||||||
|
solvers = [{
|
||||||
|
dns01 = {
|
||||||
|
cloudflare = {
|
||||||
|
email = var.cloudflare_email
|
||||||
|
apiKeySecretRef = {
|
||||||
|
name = "cloudflare-api-key"
|
||||||
|
key = "api-key"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [
|
||||||
|
helm_release.cert_manager,
|
||||||
|
kubernetes_secret.cloudflare_api_key,
|
||||||
|
]
|
||||||
|
}
|
||||||
+361
@@ -0,0 +1,361 @@
|
|||||||
|
# Gitea — local git server for private/sensitive infrastructure repos
|
||||||
|
# Accessible at git.neuralplatform.dev (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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_namespace" "git" {
|
||||||
|
metadata {
|
||||||
|
name = "git"
|
||||||
|
labels = {
|
||||||
|
managed-by = "terraform"
|
||||||
|
tier = "infrastructure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_deployment" "gitea" {
|
||||||
|
metadata {
|
||||||
|
name = "gitea"
|
||||||
|
namespace = kubernetes_namespace.git.metadata[0].name
|
||||||
|
labels = { app = "gitea" }
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
strategy { type = "Recreate" }
|
||||||
|
|
||||||
|
selector {
|
||||||
|
match_labels = { app = "gitea" }
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = { app = "gitea" }
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
container {
|
||||||
|
name = "gitea"
|
||||||
|
image = "gitea/gitea:1.22"
|
||||||
|
|
||||||
|
port {
|
||||||
|
name = "http"
|
||||||
|
container_port = 3000
|
||||||
|
}
|
||||||
|
port {
|
||||||
|
name = "ssh"
|
||||||
|
container_port = 22
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "GITEA__database__DB_TYPE"
|
||||||
|
value = "postgres"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__database__HOST"
|
||||||
|
value = "postgres-postgresql.platform.svc.cluster.local:5432"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__database__NAME"
|
||||||
|
value = "gitea"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__database__USER"
|
||||||
|
value = "gitea"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__database__PASSWD"
|
||||||
|
value_from {
|
||||||
|
secret_key_ref {
|
||||||
|
name = kubernetes_secret.gitea_db_secret.metadata[0].name
|
||||||
|
key = "password"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__server__DOMAIN"
|
||||||
|
value = var.gitea_domain
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__server__ROOT_URL"
|
||||||
|
value = "https://${var.gitea_domain}"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__server__SSH_DOMAIN"
|
||||||
|
value = var.gitea_domain
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__server__SSH_PORT"
|
||||||
|
value = "30022"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__server__START_SSH_SERVER"
|
||||||
|
value = "false"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__service__DISABLE_REGISTRATION"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__service__REQUIRE_SIGNIN_VIEW"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__security__INSTALL_LOCK"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "GITEA__packages__ENABLED"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "GITEA__webhook__ALLOWED_HOST_LIST"
|
||||||
|
value = "192.168.0.0/24,10.0.0.0/8"
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "data"
|
||||||
|
mount_path = "/data"
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "custom-css"
|
||||||
|
mount_path = "/data/gitea/custom/public/assets/css/custom.css"
|
||||||
|
sub_path = "custom.css"
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "custom-css"
|
||||||
|
mount_path = "/data/gitea/custom/templates/custom/header.tmpl"
|
||||||
|
sub_path = "header.tmpl"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
memory = "256Mi"
|
||||||
|
cpu = "100m"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "512Mi"
|
||||||
|
cpu = "500m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "data"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.gitea_data.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "custom-css"
|
||||||
|
config_map {
|
||||||
|
name = kubernetes_config_map.gitea_custom_css.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [helm_release.postgres]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "gitea" {
|
||||||
|
metadata {
|
||||||
|
name = "gitea"
|
||||||
|
namespace = kubernetes_namespace.git.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
selector = { app = "gitea" }
|
||||||
|
|
||||||
|
port {
|
||||||
|
name = "http"
|
||||||
|
port = 3000
|
||||||
|
target_port = 3000
|
||||||
|
}
|
||||||
|
|
||||||
|
port {
|
||||||
|
name = "ssh"
|
||||||
|
port = 22
|
||||||
|
target_port = 22
|
||||||
|
node_port = 30022
|
||||||
|
}
|
||||||
|
|
||||||
|
type = "NodePort"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = kubernetes_service.gitea.metadata[0].name
|
||||||
|
port { number = 3000 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [helm_release.cert_manager]
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
# GitHub Actions Self-Hosted Runner — org-level, serves all repos
|
||||||
|
resource "kubernetes_deployment" "github_runner" {
|
||||||
|
metadata {
|
||||||
|
name = "github-runner"
|
||||||
|
namespace = kubernetes_namespace.ci.metadata[0].name
|
||||||
|
labels = {
|
||||||
|
app = "github-runner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
|
||||||
|
selector {
|
||||||
|
match_labels = {
|
||||||
|
app = "github-runner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = {
|
||||||
|
app = "github-runner"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
container {
|
||||||
|
name = "runner"
|
||||||
|
image = "myoung34/github-runner:latest"
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ACCESS_TOKEN"
|
||||||
|
value = var.github_classic_pat
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "RUNNER_SCOPE"
|
||||||
|
value = "org"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ORG_NAME"
|
||||||
|
value = "harmonic-framework"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "RUNNER_NAME"
|
||||||
|
value = "legion"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "RUNNER_WORKDIR"
|
||||||
|
value = "/tmp/runner"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "LABELS"
|
||||||
|
value = "self-hosted,linux,x64,legion"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "DISABLE_AUTO_UPDATE"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "docker-sock"
|
||||||
|
mount_path = "/var/run/docker.sock"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
memory = "512Mi"
|
||||||
|
cpu = "250m"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "4Gi"
|
||||||
|
cpu = "4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "docker-sock"
|
||||||
|
host_path {
|
||||||
|
path = "/var/run/docker.sock"
|
||||||
|
type = "Socket"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Kong API Gateway — backed by Postgres
|
||||||
|
resource "helm_release" "kong" {
|
||||||
|
name = "kong"
|
||||||
|
namespace = kubernetes_namespace.platform.metadata[0].name
|
||||||
|
repository = "https://charts.konghq.com"
|
||||||
|
chart = "kong"
|
||||||
|
|
||||||
|
timeout = 600
|
||||||
|
|
||||||
|
depends_on = [helm_release.postgres]
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "env.database"
|
||||||
|
value = "postgres"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "env.pg_host"
|
||||||
|
value = "postgres-postgresql.platform.svc.cluster.local"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "env.pg_user"
|
||||||
|
value = "kong"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "env.pg_password"
|
||||||
|
value = var.kong_db_password
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "env.pg_database"
|
||||||
|
value = "kong"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "proxy.type"
|
||||||
|
value = "ClusterIP"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "admin.enabled"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "admin.type"
|
||||||
|
value = "ClusterIP"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "admin.http.enabled"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "manager.enabled"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "manager.type"
|
||||||
|
value = "ClusterIP"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "ingressController.enabled"
|
||||||
|
value = "false"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "migrations.preUpgrade"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
set {
|
||||||
|
name = "migrations.postUpgrade"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
# Open WebUI — chat interface for Ollama with image generation
|
||||||
|
resource "kubernetes_namespace" "webui" {
|
||||||
|
metadata {
|
||||||
|
name = "webui"
|
||||||
|
labels = {
|
||||||
|
managed-by = "terraform"
|
||||||
|
tier = "application"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_persistent_volume_claim" "webui_data" {
|
||||||
|
metadata {
|
||||||
|
name = "webui-data"
|
||||||
|
namespace = kubernetes_namespace.webui.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
access_modes = ["ReadWriteOnce"]
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
storage = "5Gi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_deployment" "open_webui" {
|
||||||
|
metadata {
|
||||||
|
name = "open-webui"
|
||||||
|
namespace = kubernetes_namespace.webui.metadata[0].name
|
||||||
|
labels = {
|
||||||
|
app = "open-webui"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
|
||||||
|
selector {
|
||||||
|
match_labels = {
|
||||||
|
app = "open-webui"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = {
|
||||||
|
app = "open-webui"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
container {
|
||||||
|
name = "open-webui"
|
||||||
|
image = "ghcr.io/open-webui/open-webui:main"
|
||||||
|
|
||||||
|
port {
|
||||||
|
container_port = 8080
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "OLLAMA_BASE_URL"
|
||||||
|
value = "http://ollama.ollama.svc.cluster.local:11434"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "AUTOMATIC1111_BASE_URL"
|
||||||
|
value = "http://stable-diffusion.stable-diffusion.svc.cluster.local:7860"
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "ENABLE_IMAGE_GENERATION"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "data"
|
||||||
|
mount_path = "/app/backend/data"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
memory = "256Mi"
|
||||||
|
cpu = "100m"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "1Gi"
|
||||||
|
cpu = "1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "data"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.webui_data.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "open_webui" {
|
||||||
|
metadata {
|
||||||
|
name = "open-webui"
|
||||||
|
namespace = kubernetes_namespace.webui.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
selector = {
|
||||||
|
app = "open-webui"
|
||||||
|
}
|
||||||
|
|
||||||
|
port {
|
||||||
|
port = 8080
|
||||||
|
target_port = 8080
|
||||||
|
node_port = 30080
|
||||||
|
}
|
||||||
|
|
||||||
|
type = "NodePort"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
# 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 = kubernetes_service.ollama.metadata[0].name
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
tls {
|
||||||
|
hosts = ["registry.${var.domain_suffix}"]
|
||||||
|
secret_name = "registry-tls"
|
||||||
|
}
|
||||||
|
rule {
|
||||||
|
host = "registry.${var.domain_suffix}"
|
||||||
|
http {
|
||||||
|
path {
|
||||||
|
path = "/"
|
||||||
|
path_type = "Prefix"
|
||||||
|
backend {
|
||||||
|
service {
|
||||||
|
name = kubernetes_service.registry.metadata[0].name
|
||||||
|
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 = kubernetes_service.registry_ui.metadata[0].name
|
||||||
|
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 = kubernetes_service.verdaccio.metadata[0].name
|
||||||
|
port { number = 4873 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [helm_release.cert_manager, kubernetes_deployment.verdaccio]
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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 = kubernetes_service.devpi.metadata[0].name
|
||||||
|
port { number = 3141 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [helm_release.cert_manager, kubernetes_deployment.devpi]
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
terraform {
|
||||||
|
required_version = ">= 1.5"
|
||||||
|
|
||||||
|
required_providers {
|
||||||
|
kubernetes = {
|
||||||
|
source = "hashicorp/kubernetes"
|
||||||
|
version = "~> 2.30"
|
||||||
|
}
|
||||||
|
helm = {
|
||||||
|
source = "hashicorp/helm"
|
||||||
|
version = "~> 2.14"
|
||||||
|
}
|
||||||
|
null = {
|
||||||
|
source = "hashicorp/null"
|
||||||
|
version = "~> 3.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backend "s3" {
|
||||||
|
bucket = "legion-terraform-state"
|
||||||
|
key = "legion/terraform.tfstate"
|
||||||
|
region = "auto"
|
||||||
|
endpoint = "https://651161e0a3d321561b4c90b5bcd5f15b.r2.cloudflarestorage.com"
|
||||||
|
|
||||||
|
# R2 is S3-compatible but not AWS — skip AWS-specific checks
|
||||||
|
skip_credentials_validation = true
|
||||||
|
skip_metadata_api_check = true
|
||||||
|
skip_region_validation = true
|
||||||
|
force_path_style = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "kubernetes" {
|
||||||
|
config_path = var.kubeconfig_path
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "helm" {
|
||||||
|
kubernetes {
|
||||||
|
config_path = var.kubeconfig_path
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
# Monitoring stack — Prometheus, Grafana, Loki, Tempo, Alloy, Alertmanager
|
||||||
|
# Grafana UI: https://grafana.nook.family (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
|
||||||
|
|
||||||
|
resource "kubernetes_namespace" "monitoring" {
|
||||||
|
metadata {
|
||||||
|
name = "monitoring"
|
||||||
|
labels = {
|
||||||
|
managed-by = "terraform"
|
||||||
|
tier = "observability"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
otelcol.exporter.otlp "tempo" {
|
||||||
|
client {
|
||||||
|
endpoint = "tempo.monitoring.svc.cluster.local:4317"
|
||||||
|
tls { insecure = true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOT
|
||||||
|
]
|
||||||
|
|
||||||
|
depends_on = [helm_release.loki, helm_release.tempo]
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
resource "kubernetes_namespace" "platform" {
|
||||||
|
metadata {
|
||||||
|
name = "platform"
|
||||||
|
labels = {
|
||||||
|
managed-by = "terraform"
|
||||||
|
tier = "infrastructure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_namespace" "ci" {
|
||||||
|
metadata {
|
||||||
|
name = "ci"
|
||||||
|
labels = {
|
||||||
|
managed-by = "terraform"
|
||||||
|
tier = "infrastructure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,343 @@
|
|||||||
|
# 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)
|
||||||
|
|
||||||
|
resource "kubernetes_namespace" "neuron" {
|
||||||
|
metadata {
|
||||||
|
name = "neuron"
|
||||||
|
labels = {
|
||||||
|
managed-by = "terraform"
|
||||||
|
tier = "platform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_deployment" "neuron" {
|
||||||
|
metadata {
|
||||||
|
name = "neuron"
|
||||||
|
namespace = kubernetes_namespace.neuron.metadata[0].name
|
||||||
|
labels = {
|
||||||
|
app = "neuron"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
|
||||||
|
strategy {
|
||||||
|
type = "Recreate" # SQLite cannot handle concurrent writers
|
||||||
|
}
|
||||||
|
|
||||||
|
selector {
|
||||||
|
match_labels = {
|
||||||
|
app = "neuron"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = {
|
||||||
|
app = "neuron"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
container {
|
||||||
|
name = "neuron"
|
||||||
|
image = "registry.nook.family/neural-platform/neuron:latest"
|
||||||
|
image_pull_policy = "Always"
|
||||||
|
|
||||||
|
command = [
|
||||||
|
"bash", "-c",
|
||||||
|
"mkdir -p /root/.neuron && ln -sf /data/neuron.db /root/.neuron/neuron.db && python -m synapse platform serve --mcp-host 0.0.0.0 --mcp-port 8001 --webhook-port 3847"
|
||||||
|
]
|
||||||
|
|
||||||
|
port {
|
||||||
|
name = "mcp"
|
||||||
|
container_port = 8001
|
||||||
|
}
|
||||||
|
|
||||||
|
port {
|
||||||
|
name = "webhook"
|
||||||
|
container_port = 3847
|
||||||
|
}
|
||||||
|
|
||||||
|
env_from {
|
||||||
|
config_map_ref {
|
||||||
|
name = kubernetes_config_map.neuron_config.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
env_from {
|
||||||
|
secret_ref {
|
||||||
|
name = kubernetes_secret.neuron_secrets.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "data"
|
||||||
|
mount_path = "/data"
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "app-config"
|
||||||
|
mount_path = "/app/config/default.yaml"
|
||||||
|
sub_path = "default.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
memory = "256Mi"
|
||||||
|
cpu = "100m"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "1Gi"
|
||||||
|
cpu = "500m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
liveness_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = "mcp"
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 10
|
||||||
|
period_seconds = 30
|
||||||
|
}
|
||||||
|
|
||||||
|
readiness_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/health"
|
||||||
|
port = "mcp"
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 5
|
||||||
|
period_seconds = 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "data"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.neuron_data.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "app-config"
|
||||||
|
config_map {
|
||||||
|
name = kubernetes_config_map.neuron_config.metadata[0].name
|
||||||
|
items {
|
||||||
|
key = "default.yaml"
|
||||||
|
path = "default.yaml"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [kubernetes_persistent_volume_claim.neuron_data]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "neuron" {
|
||||||
|
metadata {
|
||||||
|
name = "neuron"
|
||||||
|
namespace = kubernetes_namespace.neuron.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
selector = {
|
||||||
|
app = "neuron"
|
||||||
|
}
|
||||||
|
|
||||||
|
port {
|
||||||
|
name = "mcp"
|
||||||
|
port = 8001
|
||||||
|
target_port = 8001
|
||||||
|
}
|
||||||
|
|
||||||
|
port {
|
||||||
|
name = "webhook"
|
||||||
|
port = 3847
|
||||||
|
target_port = 3847
|
||||||
|
}
|
||||||
|
|
||||||
|
type = "ClusterIP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 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 = kubernetes_service.neuron.metadata[0].name
|
||||||
|
port {
|
||||||
|
number = 8001
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [helm_release.cert_manager]
|
||||||
|
}
|
||||||
|
|
||||||
|
# cloudflared — Cloudflare tunnel for external access to Axon webhooks
|
||||||
|
# Routes: axon.neuralplatform.ai → neuron:3847
|
||||||
|
# webhooks.neuralplatform.ai → neuron:3847
|
||||||
|
resource "kubernetes_deployment" "cloudflared" {
|
||||||
|
metadata {
|
||||||
|
name = "cloudflared"
|
||||||
|
namespace = kubernetes_namespace.neuron.metadata[0].name
|
||||||
|
labels = {
|
||||||
|
app = "cloudflared"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
|
||||||
|
selector {
|
||||||
|
match_labels = {
|
||||||
|
app = "cloudflared"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = {
|
||||||
|
app = "cloudflared"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
container {
|
||||||
|
name = "cloudflared"
|
||||||
|
image = "cloudflare/cloudflared:latest"
|
||||||
|
args = ["tunnel", "--no-autoupdate", "run"]
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "TUNNEL_TOKEN"
|
||||||
|
value = var.cloudflared_tunnel_token
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
memory = "64Mi"
|
||||||
|
cpu = "50m"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "128Mi"
|
||||||
|
cpu = "200m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
liveness_probe {
|
||||||
|
http_get {
|
||||||
|
path = "/ready"
|
||||||
|
port = 20241
|
||||||
|
}
|
||||||
|
initial_delay_seconds = 10
|
||||||
|
period_seconds = 30
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
depends_on = [kubernetes_deployment.neuron]
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Ollama — local LLM inference with GPU
|
||||||
|
resource "kubernetes_namespace" "ollama" {
|
||||||
|
metadata {
|
||||||
|
name = "ollama"
|
||||||
|
labels = {
|
||||||
|
managed-by = "terraform"
|
||||||
|
tier = "application"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_deployment" "ollama" {
|
||||||
|
metadata {
|
||||||
|
name = "ollama"
|
||||||
|
namespace = kubernetes_namespace.ollama.metadata[0].name
|
||||||
|
labels = {
|
||||||
|
app = "ollama"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
|
||||||
|
strategy {
|
||||||
|
type = "Recreate"
|
||||||
|
}
|
||||||
|
|
||||||
|
selector {
|
||||||
|
match_labels = {
|
||||||
|
app = "ollama"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = {
|
||||||
|
app = "ollama"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
container {
|
||||||
|
name = "ollama"
|
||||||
|
image = "ollama/ollama:latest"
|
||||||
|
|
||||||
|
port {
|
||||||
|
container_port = 11434
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "NVIDIA_VISIBLE_DEVICES"
|
||||||
|
value = "all"
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "models"
|
||||||
|
mount_path = "/root/.ollama"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
memory = "2Gi"
|
||||||
|
cpu = "500m"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "8Gi"
|
||||||
|
cpu = "4"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "models"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.ollama_models.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "ollama" {
|
||||||
|
metadata {
|
||||||
|
name = "ollama"
|
||||||
|
namespace = kubernetes_namespace.ollama.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
selector = {
|
||||||
|
app = "ollama"
|
||||||
|
}
|
||||||
|
|
||||||
|
port {
|
||||||
|
port = 11434
|
||||||
|
target_port = 11434
|
||||||
|
node_port = 31434
|
||||||
|
}
|
||||||
|
|
||||||
|
type = "NodePort"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,230 @@
|
|||||||
|
# Package registries — Verdaccio (npm) and devpi (PyPI)
|
||||||
|
# Both proxy to public registries and cache locally on Legion
|
||||||
|
|
||||||
|
resource "kubernetes_namespace" "packages" {
|
||||||
|
metadata {
|
||||||
|
name = "packages"
|
||||||
|
labels = {
|
||||||
|
managed-by = "terraform"
|
||||||
|
tier = "infrastructure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_deployment" "verdaccio" {
|
||||||
|
metadata {
|
||||||
|
name = "verdaccio"
|
||||||
|
namespace = kubernetes_namespace.packages.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
selector {
|
||||||
|
match_labels = { app = "verdaccio" }
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = { app = "verdaccio" }
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
security_context {
|
||||||
|
fs_group = 101
|
||||||
|
}
|
||||||
|
container {
|
||||||
|
name = "verdaccio"
|
||||||
|
image = "verdaccio/verdaccio:5"
|
||||||
|
|
||||||
|
port {
|
||||||
|
container_port = 4873
|
||||||
|
}
|
||||||
|
|
||||||
|
# Override k8s service env injection which conflicts with Verdaccio's port detection
|
||||||
|
env {
|
||||||
|
name = "VERDACCIO_PORT"
|
||||||
|
value = "4873"
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "config"
|
||||||
|
mount_path = "/verdaccio/conf/config.yaml"
|
||||||
|
sub_path = "config.yaml"
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "data"
|
||||||
|
mount_path = "/verdaccio/storage"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = { memory = "128Mi", cpu = "50m" }
|
||||||
|
limits = { memory = "512Mi" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "config"
|
||||||
|
config_map {
|
||||||
|
name = kubernetes_config_map.verdaccio_config.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "data"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.verdaccio_data.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "verdaccio" {
|
||||||
|
metadata {
|
||||||
|
name = "verdaccio"
|
||||||
|
namespace = kubernetes_namespace.packages.metadata[0].name
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
selector = { app = "verdaccio" }
|
||||||
|
port {
|
||||||
|
port = 4873
|
||||||
|
target_port = 4873
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# devpi — PyPI proxy/cache
|
||||||
|
resource "kubernetes_deployment" "devpi" {
|
||||||
|
metadata {
|
||||||
|
name = "devpi"
|
||||||
|
namespace = kubernetes_namespace.packages.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
selector {
|
||||||
|
match_labels = { app = "devpi" }
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = { app = "devpi" }
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
init_container {
|
||||||
|
name = "devpi-init"
|
||||||
|
image = "python:3.12-slim"
|
||||||
|
command = ["sh", "-c", "pip install -q devpi-server devpi-web && if [ ! -f /data/.serverversion ]; then devpi-init --serverdir /data; fi && chmod 600 /data/.secret 2>/dev/null || true"]
|
||||||
|
volume_mount {
|
||||||
|
name = "data"
|
||||||
|
mount_path = "/data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
container {
|
||||||
|
name = "devpi"
|
||||||
|
image = "python:3.12-slim"
|
||||||
|
command = ["sh", "-c", "pip install -q devpi-server devpi-web && exec devpi-server --serverdir /data --host 0.0.0.0 --port 3141 --outside-url https://pypi.neuralplatform.ai"]
|
||||||
|
|
||||||
|
port {
|
||||||
|
container_port = 3141
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "data"
|
||||||
|
mount_path = "/data"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = { memory = "512Mi", cpu = "100m" }
|
||||||
|
limits = { memory = "2Gi" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "data"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.devpi_data.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "devpi" {
|
||||||
|
metadata {
|
||||||
|
name = "devpi"
|
||||||
|
namespace = kubernetes_namespace.packages.metadata[0].name
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
selector = { app = "devpi" }
|
||||||
|
port {
|
||||||
|
port = 3141
|
||||||
|
target_port = 3141
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# 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;"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
# Local Docker Registry — push images locally, no DockerHub
|
||||||
|
resource "kubernetes_namespace" "registry" {
|
||||||
|
metadata {
|
||||||
|
name = "registry"
|
||||||
|
labels = {
|
||||||
|
managed-by = "terraform"
|
||||||
|
tier = "infrastructure"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_deployment" "registry" {
|
||||||
|
metadata {
|
||||||
|
name = "registry"
|
||||||
|
namespace = kubernetes_namespace.registry.metadata[0].name
|
||||||
|
labels = {
|
||||||
|
app = "registry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
|
||||||
|
selector {
|
||||||
|
match_labels = {
|
||||||
|
app = "registry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = {
|
||||||
|
app = "registry"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
container {
|
||||||
|
name = "registry"
|
||||||
|
image = "registry:2"
|
||||||
|
|
||||||
|
port {
|
||||||
|
container_port = 5000
|
||||||
|
}
|
||||||
|
|
||||||
|
volume_mount {
|
||||||
|
name = "data"
|
||||||
|
mount_path = "/var/lib/registry"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
memory = "64Mi"
|
||||||
|
cpu = "50m"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "256Mi"
|
||||||
|
cpu = "200m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
volume {
|
||||||
|
name = "data"
|
||||||
|
persistent_volume_claim {
|
||||||
|
claim_name = kubernetes_persistent_volume_claim.registry_data.metadata[0].name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "registry" {
|
||||||
|
metadata {
|
||||||
|
name = "registry"
|
||||||
|
namespace = kubernetes_namespace.registry.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
selector = { app = "registry" }
|
||||||
|
port {
|
||||||
|
port = 5000
|
||||||
|
target_port = 5000
|
||||||
|
}
|
||||||
|
type = "ClusterIP"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Docker Registry UI — browse images and tags visually
|
||||||
|
resource "kubernetes_deployment" "registry_ui" {
|
||||||
|
metadata {
|
||||||
|
name = "registry-ui"
|
||||||
|
namespace = kubernetes_namespace.registry.metadata[0].name
|
||||||
|
labels = { app = "registry-ui" }
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
replicas = 1
|
||||||
|
selector {
|
||||||
|
match_labels = { app = "registry-ui" }
|
||||||
|
}
|
||||||
|
template {
|
||||||
|
metadata {
|
||||||
|
labels = { app = "registry-ui" }
|
||||||
|
}
|
||||||
|
spec {
|
||||||
|
container {
|
||||||
|
name = "registry-ui"
|
||||||
|
image = "joxit/docker-registry-ui:latest"
|
||||||
|
|
||||||
|
port {
|
||||||
|
container_port = 80
|
||||||
|
}
|
||||||
|
|
||||||
|
env {
|
||||||
|
name = "REGISTRY_TITLE"
|
||||||
|
value = "Nook Registry"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "NGINX_PROXY_PASS_URL"
|
||||||
|
value = "http://registry:5000"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "SINGLE_REGISTRY"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "SHOW_CONTENT_DIGEST"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
env {
|
||||||
|
name = "DELETE_IMAGES"
|
||||||
|
value = "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
resources {
|
||||||
|
requests = {
|
||||||
|
memory = "32Mi"
|
||||||
|
cpu = "20m"
|
||||||
|
}
|
||||||
|
limits = {
|
||||||
|
memory = "64Mi"
|
||||||
|
cpu = "100m"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "kubernetes_service" "registry_ui" {
|
||||||
|
metadata {
|
||||||
|
name = "registry-ui"
|
||||||
|
namespace = kubernetes_namespace.registry.metadata[0].name
|
||||||
|
}
|
||||||
|
|
||||||
|
spec {
|
||||||
|
selector = { app = "registry-ui" }
|
||||||
|
port {
|
||||||
|
port = 80
|
||||||
|
target_port = 80
|
||||||
|
}
|
||||||
|
type = "ClusterIP"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Traefik configuration — global HTTP→HTTPS redirect
|
||||||
|
# Applied via k3s HelmChartConfig (managed outside of Helm provider)
|
||||||
|
|
||||||
|
resource "kubernetes_manifest" "traefik_config" {
|
||||||
|
field_manager {
|
||||||
|
force_conflicts = true
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
apiVersion = "helm.cattle.io/v1"
|
||||||
|
kind = "HelmChartConfig"
|
||||||
|
metadata = {
|
||||||
|
name = "traefik"
|
||||||
|
namespace = "kube-system"
|
||||||
|
}
|
||||||
|
spec = {
|
||||||
|
valuesContent = <<-YAML
|
||||||
|
ports:
|
||||||
|
web:
|
||||||
|
redirectTo:
|
||||||
|
port: websecure
|
||||||
|
YAML
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
variable "kubeconfig_path" {
|
||||||
|
description = "Path to Legion kubeconfig"
|
||||||
|
type = string
|
||||||
|
default = "~/.kube/legion-config"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "legion_ip" {
|
||||||
|
description = "Legion server LAN IP (DHCP reserved)"
|
||||||
|
type = string
|
||||||
|
default = "192.168.68.77"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "domain_suffix" {
|
||||||
|
description = "Domain suffix for nook.family family apps"
|
||||||
|
type = string
|
||||||
|
default = "nook.family"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "infra_domain" {
|
||||||
|
description = "Domain for infrastructure / platform services"
|
||||||
|
type = string
|
||||||
|
default = "neuralplatform.ai"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cloudflare_api_key" {
|
||||||
|
description = "Cloudflare global API key for cert-manager DNS-01"
|
||||||
|
type = string
|
||||||
|
sensitive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "cloudflare_email" {
|
||||||
|
description = "Cloudflare account email"
|
||||||
|
type = string
|
||||||
|
default = "andersonwilliam85@gmail.com"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "gitea_domain" {
|
||||||
|
description = "Domain for Gitea git server"
|
||||||
|
type = string
|
||||||
|
default = "git.neuralplatform.dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
resource "kubernetes_namespace" "vault" {
|
||||||
|
metadata {
|
||||||
|
name = "vault"
|
||||||
|
labels = {
|
||||||
|
managed-by = "terraform"
|
||||||
|
tier = "platform"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user