From e85c0365cd56d5f604593319c350b4cd3a8929a8 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sun, 25 Jan 2026 21:40:39 +0000 Subject: [PATCH] Add AFFiNE visual canvas for storytelling - Deploy AFFiNE as self-hosted visual canvas tool - Uses shared PostgreSQL and Redis from cluster - NFS storage for uploads and configuration - Email configured via mailserver.viktorbarzin.me - Ingress at affine.viktorbarzin.me [ci skip] --- main.tf | 10 ++ modules/kubernetes/affine/main.tf | 217 ++++++++++++++++++++++++++++++ modules/kubernetes/main.tf | 28 +++- 3 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 modules/kubernetes/affine/main.tf diff --git a/main.tf b/main.tf index 68c3170d..63ebf1d8 100644 --- a/main.tf +++ b/main.tf @@ -139,6 +139,10 @@ variable "freedify_credentials" { type = map(any) } variable "mcaptcha_postgresql_password" { type = string } variable "mcaptcha_cookie_secret" { type = string } variable "mcaptcha_captcha_salt" { type = string } +variable "openrouter_api_key" { type = string } +variable "slack_bot_token" { type = string } +variable "slack_channel" { type = string } +variable "affine_postgresql_password" { type = string } provider "kubernetes" { config_path = var.prod ? "" : "~/.kube/config" @@ -570,6 +574,12 @@ module "kubernetes_cluster" { mcaptcha_postgresql_password = var.mcaptcha_postgresql_password mcaptcha_cookie_secret = var.mcaptcha_cookie_secret mcaptcha_captcha_salt = var.mcaptcha_captcha_salt + + openrouter_api_key = var.openrouter_api_key + slack_bot_token = var.slack_bot_token + slack_channel = var.slack_channel + + affine_postgresql_password = var.affine_postgresql_password } diff --git a/modules/kubernetes/affine/main.tf b/modules/kubernetes/affine/main.tf new file mode 100644 index 00000000..b8e8b49c --- /dev/null +++ b/modules/kubernetes/affine/main.tf @@ -0,0 +1,217 @@ +variable "tls_secret_name" {} +variable "tier" { type = string } +variable "postgresql_password" {} +variable "smtp_password" { type = string } + +resource "kubernetes_namespace" "affine" { + metadata { + name = "affine" + } +} + +module "tls_secret" { + source = "../setup_tls_secret" + namespace = kubernetes_namespace.affine.metadata[0].name + tls_secret_name = var.tls_secret_name +} + +locals { + common_env = [ + { + name = "DATABASE_URL" + value = "postgresql://affine:${var.postgresql_password}@postgresql.dbaas.svc.cluster.local:5432/affine" + }, + { + name = "REDIS_SERVER_HOST" + value = "redis.redis.svc.cluster.local" + }, + { + name = "AFFINE_INDEXER_ENABLED" + value = "false" + }, + { + name = "NODE_OPTIONS" + value = "--max-old-space-size=4096" + }, + # Server URL configuration + { + name = "AFFINE_SERVER_EXTERNAL_URL" + value = "https://affine.viktorbarzin.me" + }, + { + name = "AFFINE_SERVER_HTTPS" + value = "true" + }, + # Email/SMTP configuration + { + name = "MAILER_HOST" + value = "mailserver.viktorbarzin.me" + }, + { + name = "MAILER_PORT" + value = "587" + }, + { + name = "MAILER_USER" + value = "info@viktorbarzin.me" + }, + { + name = "MAILER_PASSWORD" + value = var.smtp_password + }, + { + name = "MAILER_SENDER" + value = "AFFiNE " + }, + ] +} + +resource "kubernetes_deployment" "affine" { + metadata { + name = "affine" + namespace = kubernetes_namespace.affine.metadata[0].name + labels = { + app = "affine" + tier = var.tier + } + } + spec { + replicas = 1 + selector { + match_labels = { + app = "affine" + } + } + template { + metadata { + labels = { + app = "affine" + } + } + spec { + # Init container to run database migrations + init_container { + name = "migration" + image = "ghcr.io/toeverything/affine:stable" + command = ["sh", "-c", "node ./scripts/self-host-predeploy.js"] + + dynamic "env" { + for_each = local.common_env + content { + name = env.value.name + value = env.value.value + } + } + + volume_mount { + name = "data" + mount_path = "/root/.affine/storage" + sub_path = "storage" + } + volume_mount { + name = "data" + mount_path = "/root/.affine/config" + sub_path = "config" + } + } + + container { + name = "affine" + image = "ghcr.io/toeverything/affine:stable" + + port { + container_port = 3010 + } + + dynamic "env" { + for_each = local.common_env + content { + name = env.value.name + value = env.value.value + } + } + + volume_mount { + name = "data" + mount_path = "/root/.affine/storage" + sub_path = "storage" + } + volume_mount { + name = "data" + mount_path = "/root/.affine/config" + sub_path = "config" + } + + resources { + requests = { + memory = "512Mi" + cpu = "100m" + } + limits = { + memory = "4Gi" + cpu = "2" + } + } + + liveness_probe { + http_get { + path = "/info" + port = 3010 + } + initial_delay_seconds = 120 + period_seconds = 30 + timeout_seconds = 10 + } + readiness_probe { + http_get { + path = "/info" + port = 3010 + } + initial_delay_seconds = 60 + period_seconds = 10 + timeout_seconds = 5 + } + } + volume { + name = "data" + nfs { + server = "10.0.10.15" + path = "/mnt/main/affine" + } + } + } + } + } +} + +resource "kubernetes_service" "affine" { + metadata { + name = "affine" + namespace = kubernetes_namespace.affine.metadata[0].name + labels = { + app = "affine" + } + } + + spec { + selector = { + app = "affine" + } + port { + name = "http" + port = 80 + target_port = 3010 + } + } +} + +module "ingress" { + source = "../ingress_factory" + namespace = kubernetes_namespace.affine.metadata[0].name + name = "affine" + tls_secret_name = var.tls_secret_name + max_body_size = "500m" + extra_annotations = { + "nginx.ingress.kubernetes.io/proxy-body-size" : "500m" + } +} diff --git a/modules/kubernetes/main.tf b/modules/kubernetes/main.tf index 67f989a3..737aba30 100644 --- a/modules/kubernetes/main.tf +++ b/modules/kubernetes/main.tf @@ -118,6 +118,10 @@ variable "freedify_credentials" { type = map(any) } variable "mcaptcha_postgresql_password" { type = string } variable "mcaptcha_cookie_secret" { type = string } variable "mcaptcha_captcha_salt" { type = string } +variable "openrouter_api_key" { type = string } +variable "slack_bot_token" { type = string } +variable "slack_channel" { type = string } +variable "affine_postgresql_password" { type = string } variable "defcon_level" { @@ -143,7 +147,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" + "servarr", "jsoncrack", "paperless-ngx", "frigate", "audiobookshelf", "tandoor", "ebook2audiobook", "netbox", "speedtest", "resume", "freedify", "mcaptcha", "affine" ], } active_modules = distinct(flatten([ @@ -539,10 +543,13 @@ module "redis" { } module "ytdlp" { - source = "./youtube_dl" - for_each = contains(local.active_modules, "ytdlp") ? { ytdlp = true } : {} - tls_secret_name = var.tls_secret_name - tier = local.tiers.aux + source = "./youtube_dl" + for_each = contains(local.active_modules, "ytdlp") ? { ytdlp = true } : {} + tls_secret_name = var.tls_secret_name + tier = local.tiers.aux + openrouter_api_key = var.openrouter_api_key + slack_bot_token = var.slack_bot_token + slack_channel = var.slack_channel depends_on = [null_resource.core_services] } @@ -1062,3 +1069,14 @@ module "freedify" { for_each = contains(local.active_modules, "freedify") ? { freedify = true } : {} additional_credentials = var.freedify_credentials } + +module "affine" { + source = "./affine" + for_each = contains(local.active_modules, "affine") ? { affine = true } : {} + tls_secret_name = var.tls_secret_name + postgresql_password = var.affine_postgresql_password + smtp_password = var.mailserver_accounts["info@viktorbarzin.me"] + tier = local.tiers.aux + + depends_on = [null_resource.core_services] +}