Initial commit — migrate legion Terraform from infrastructure repo

This commit is contained in:
Will Anderson
2026-03-23 07:35:03 -05:00
commit 7751eddd73
23 changed files with 2903 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
# Terraform
**/.terraform/
**/*.tfstate
**/*.tfstate.backup
**/tfplan
**/.terraform.lock.hcl
# Secrets
**/*.json
**/*.env
**/.envrc
# OS
.DS_Store
+16
View File
@@ -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
```
+7
View File
@@ -0,0 +1,7 @@
.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl
terraform.tfvars
.envrc
vault-gcp-sa.json
+271
View File
@@ -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",
]
}
}
+137
View File
@@ -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 {}
}
}
}
}
}
}
}
+63
View File
@@ -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
View File
@@ -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]
}
+94
View File
@@ -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"
}
}
}
}
}
}
+81
View File
@@ -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"
}
}
+124
View File
@@ -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"
}
}
+180
View File
@@ -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]
}
+41
View File
@@ -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
}
}
+288
View File
@@ -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]
}
+19
View File
@@ -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"
}
}
}
+343
View File
@@ -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]
}
+118
View File
@@ -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"
}
}
+230
View File
@@ -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
}
}
}
+34
View File
@@ -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;"
}
}
+34
View File
@@ -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"
}
}
+183
View File
@@ -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"
}
}
+25
View File
@@ -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
}
}
}
+131
View File
@@ -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
View File
@@ -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,
]
}