From 6e1d8c0c8bcb850b3d756f72df67cdccdf370bd0 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Wed, 25 Mar 2026 15:04:27 +0200 Subject: [PATCH] add ebooks stack: consolidate book services into single namespace [ci skip] - New ebooks namespace with CWA, Stacks, Audiobookshelf, book-search - book-search (renamed from audiobook-search) with CWA ingest volume - Comment out audiobook_search module from servarr - All NFS volumes and secrets consolidated --- stacks/ebooks/main.tf | 791 +++++++++++++++++++++++++++++++++++ stacks/ebooks/terragrunt.hcl | 9 + stacks/ebooks/tiers.tf | 10 + stacks/servarr/main.tf | 19 +- 4 files changed, 820 insertions(+), 9 deletions(-) create mode 100644 stacks/ebooks/main.tf create mode 100644 stacks/ebooks/terragrunt.hcl create mode 100644 stacks/ebooks/tiers.tf diff --git a/stacks/ebooks/main.tf b/stacks/ebooks/main.tf new file mode 100644 index 00000000..d402c0b9 --- /dev/null +++ b/stacks/ebooks/main.tf @@ -0,0 +1,791 @@ +variable "tls_secret_name" { + type = string + sensitive = true +} +variable "nfs_server" { type = string } + +resource "kubernetes_namespace" "ebooks" { + metadata { + name = "ebooks" + labels = { + tier = local.tiers.edge + } + } +} + +# ExternalSecrets for all three sources +resource "kubernetes_manifest" "calibre_external_secret" { + manifest = { + apiVersion = "external-secrets.io/v1beta1" + kind = "ExternalSecret" + metadata = { + name = "calibre-secrets" + namespace = "ebooks" + } + spec = { + refreshInterval = "15m" + secretStoreRef = { + name = "vault-kv" + kind = "ClusterSecretStore" + } + target = { + name = "calibre-secrets" + } + dataFrom = [{ + extract = { + key = "calibre" + } + }] + } + } + depends_on = [kubernetes_namespace.ebooks] +} + +resource "kubernetes_manifest" "audiobookshelf_external_secret" { + manifest = { + apiVersion = "external-secrets.io/v1beta1" + kind = "ExternalSecret" + metadata = { + name = "audiobookshelf-secrets" + namespace = "ebooks" + } + spec = { + refreshInterval = "15m" + secretStoreRef = { + name = "vault-kv" + kind = "ClusterSecretStore" + } + target = { + name = "audiobookshelf-secrets" + } + dataFrom = [{ + extract = { + key = "audiobookshelf" + } + }] + } + } + depends_on = [kubernetes_namespace.ebooks] +} + +resource "kubernetes_manifest" "servarr_external_secret" { + manifest = { + apiVersion = "external-secrets.io/v1beta1" + kind = "ExternalSecret" + metadata = { + name = "servarr-secrets" + namespace = "ebooks" + } + spec = { + refreshInterval = "15m" + secretStoreRef = { + name = "vault-kv" + kind = "ClusterSecretStore" + } + target = { + name = "servarr-secrets" + } + dataFrom = [{ + extract = { + key = "servarr" + } + }] + } + } + depends_on = [kubernetes_namespace.ebooks] +} + +# Data sources to read ExternalSecret-created secrets +data "kubernetes_secret" "calibre_secrets" { + metadata { + name = "calibre-secrets" + namespace = kubernetes_namespace.ebooks.metadata[0].name + } + depends_on = [kubernetes_manifest.calibre_external_secret] +} + +data "kubernetes_secret" "audiobookshelf_secrets" { + metadata { + name = "audiobookshelf-secrets" + namespace = kubernetes_namespace.ebooks.metadata[0].name + } + depends_on = [kubernetes_manifest.audiobookshelf_external_secret] +} + +data "kubernetes_secret" "servarr_secrets" { + metadata { + name = "servarr-secrets" + namespace = kubernetes_namespace.ebooks.metadata[0].name + } + depends_on = [kubernetes_manifest.servarr_external_secret] +} + +locals { + calibre_homepage_credentials = jsondecode(data.kubernetes_secret.calibre_secrets.data["homepage_credentials"]) + audiobookshelf_homepage_credentials = jsondecode(data.kubernetes_secret.audiobookshelf_secrets.data["homepage_credentials"]) +} + +module "tls_secret" { + source = "../../modules/kubernetes/setup_tls_secret" + namespace = kubernetes_namespace.ebooks.metadata[0].name + tls_secret_name = var.tls_secret_name +} + +# NFS Volumes - Calibre +module "nfs_calibre_library" { + source = "../../modules/kubernetes/nfs_volume" + name = "calibre-library" + namespace = kubernetes_namespace.ebooks.metadata[0].name + nfs_server = var.nfs_server + nfs_path = "/mnt/main/calibre-web-automated/calibre-library" +} + +module "nfs_calibre_config" { + source = "../../modules/kubernetes/nfs_volume" + name = "calibre-config" + namespace = kubernetes_namespace.ebooks.metadata[0].name + nfs_server = var.nfs_server + nfs_path = "/mnt/main/calibre-web-automated/config" +} + +module "nfs_calibre_ingest" { + source = "../../modules/kubernetes/nfs_volume" + name = "calibre-ingest" + namespace = kubernetes_namespace.ebooks.metadata[0].name + nfs_server = var.nfs_server + nfs_path = "/mnt/main/calibre-web-automated/cwa-book-ingest" +} + +module "nfs_calibre_stacks_config" { + source = "../../modules/kubernetes/nfs_volume" + name = "calibre-stacks-config" + namespace = kubernetes_namespace.ebooks.metadata[0].name + nfs_server = var.nfs_server + nfs_path = "/mnt/main/calibre-web-automated/stacks" +} + +# NFS Volumes - Audiobookshelf +module "nfs_audiobookshelf_audiobooks" { + source = "../../modules/kubernetes/nfs_volume" + name = "audiobookshelf-audiobooks" + namespace = kubernetes_namespace.ebooks.metadata[0].name + nfs_server = var.nfs_server + nfs_path = "/mnt/main/audiobookshelf/audiobooks" +} + +module "nfs_audiobookshelf_podcasts" { + source = "../../modules/kubernetes/nfs_volume" + name = "audiobookshelf-podcasts" + namespace = kubernetes_namespace.ebooks.metadata[0].name + nfs_server = var.nfs_server + nfs_path = "/mnt/main/audiobookshelf/podcasts" +} + +module "nfs_audiobookshelf_config" { + source = "../../modules/kubernetes/nfs_volume" + name = "audiobookshelf-config" + namespace = kubernetes_namespace.ebooks.metadata[0].name + nfs_server = var.nfs_server + nfs_path = "/mnt/main/audiobookshelf/config" +} + +module "nfs_audiobookshelf_metadata" { + source = "../../modules/kubernetes/nfs_volume" + name = "audiobookshelf-metadata" + namespace = kubernetes_namespace.ebooks.metadata[0].name + nfs_server = var.nfs_server + nfs_path = "/mnt/main/audiobookshelf/metadata" +} + +# Calibre-Web-Automated Deployment +resource "kubernetes_deployment" "calibre-web-automated" { + wait_for_rollout = true + metadata { + name = "calibre-web-automated" + namespace = kubernetes_namespace.ebooks.metadata[0].name + labels = { + app = "calibre-web-automated" + tier = local.tiers.edge + } + annotations = { + "reloader.stakater.com/search" = "true" + } + } + spec { + replicas = 1 + strategy { + type = "Recreate" + } + selector { + match_labels = { + app = "calibre-web-automated" + } + } + template { + metadata { + annotations = { + "diun.enable" = "false" + "diun.include_tags" = "^\\d+(?:\\.\\d+)?(?:\\.\\d+)?$" + } + labels = { + app = "calibre-web-automated" + } + } + spec { + container { + image = "viktorbarzin/calibre-web-automated:latest" + name = "calibre-web-automated" + env { + name = "PUID" + value = 1000 + } + env { + name = "PGID" + value = 1000 + } + env { + name = "NO_CHOWN" + value = "true" + } + env { + name = "NETWORK_SHARE_MODE" + value = "true" + } + env { + name = "CALIBRE_PORT" + value = "8083" + } + + port { + container_port = 8083 + } + startup_probe { + http_get { + path = "/" + port = 8083 + } + initial_delay_seconds = 10 + timeout_seconds = 5 + period_seconds = 5 + failure_threshold = 24 + } + liveness_probe { + http_get { + path = "/" + port = 8083 + } + timeout_seconds = 10 + period_seconds = 30 + failure_threshold = 6 + } + resources { + requests = { + cpu = "50m" + memory = "512Mi" + } + limits = { + memory = "1Gi" + } + } + volume_mount { + name = "config" + mount_path = "/config" + } + volume_mount { + name = "library" + mount_path = "/calibre-library" + } + volume_mount { + name = "ingest" + mount_path = "/cwa-book-ingest" + } + } + volume { + name = "library" + persistent_volume_claim { + claim_name = module.nfs_calibre_library.claim_name + } + } + volume { + name = "config" + persistent_volume_claim { + claim_name = module.nfs_calibre_config.claim_name + } + } + volume { + name = "ingest" + persistent_volume_claim { + claim_name = module.nfs_calibre_ingest.claim_name + } + } + } + } + } + lifecycle { + ignore_changes = [spec[0].template[0].spec[0].dns_config] + } +} + +resource "kubernetes_service" "calibre" { + metadata { + name = "calibre" + namespace = kubernetes_namespace.ebooks.metadata[0].name + labels = { + "app" = "calibre" + } + } + + spec { + selector = { + app = "calibre-web-automated" + } + port { + name = "http" + target_port = 8083 + port = 80 + protocol = "TCP" + } + } +} + +module "calibre_ingress" { + source = "../../modules/kubernetes/ingress_factory" + namespace = kubernetes_namespace.ebooks.metadata[0].name + name = "calibre" + tls_secret_name = var.tls_secret_name + extra_annotations = { + "gethomepage.dev/enabled" = "true" + "gethomepage.dev/description" = "Book library" + "gethomepage.dev/group" = "Media & Entertainment" + "gethomepage.dev/icon" = "calibre-web.png" + "gethomepage.dev/name" = "Calibre" + "gethomepage.dev/widget.type" = "calibreweb" + "gethomepage.dev/widget.url" = "http://calibre.ebooks.svc.cluster.local" + "gethomepage.dev/widget.username" = local.calibre_homepage_credentials["calibre-web"]["username"] + "gethomepage.dev/widget.password" = local.calibre_homepage_credentials["calibre-web"]["password"] + "gethomepage.dev/pod-selector" = "" + } + rybbit_site_id = "17a5c7fbb077" + custom_content_security_policy = "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://rybbit.viktorbarzin.me" +} + +# Stacks - Anna's Archive Download Manager +resource "kubernetes_deployment" "annas-archive-stacks" { + metadata { + name = "annas-archive-stacks" + namespace = kubernetes_namespace.ebooks.metadata[0].name + labels = { + app = "annas-archive-stacks" + tier = local.tiers.edge + } + } + spec { + replicas = 1 + selector { + match_labels = { + app = "annas-archive-stacks" + } + } + template { + metadata { + labels = { + app = "annas-archive-stacks" + } + } + spec { + container { + image = "zelest/stacks:latest" + name = "annas-archive-stacks" + resources { + requests = { + cpu = "10m" + memory = "384Mi" + } + limits = { + memory = "384Mi" + } + } + port { + container_port = 7788 + } + volume_mount { + name = "config" + mount_path = "/opt/stacks/config" + } + volume_mount { + name = "ingest" + mount_path = "/opt/stacks/download" + } + } + volume { + name = "config" + persistent_volume_claim { + claim_name = module.nfs_calibre_stacks_config.claim_name + } + } + volume { + name = "ingest" + persistent_volume_claim { + claim_name = module.nfs_calibre_ingest.claim_name + } + } + } + } + } + lifecycle { + ignore_changes = [spec[0].template[0].spec[0].dns_config] + } +} + +resource "kubernetes_service" "annas-archive-stacks" { + metadata { + name = "annas-archive-stacks" + namespace = kubernetes_namespace.ebooks.metadata[0].name + labels = { + "app" = "annas-archive-stacks" + } + } + + spec { + selector = { + app = "annas-archive-stacks" + } + port { + name = "http" + port = "80" + target_port = 7788 + } + } +} + +module "stacks_ingress" { + source = "../../modules/kubernetes/ingress_factory" + namespace = kubernetes_namespace.ebooks.metadata[0].name + name = "stacks" + service_name = "annas-archive-stacks" + tls_secret_name = var.tls_secret_name + protected = true + rybbit_site_id = "ce5f8aed6bbb" + extra_annotations = { + "gethomepage.dev/enabled" = "false" + } +} + +# Audiobookshelf Deployment +resource "kubernetes_deployment" "audiobookshelf" { + metadata { + name = "audiobookshelf" + namespace = kubernetes_namespace.ebooks.metadata[0].name + labels = { + app = "audiobookshelf" + tier = local.tiers.edge + } + annotations = { + "reloader.stakater.com/search" = "true" + } + } + spec { + replicas = 1 + strategy { + type = "Recreate" + } + selector { + match_labels = { + app = "audiobookshelf" + } + } + template { + metadata { + labels = { + app = "audiobookshelf" + } + } + spec { + container { + image = "ghcr.io/advplyr/audiobookshelf:2.32.1" + name = "audiobookshelf" + + port { + container_port = 80 + } + liveness_probe { + http_get { + path = "/healthcheck" + port = 80 + } + initial_delay_seconds = 15 + period_seconds = 30 + timeout_seconds = 5 + failure_threshold = 5 + } + readiness_probe { + http_get { + path = "/healthcheck" + port = 80 + } + initial_delay_seconds = 5 + period_seconds = 30 + timeout_seconds = 5 + failure_threshold = 3 + } + volume_mount { + name = "audiobooks" + mount_path = "/audiobooks" + } + volume_mount { + name = "podcasts" + mount_path = "/podcasts" + } + volume_mount { + name = "config" + mount_path = "/config" + } + volume_mount { + name = "metadata" + mount_path = "/metadata" + } + resources { + requests = { + cpu = "15m" + memory = "64Mi" + } + limits = { + memory = "256Mi" + } + } + } + volume { + name = "audiobooks" + persistent_volume_claim { + claim_name = module.nfs_audiobookshelf_audiobooks.claim_name + } + } + volume { + name = "podcasts" + persistent_volume_claim { + claim_name = module.nfs_audiobookshelf_podcasts.claim_name + } + } + volume { + name = "config" + persistent_volume_claim { + claim_name = module.nfs_audiobookshelf_config.claim_name + } + } + volume { + name = "metadata" + persistent_volume_claim { + claim_name = module.nfs_audiobookshelf_metadata.claim_name + } + } + } + } + } + lifecycle { + ignore_changes = [spec[0].template[0].spec[0].dns_config] + } +} + +resource "kubernetes_service" "audiobookshelf" { + metadata { + name = "audiobookshelf" + namespace = kubernetes_namespace.ebooks.metadata[0].name + labels = { + "app" = "audiobookshelf" + } + } + + spec { + selector = { + app = "audiobookshelf" + } + port { + name = "http" + target_port = 80 + port = 80 + protocol = "TCP" + } + } +} + +module "audiobookshelf_ingress" { + source = "../../modules/kubernetes/ingress_factory" + namespace = kubernetes_namespace.ebooks.metadata[0].name + name = "audiobookshelf" + tls_secret_name = var.tls_secret_name + rybbit_site_id = "b38fda4285df" + extra_annotations = { + "gethomepage.dev/enabled" = "true" + "gethomepage.dev/name" = "Audiobookshelf" + "gethomepage.dev/description" = "Audiobook library" + "gethomepage.dev/icon" = "audiobookshelf.png" + "gethomepage.dev/group" = "Media & Entertainment" + "gethomepage.dev/pod-selector" = "" + "gethomepage.dev/widget.type" = "audiobookshelf" + "gethomepage.dev/widget.url" = "http://audiobookshelf.ebooks.svc.cluster.local" + "gethomepage.dev/widget.key" = local.audiobookshelf_homepage_credentials["audiobookshelf"]["token"] + } +} + +# Book-Search Deployment (renamed from audiobook-search) +resource "kubernetes_deployment" "book_search" { + metadata { + name = "book-search" + namespace = kubernetes_namespace.ebooks.metadata[0].name + labels = { + app = "book-search" + tier = local.tiers.edge + } + } + spec { + replicas = 1 + selector { + match_labels = { + app = "book-search" + } + } + template { + metadata { + labels = { + app = "book-search" + } + } + spec { + container { + image = "viktorbarzin/book-search:latest" + image_pull_policy = "Always" + name = "book-search" + + port { + container_port = 8000 + } + env { + name = "QBITTORRENT_URL" + value = "http://qbittorrent.servarr.svc.cluster.local" + } + env { + name = "QBITTORRENT_PASS" + value_from { + secret_key_ref { + name = "servarr-secrets" + key = "qbittorrent_password" + } + } + } + env { + name = "AUDIOBOOKSHELF_URL" + value = "http://audiobookshelf.ebooks.svc.cluster.local" + } + env { + name = "AUDIOBOOKSHELF_TOKEN" + value_from { + secret_key_ref { + name = "servarr-secrets" + key = "audiobookshelf_api_token" + } + } + } + env { + name = "MAM_EMAIL" + value_from { + secret_key_ref { + name = "servarr-secrets" + key = "mam_email" + } + } + } + env { + name = "MAM_PASSWORD" + value_from { + secret_key_ref { + name = "servarr-secrets" + key = "mam_password" + } + } + } + env { + name = "CWA_INGEST_PATH" + value = "/cwa-book-ingest" + } + resources { + requests = { + cpu = "10m" + memory = "64Mi" + } + limits = { + memory = "128Mi" + } + } + liveness_probe { + http_get { + path = "/health" + port = 8000 + } + initial_delay_seconds = 10 + period_seconds = 30 + } + volume_mount { + name = "cwa-ingest" + mount_path = "/cwa-book-ingest" + } + volume_mount { + name = "audiobooks" + mount_path = "/audiobooks" + } + } + volume { + name = "cwa-ingest" + persistent_volume_claim { + claim_name = module.nfs_calibre_ingest.claim_name + } + } + volume { + name = "audiobooks" + persistent_volume_claim { + claim_name = module.nfs_audiobookshelf_audiobooks.claim_name + } + } + } + } + } + lifecycle { + ignore_changes = [spec[0].template[0].spec[0].dns_config] + } +} + +resource "kubernetes_service" "book_search" { + metadata { + name = "book-search" + namespace = kubernetes_namespace.ebooks.metadata[0].name + labels = { + app = "book-search" + } + } + + spec { + selector = { + app = "book-search" + } + port { + name = "http" + port = 80 + target_port = 8000 + } + } +} + +module "book_search_ingress" { + source = "../../modules/kubernetes/ingress_factory" + namespace = kubernetes_namespace.ebooks.metadata[0].name + name = "book-search" + tls_secret_name = var.tls_secret_name + protected = true + extra_annotations = { + "gethomepage.dev/enabled" = "true" + "gethomepage.dev/name" = "Book Search" + "gethomepage.dev/description" = "Search & download books" + "gethomepage.dev/icon" = "audiobookshelf.png" + "gethomepage.dev/group" = "Media & Entertainment" + "gethomepage.dev/pod-selector" = "" + } +} diff --git a/stacks/ebooks/terragrunt.hcl b/stacks/ebooks/terragrunt.hcl new file mode 100644 index 00000000..7aa411a2 --- /dev/null +++ b/stacks/ebooks/terragrunt.hcl @@ -0,0 +1,9 @@ +include "root" { + path = find_in_parent_folders() +} + +dependency "platform" { + config_path = "../platform" + skip_outputs = true +} + diff --git a/stacks/ebooks/tiers.tf b/stacks/ebooks/tiers.tf new file mode 100644 index 00000000..eb0f8083 --- /dev/null +++ b/stacks/ebooks/tiers.tf @@ -0,0 +1,10 @@ +# Generated by Terragrunt. Sig: nIlQXj57tbuaRZEa +locals { + tiers = { + core = "0-core" + cluster = "1-cluster" + gpu = "2-gpu" + edge = "3-edge" + aux = "4-aux" + } +} diff --git a/stacks/servarr/main.tf b/stacks/servarr/main.tf index 5061d96a..393dc121 100644 --- a/stacks/servarr/main.tf +++ b/stacks/servarr/main.tf @@ -115,12 +115,13 @@ module "aiostreams" { nfs_server = var.nfs_server } -module "audiobook_search" { - source = "./audiobook-search" - tls_secret_name = var.tls_secret_name - tier = local.tiers.aux - audiobookshelf_token = data.kubernetes_secret.eso_secrets.data["audiobookshelf_api_token"] - qbittorrent_password = data.kubernetes_secret.eso_secrets.data["qbittorrent_password"] - mam_email = data.kubernetes_secret.eso_secrets.data["mam_email"] - mam_password = data.kubernetes_secret.eso_secrets.data["mam_password"] -} +# Moved to stacks/ebooks/ namespace +# module "audiobook_search" { +# source = "./audiobook-search" +# tls_secret_name = var.tls_secret_name +# tier = local.tiers.aux +# audiobookshelf_token = data.kubernetes_secret.eso_secrets.data["audiobookshelf_api_token"] +# qbittorrent_password = data.kubernetes_secret.eso_secrets.data["qbittorrent_password"] +# mam_email = data.kubernetes_secret.eso_secrets.data["mam_email"] +# mam_password = data.kubernetes_secret.eso_secrets.data["mam_password"] +# }