From a44dfac72132176375b3f883e712ef8752a9086c Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Fri, 13 Feb 2026 22:57:36 +0000 Subject: [PATCH] [ci skip] Deploy MoltBot (OpenClaw) AI agent gateway Add new Kubernetes service for OpenClaw gateway connected to in-cluster Ollama, with kubectl/terraform/git access for infrastructure management. Protected behind Authentik SSO. --- main.tf | 2 + modules/kubernetes/main.tf | 13 +- modules/kubernetes/moltbot/main.tf | 323 +++++++++++++++++++++++++++++ secrets/nfs_directories.txt | Bin 1603 -> 1634 bytes 4 files changed, 337 insertions(+), 1 deletion(-) create mode 100644 modules/kubernetes/moltbot/main.tf 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 5c215156f8b20cc676e4964a904dac3acef38876..17e83a1d87b70b6292db4ecbc1a42aadf06050a4 100644 GIT binary patch literal 1634 zcmV-o2A%l;M@dveQdv+`06R;7ioxT+R9(QlcN#dDG_=~dS4s#9Gaf1he zG8eCp?BY%Ok|A=Z(Crb~y_olgilhe{vZ@jio$8dipjk0vQ{6eXzcJg(t!JR(Rvuuf zz!Q3Qn`4isrI{2FT>>Hm`duV={d^Vek zh_o)T^Q2Gh_W$md1>sB#N$-Kyt6f#K^tqG}{1g9hV&)eH5l3KkCbXCx9tmL-%BE)u zD}`Q5mK7!klF<_#BGOkQIQH8Abt#AcQK1SDs6*djv~xn*&xQ3Lm1Yw<8|}doFDicXb_GJ6Ghu1GM?niqm>T+Jdgh*A z{tQW!G{o`uHLWWn=g#A)MSm{zr2`5^meLFolZjJb?=Pbdu{TUiw=eX(52(UwEX>b| z0Q~Z$^tdy$x3VgH{PIk7N4~~Dg*LA6hD+HeFXY{VZBwFg`q>sR68NrtT5z82=%m)1 zl<2s8IieRlCwp+%L@ARK+^uTnL)Z%|ENVmo@=Uz$Q{^3Dm>RrPx&L2c&YFAExT&Ls zzNr>W0@ILk5F?_b0_M^;4srr7NvqAPygM*7dc0)qw3*ZdNQ)Q*^Bv9#Q$1G&?AT|x zEpOO*e_Ejr_Ij6hTgQ&FDILDsSe|EKkApj&z=IE%AXwdES?D`(IG-MP7hYsa75_Uo zt+8l5lYuPc7w&UCNC!Fm&n@HNb&AzeEt@HINNeL*?x&-}aQgkpzU1?j*_@qRb?S`M zWlG6(`IQ1_B8`0H3ft1@-_TDqA4dl)Q9AYn2ep;vOjRJvaW$f1^eaRCoYvgwBEh+k zVD^V!@mKX$iNMoz_;-0xX2Uh(o?%PHo)35WuNEAVvqjxRm(NjB@({>|&lBvxUxi!ijUEP(=GSEbDeJP9uU{uN z-qf<3`V^&df*f*li#Y|sW(#eu#hL&0HjsY{HE7b@?_PTw^cv%COwG@M1ULzbU8ygC zvzdY7%Sb=C@)>pXvJI!9Q#pD=Y@3u6Q-toX5x8FXfN0PWKXdzmnO3+WfJrE%gx$Ej zjS0$lIRQ|M~#eCn68kP#wi_2&x&tjdgJvx$e% zL>X$1R7EE-9-`|LR1E?DEJk1;-Tsa;l*u=PY`h1^m5tm8?P*g zObzb=6ROk%U-T=JRo`sQZ4ZZv~ zH|!n8F!}(gW;sbn|9wC=*8%x{l6tLdCcVHSE7<~PPY~c#0akrbt3S8ogY&!lO>1&< zcd2@YIY~(uf3P%7M<^eMy{TZ^P(PLM3&kz42RM{5=Tz1iI_>yU@wO(E2NFjB-HTW1 zfnG3I4nhQsoU)|PE!#vr+w4cLs>o@NJGz#Q2-yV#gpxG))&D76%E1BdeKmiDNDBLJ zxx&1E7>kPFrB=5B$2cSz&9KxzL&d+Z@x<0DEAtxL39qFn-fxjuyc54=;r8N8b`uLV zd^q17G{kfLijFomODV%<;LckJY~`n9W|#jLBCYmTV@xSFFZs-lb;Hp9H4=a1Ru(pE|cPdvarB$ljVlL_ZKR;2gC%?nEW0G6Qw za!KOCpI^3|bqf(%4yGdtR8b$;x{_J8RO8!J%c|W49J42d>%g^0ZaNMq>>f^|IrQYB z_~8M^msYpdDRX)e=1*2O^^F4q{j6rfWomLYd0RR91 literal 1603 zcmV-J2E6$IM@dveQdv+`0HA!?FVaQUhVvT3FRvNGsG^2Bbe0kYu;eu;~&eY$5RN~q)fCX(HY;Cc_6Y1 zq<^JQrMf*fLp=W#F&>da!^!+UcnKezGjo{55tIAMtv z_o7(;fE(5x2R)k%VP;~qP%u9S?GOtWb{cSKNT`epETO(2+FWW)x3CgUU+0)iDs{V3 zITD_70oQZuo;)8=DCU=gN2eLQDbcUaE+pz3H)FN?0+7k=X&ktK62*?^NUwZ%n9;bt zU$`K422ts1Wg20Bna3T4v#-{LY~M@wvl&a zUJ{e3qwr@;;ey%ap<95@u~kVn_?~W#`dizf18oDS{}oy+Tl2juI02W0ssYlj!3VhS za2(bQxY~f$?HUBA+hoqKQSQ1&0`X*aRfg*AA=7-_TOtrO>i%G(`ju#R?NzA*#Big{ zi4pk?z?#;*5JtLI7o4dGNwY|fX2v$aRC4D%EkXmXB8sD!xnmWc&E@4fL|PRKQsXB5 zu>|GWduBH_oxW@-{iU)!pv9%a>7G?5(Vii{s40A^8U%DI8FIbABN8qi%~XL4?aS!66?!Gj@np`Jo8-Z#Gq5!q7~3_!Psf{i>DiMot@Na6@SN8^bBlw{sp0F zS5{&O9Yw;$HU=U{3$R_oTq>vo&u*Wq9~SC#INZb;h=@HuSN+sd{+}Fy-4^M{B(d=P zPJ>A_8A~0;76dwt z8>8sMKo<%Wcg)CwEJM<6-l`7kIfE8v-kIaKd%zH|d{O6a`92Pp8@Xi;{lJ z1RRODMzF%#hSYJVOI9VL(fiDm<|brwCL`O~Rhwx!1BRS78eLZ{+bTo!#5 zr^m-knEuF7OGu6*m{e}@HaVVYqd|LOoL$OlUXKpCs;#G;lqNQYT{5!CsEme*!6`DW zrDYIQZd6rPtY+Y{7pUR-h8Lj3H_p&c{_+;@9U^~S#SohyKSFR3lD&UN zW5E6G;hm0#%ry+F;9~_JXbksns>Q8+qjW_#~*#50xoc(8`%uALo%ZYm&)ljW;q z>2=;aVrRp}jaT~W<+4>>xG^uMRs@;4zvbpP8B(zM*J-21#~N?X)?e4` zrHBjMGM!$li|%9uVQ7SKwtTN=*k&r;b6JJa4`aMkd_sQlZVvbr33m@!xWOLvxrPEV z$j}$qgA$h2CM!KQW@HNd53rUb4lN~bTtLj7qf(LZ6Pepzx`13B$=oPI7b6Q1daaU~ B7Lot}