diff --git a/main.tf b/main.tf index 070f0a66..c0a08a70 100644 --- a/main.tf +++ b/main.tf @@ -153,6 +153,7 @@ variable "slack_channel" { type = string } variable "affine_postgresql_password" { type = string } variable "health_postgresql_password" { type = string } variable "health_secret_key" { type = string } +variable "moltbot_ssh_key" { type = string } variable "kube_config_path" { type = string @@ -614,6 +615,7 @@ module "kubernetes_cluster" { affine_postgresql_password = var.affine_postgresql_password health_postgresql_password = var.health_postgresql_password health_secret_key = var.health_secret_key + moltbot_ssh_key = var.moltbot_ssh_key } diff --git a/modules/kubernetes/main.tf b/modules/kubernetes/main.tf index d3f0151f..c55ad15f 100644 --- a/modules/kubernetes/main.tf +++ b/modules/kubernetes/main.tf @@ -124,6 +124,7 @@ variable "slack_channel" { type = string } variable "affine_postgresql_password" { type = string } variable "health_postgresql_password" { type = string } variable "health_secret_key" { type = string } +variable "moltbot_ssh_key" { type = string } variable "defcon_level" { @@ -149,7 +150,7 @@ locals { "url", "excalidraw", "travel_blog", "dashy", "send", "ytdlp", "wealthfolio", "rybbit", "stirling-pdf", "networking-toolbox", "navidrome", "freshrss", "forgejo", "tor-proxy", "real-estate-crawler", "n8n", "changedetection", "linkwarden", "matrix", "homepage", "meshcentral", "diun", "cyberchef", "ntfy", "ollama", - "servarr", "jsoncrack", "paperless-ngx", "frigate", "audiobookshelf", "tandoor", "ebook2audiobook", "netbox", "speedtest", "resume", "freedify", "mcaptcha", "affine", "plotting-book", "whisper", "grampsweb", "osm-routing" + "servarr", "jsoncrack", "paperless-ngx", "frigate", "audiobookshelf", "tandoor", "ebook2audiobook", "netbox", "speedtest", "resume", "freedify", "mcaptcha", "affine", "plotting-book", "whisper", "grampsweb", "osm-routing", "moltbot" ], } active_modules = distinct(flatten([ @@ -1130,3 +1131,13 @@ module "grampsweb" { depends_on = [null_resource.core_services] } + +module "moltbot" { + source = "./moltbot" + for_each = contains(local.active_modules, "moltbot") ? { moltbot = true } : {} + tls_secret_name = var.tls_secret_name + ssh_key = var.moltbot_ssh_key + tier = local.tiers.aux + + depends_on = [null_resource.core_services] +} diff --git a/modules/kubernetes/moltbot/main.tf b/modules/kubernetes/moltbot/main.tf new file mode 100644 index 00000000..9cc3e3ba --- /dev/null +++ b/modules/kubernetes/moltbot/main.tf @@ -0,0 +1,323 @@ +variable "tls_secret_name" {} +variable "tier" { type = string } +variable "ssh_key" {} + +resource "kubernetes_namespace" "moltbot" { + metadata { + name = "moltbot" + } +} + +module "tls_secret" { + source = "../setup_tls_secret" + namespace = kubernetes_namespace.moltbot.metadata[0].name + tls_secret_name = var.tls_secret_name +} + +resource "kubernetes_service_account" "moltbot" { + metadata { + name = "moltbot" + namespace = kubernetes_namespace.moltbot.metadata[0].name + } +} + +resource "kubernetes_cluster_role_binding" "moltbot" { + metadata { + name = "moltbot-cluster-admin" + } + subject { + kind = "ServiceAccount" + name = kubernetes_service_account.moltbot.metadata[0].name + namespace = kubernetes_namespace.moltbot.metadata[0].name + } + role_ref { + api_group = "rbac.authorization.k8s.io" + kind = "ClusterRole" + name = "cluster-admin" + } +} + +resource "kubernetes_secret" "ssh_key" { + metadata { + name = "ssh-key" + namespace = kubernetes_namespace.moltbot.metadata[0].name + } + data = { + "id_rsa" = var.ssh_key + } + type = "generic" +} + +resource "kubernetes_config_map" "git_crypt_key" { + metadata { + name = "git-crypt-key" + namespace = kubernetes_namespace.moltbot.metadata[0].name + } + data = { + "key" = filebase64("${path.root}/.git/git-crypt/keys/default") + } +} + +resource "kubernetes_config_map" "openclaw_config" { + metadata { + name = "openclaw-config" + namespace = kubernetes_namespace.moltbot.metadata[0].name + } + data = { + "openclaw.json" = jsonencode({ + gateway = { + bind = "lan" + controlUi = { + dangerouslyDisableDeviceAuth = true + allowedOrigins = ["https://moltbot.viktorbarzin.me"] + } + } + models = { + providers = { + ollama = { + baseUrl = "http://ollama.ollama.svc.cluster.local:11434/v1" + apiKey = "ollama-local" + api = "openai-completions" + models = [ + { id = "qwen2.5:14b", name = "Qwen 2.5 14B" }, + { id = "qwen2.5-coder:14b", name = "Qwen 2.5 Coder 14B" }, + { id = "deepseek-r1:14b", name = "DeepSeek R1 14B" }, + { id = "qwen2.5:7b", name = "Qwen 2.5 7B" }, + { id = "qwen2.5-coder:7b", name = "Qwen 2.5 Coder 7B" }, + { id = "gemma2:9b", name = "Gemma 2 9B" }, + { id = "llama3.1:latest", name = "Llama 3.1 8B" }, + ] + } + } + } + }) + } +} + +resource "random_password" "gateway_token" { + length = 32 + special = false +} + +resource "kubernetes_deployment" "moltbot" { + metadata { + name = "moltbot" + namespace = kubernetes_namespace.moltbot.metadata[0].name + labels = { + app = "moltbot" + tier = var.tier + } + } + spec { + strategy { + type = "Recreate" + } + replicas = 1 + selector { + match_labels = { + app = "moltbot" + } + } + template { + metadata { + labels = { + app = "moltbot" + } + } + spec { + service_account_name = kubernetes_service_account.moltbot.metadata[0].name + + # Init container 1: Download kubectl, terraform, git-crypt to /tools + init_container { + name = "install-tools" + image = "alpine:3.20" + command = ["sh", "-c", <<-EOF + set -e + apk add --no-cache curl unzip git-crypt + # kubectl + curl -sL "https://dl.k8s.io/release/v1.34.2/bin/linux/amd64/kubectl" -o /tools/kubectl + chmod +x /tools/kubectl + # terraform + curl -sL "https://releases.hashicorp.com/terraform/1.12.1/terraform_1.12.1_linux_amd64.zip" -o /tmp/tf.zip + unzip /tmp/tf.zip -d /tools + chmod +x /tools/terraform + # git-crypt (copy from apk install) + cp /usr/bin/git-crypt /tools/git-crypt + EOF + ] + volume_mount { + name = "tools" + mount_path = "/tools" + } + } + + # Init container 2: Clone infra repo, unlock git-crypt, run terraform init + init_container { + name = "clone-repo" + image = "alpine/git" + command = ["sh", "-c", <<-EOF + set -e + apk add --no-cache openssh-client bash git-crypt + export PATH="/tools:$PATH" + # Copy OpenClaw config to writable home dir + cp /openclaw-config-src/openclaw.json /openclaw-home/openclaw.json + # Setup SSH key + mkdir -p /root/.ssh + cp /ssh/id_rsa /root/.ssh/id_rsa + chmod 600 /root/.ssh/id_rsa + ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null + # Clone repo if not already present + if [ ! -d /workspace/infra/.git ]; then + git clone git@github.com:ViktorBarzin/infra.git /workspace/infra + else + cd /workspace/infra && git pull --ff-only || true + fi + cd /workspace/infra + # Unlock git-crypt + echo "$GIT_CRYPT_KEY" | base64 -d > /tmp/git-crypt-key + git-crypt unlock /tmp/git-crypt-key || true + rm /tmp/git-crypt-key + # Terraform init + /tools/terraform init + EOF + ] + env { + name = "GIT_CRYPT_KEY" + value_from { + config_map_key_ref { + name = kubernetes_config_map.git_crypt_key.metadata[0].name + key = "key" + } + } + } + volume_mount { + name = "tools" + mount_path = "/tools" + } + volume_mount { + name = "workspace" + mount_path = "/workspace" + } + volume_mount { + name = "ssh-key" + mount_path = "/ssh" + } + volume_mount { + name = "openclaw-home" + mount_path = "/openclaw-home" + } + volume_mount { + name = "openclaw-config" + mount_path = "/openclaw-config-src" + } + } + + # Main container: OpenClaw + container { + name = "moltbot" + image = "ghcr.io/openclaw/openclaw:latest" + command = ["node", "openclaw.mjs", "gateway", "--allow-unconfigured", "--bind", "lan"] + port { + container_port = 18789 + } + env { + name = "OPENCLAW_GATEWAY_TOKEN" + value = random_password.gateway_token.result + } + env { + name = "PATH" + value = "/tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + } + env { + name = "TF_VAR_prod" + value = "true" + } + volume_mount { + name = "tools" + mount_path = "/tools" + } + volume_mount { + name = "workspace" + mount_path = "/workspace" + } + volume_mount { + name = "data" + mount_path = "/data" + } + volume_mount { + name = "ssh-key" + mount_path = "/ssh" + } + volume_mount { + name = "openclaw-home" + mount_path = "/home/node/.openclaw" + } + } + + volume { + name = "tools" + empty_dir {} + } + volume { + name = "openclaw-home" + empty_dir {} + } + volume { + name = "workspace" + nfs { + server = "10.0.10.15" + path = "/mnt/main/moltbot/workspace" + } + } + volume { + name = "data" + nfs { + server = "10.0.10.15" + path = "/mnt/main/moltbot/data" + } + } + volume { + name = "ssh-key" + secret { + secret_name = kubernetes_secret.ssh_key.metadata[0].name + default_mode = "0600" + } + } + volume { + name = "openclaw-config" + config_map { + name = kubernetes_config_map.openclaw_config.metadata[0].name + } + } + } + } + } +} + +resource "kubernetes_service" "moltbot" { + metadata { + name = "moltbot" + namespace = kubernetes_namespace.moltbot.metadata[0].name + labels = { + app = "moltbot" + } + } + spec { + selector = { + app = "moltbot" + } + port { + port = 80 + target_port = 18789 + } + } +} + +module "ingress" { + source = "../ingress_factory" + namespace = kubernetes_namespace.moltbot.metadata[0].name + name = "moltbot" + tls_secret_name = var.tls_secret_name + port = 80 + protected = true +} diff --git a/secrets/nfs_directories.txt b/secrets/nfs_directories.txt index 5c215156..17e83a1d 100644 Binary files a/secrets/nfs_directories.txt and b/secrets/nfs_directories.txt differ