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]
This commit is contained in:
Viktor Barzin 2026-01-25 21:40:39 +00:00
parent 5ded70c1ab
commit e85c0365cd
3 changed files with 250 additions and 5 deletions

10
main.tf
View file

@ -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
}

View file

@ -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 <info@viktorbarzin.me>"
},
]
}
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"
}
}

View file

@ -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]
}