From b1d152be1fedd098e93cf0afb62bb2fcb7703d36 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Thu, 16 Apr 2026 13:45:04 +0000 Subject: [PATCH] [infra] Auto-create Cloudflare DNS records from ingress_factory MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context Deploying new services required manually adding hostnames to cloudflare_proxied_names/cloudflare_non_proxied_names in config.tfvars — a separate file from the service stack. This was frequently forgotten, leaving services unreachable externally. ## This change: - Add `dns_type` parameter to `ingress_factory` and `reverse_proxy/factory` modules. Setting `dns_type = "proxied"` or `"non-proxied"` auto-creates the Cloudflare DNS record (CNAME to tunnel or A/AAAA to public IP). - Simplify cloudflared tunnel from 100 per-hostname rules to wildcard `*.viktorbarzin.me → Traefik`. Traefik still handles host-based routing. - Add global Cloudflare provider via terragrunt.hcl (separate cloudflare_provider.tf with Vault-sourced API key). - Migrate 118 hostnames from centralized config.tfvars to per-service dns_type. 17 hostnames remain centrally managed (Helm ingresses, special cases). - Update docs, AGENTS.md, CLAUDE.md, dns.md runbook. ``` BEFORE AFTER config.tfvars (manual list) stacks//main.tf | module "ingress" { v dns_type = "proxied" stacks/cloudflared/ } for_each = list | cloudflare_record auto-creates tunnel per-hostname cloudflare_record + annotation ``` ## What is NOT in this change: - Uptime Kuma monitor migration (still reads from config.tfvars) - 17 remaining centrally-managed hostnames (Helm, special cases) - Removal of allow_overwrite (keep until migration confirmed stable) Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/CLAUDE.md | 4 +- AGENTS.md | 2 +- config.tfvars | Bin 10293 -> 9101 bytes docs/architecture/dns.md | 30 ++++-- modules/kubernetes/ingress_factory/main.tf | 84 ++++++++++++++- stacks/_template/main.tf.example | 3 +- stacks/actualbudget/factory/main.tf | 1 + stacks/affine/main.tf | 1 + stacks/authentik/modules/authentik/main.tf | 1 + stacks/beads-server/main.tf | 17 ++- stacks/blog/main.tf | 1 + stacks/changedetection/main.tf | 1 + stacks/city-guesser/main.tf | 1 + stacks/claude-memory/main.tf | 1 + .../modules/cloudflared/cloudflare.tf | 23 ++-- stacks/crowdsec/modules/crowdsec/main.tf | 1 + stacks/cyberchef/main.tf | 1 + stacks/dashy/main.tf | 1 + stacks/dawarich/main.tf | 1 + stacks/dbaas/modules/dbaas/main.tf | 98 +++++++++++++++++- stacks/ebook2audiobook/main.tf | 2 + stacks/ebooks/main.tf | 4 + stacks/echo/main.tf | 1 + stacks/excalidraw/main.tf | 1 + stacks/f1-stream/main.tf | 1 + stacks/foolery/main.tf | 1 + stacks/forgejo/main.tf | 1 + stacks/freedify/factory/main.tf | 1 + stacks/freshrss/main.tf | 1 + stacks/frigate/main.tf | 1 + stacks/hackmd/main.tf | 1 + stacks/headscale/modules/headscale/main.tf | 1 + stacks/health/main.tf | 1 + stacks/homepage/main.tf | 1 + stacks/immich/frame.tf | 1 + stacks/immich/main.tf | 1 + stacks/insta2spotify/main.tf | 1 + stacks/jsoncrack/main.tf | 1 + stacks/k8s-dashboard/main.tf | 1 + stacks/k8s-portal/modules/k8s-portal/main.tf | 1 + stacks/kms/main.tf | 1 + stacks/linkwarden/main.tf | 1 + .../modules/mailserver/roundcubemail.tf | 1 + stacks/matrix/main.tf | 1 + stacks/meshcentral/main.tf | 1 + stacks/n8n/main.tf | 1 + stacks/navidrome/main.tf | 1 + stacks/netbox/main.tf | 1 + stacks/networking-toolbox/main.tf | 1 + stacks/nextcloud/main.tf | 1 + stacks/novelapp/main.tf | 1 + stacks/ntfy/main.tf | 1 + stacks/ollama/main.tf | 2 + stacks/onlyoffice/main.tf | 1 + stacks/openclaw/main.tf | 2 + stacks/owntracks/main.tf | 1 + stacks/paperless-ngx/main.tf | 1 + stacks/phpipam/main.tf | 1 + stacks/plotting-book/main.tf | 1 + stacks/poison-fountain/main.tf | 1 + stacks/priority-pass/main.tf | 1 + stacks/privatebin/main.tf | 1 + stacks/real-estate-crawler/main.tf | 2 + stacks/resume/main.tf | 1 + .../modules/reverse_proxy/factory/main.tf | 75 +++++++++++++- .../modules/reverse_proxy/main.tf | 16 +++ stacks/rybbit/main.tf | 2 + stacks/send/main.tf | 1 + stacks/servarr/aiostreams/main.tf | 1 + stacks/servarr/flaresolverr/main.tf | 1 + stacks/servarr/lidarr/main.tf | 2 + stacks/servarr/listenarr/main.tf | 1 + stacks/servarr/prowlarr/main.tf | 1 + stacks/servarr/qbittorrent/main.tf | 1 + stacks/servarr/soulseek/main.tf | 1 + stacks/speedtest/main.tf | 1 + stacks/stirling-pdf/main.tf | 1 + stacks/tandoor/main.tf | 1 + stacks/technitium/modules/technitium/main.tf | 33 ++++-- stacks/terminal/main.tf | 2 + stacks/trading-bot/main.tf | 1 + stacks/traefik/modules/traefik/main.tf | 1 + stacks/tuya-bridge/main.tf | 1 + .../uptime-kuma/modules/uptime-kuma/main.tf | 1 + stacks/url/main.tf | 2 + stacks/vault/main.tf | 1 + .../vaultwarden/modules/vaultwarden/main.tf | 1 + stacks/vpa/modules/vpa/main.tf | 1 + stacks/wealthfolio/main.tf | 1 + stacks/webhook_handler/main.tf | 1 + stacks/woodpecker/main.tf | 1 + stacks/xray/modules/xray/main.tf | 3 + stacks/ytdlp/main.tf | 2 + terragrunt.hcl | 25 ++++- 94 files changed, 471 insertions(+), 34 deletions(-) diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 0ad70344..bd965873 100755 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -28,7 +28,7 @@ Violations cause state drift, which causes future applies to break or silently r - **Apply**: Authenticate via `vault login -method=oidc`, then use `scripts/tg` (preferred — handles state decrypt/encrypt) or `terragrunt` directly. `scripts/tg` adds `-auto-approve` for `--non-interactive` applies. - **New services need CI/CD** and **monitoring** (Prometheus/Uptime Kuma) - **New service**: Use `setup-project` skill for full workflow -- **Ingress**: `ingress_factory` module. Auth: `protected = true`. Anti-AI: on by default. +- **Ingress**: `ingress_factory` module. Auth: `protected = true`. Anti-AI: on by default. **DNS**: `dns_type = "proxied"` (Cloudflare CDN) or `"non-proxied"` (direct A/AAAA). DNS records are auto-created — no need to edit `config.tfvars`. - **Docker images**: Always build for `linux/amd64`. Use 8-char git SHA tags — `:latest` causes stale pull-through cache. - **Private registry**: `registry.viktorbarzin.me` (htpasswd auth, credentials in Vault `secret/viktor`). Use `image: registry.viktorbarzin.me/:` + `imagePullSecrets: [{name: registry-credentials}]`. Kyverno auto-syncs the secret to all namespaces. Build & push from registry VM (`10.0.20.10`). Containerd `hosts.toml` redirects pulls to LAN IP directly. Web UI at `docker.viktorbarzin.me` (Authentik-protected). - **LinuxServer.io containers**: `DOCKER_MODS` runs apt-get on every start — bake slow mods into a custom image (`RUN /docker-mods || true` then `ENV DOCKER_MODS=`). Set `NO_CHOWN=true` to skip recursive chown that hangs on NFS mounts. @@ -133,7 +133,7 @@ Repo IDs: infra=1, Website=2, finance=3, health=4, travel_blog=5, webhook-handle - Alert cascade inhibitions: if node is down, suppress pod alerts on that node. - Exclude completed CronJob pods from "pod not ready" alerts. - Every new service gets Prometheus scrape config + Uptime Kuma monitor. External monitors auto-created for Cloudflare-proxied services by `external-monitor-sync` CronJob (10min, uptime-kuma ns). -- **External monitoring**: `[External] ` monitors in Uptime Kuma test full external path (DNS → Cloudflare → Tunnel → Traefik). Divergence metric `external_internal_divergence_count` → alert `ExternalAccessDivergence` (15min). Config: `stacks/uptime-kuma/`, targets from `cloudflare_proxied_names` in `config.tfvars`. +- **External monitoring**: `[External] ` monitors in Uptime Kuma test full external path (DNS → Cloudflare → Tunnel → Traefik). Divergence metric `external_internal_divergence_count` → alert `ExternalAccessDivergence` (15min). Config: `stacks/uptime-kuma/`, targets from `cloudflare_proxied_names` in `config.tfvars` (17 remaining centrally-managed hostnames; most DNS records now auto-created by `ingress_factory` `dns_type` param). - Key alerts: OOMKill, pod replica mismatch, 4xx/5xx error rates, UPS battery, CPU temp, SSD writes, NFS responsiveness, ClusterMemoryRequestsHigh (>85%), ContainerNearOOM (>85% limit), PodUnschedulable, ExternalAccessDivergence. - **E2E email monitoring**: CronJob `email-roundtrip-monitor` (every 20 min) sends test email via Mailgun API to `smoke-test@viktorbarzin.me` (catch-all → `spam@`), verifies IMAP delivery, deletes test email, pushes metrics to Pushgateway + Uptime Kuma. Alerts: `EmailRoundtripFailing` (60m), `EmailRoundtripStale` (60m), `EmailRoundtripNeverRun` (60m). Outbound relay: Brevo EU (`smtp-relay.brevo.com:587`, 300/day free — migrated from Mailgun). Mailserver on dedicated MetalLB IP `10.0.20.202` with `externalTrafficPolicy: Local` for CrowdSec real-IP detection. Vault: `mailgun_api_key` in `secret/viktor` (probe), `brevo_api_key` in `secret/viktor` (relay). diff --git a/AGENTS.md b/AGENTS.md index 1a8c79d2..8ce10dc1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,7 +51,7 @@ Terragrunt-based homelab managing a Kubernetes cluster (5 nodes, v1.34.2) on Pro ## Key Paths - `stacks//main.tf` — service definition - `stacks/platform/modules//` — core infra modules -- `modules/kubernetes/ingress_factory/` — standardized ingress with auth, rate limiting, anti-AI +- `modules/kubernetes/ingress_factory/` — standardized ingress with auth, rate limiting, anti-AI, and auto Cloudflare DNS (`dns_type = "proxied"` or `"non-proxied"`) - `modules/kubernetes/nfs_volume/` — NFS volume module (CSI-backed, soft mount) - `config.tfvars` — non-secret configuration (plaintext) - `secrets.sops.json` — all secrets (SOPS-encrypted JSON) diff --git a/config.tfvars b/config.tfvars index 37fe950261f227238b677ab66d618c14dbca9b89..11f75005738e9c1ba4c3316182959dd73ee40efe 100644 GIT binary patch literal 9101 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 LQAVlZWYCM{q!!yB literal 10293 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/main.tf + module "ingress" { + source = ingress_factory + dns_type = "proxied" # ← auto-creates Cloudflare DNS record + } +``` + +- **`dns_type = "proxied"`**: Creates CNAME → `{tunnel_id}.cfargotunnel.com` (Cloudflare CDN) +- **`dns_type = "non-proxied"`**: Creates A → public IP + AAAA → IPv6 +- **`dns_type = "none"`** (default): No DNS record + +The Cloudflare tunnel uses a **wildcard rule** (`*.viktorbarzin.me → Traefik`) — no per-hostname tunnel config needed. Traefik handles host-based routing via K8s Ingress resources. ### Record Types | Type | Records | Target | Example | |------|---------|--------|---------| -| Proxied CNAME | ~30 domains | `{tunnel_id}.cfargotunnel.com` | blog, hackmd, homepage, ntfy | -| Non-proxied A | ~20 domains | `176.12.22.76` (public IP) | mail, headscale, immich, vaultwarden | -| Non-proxied AAAA | ~20 domains | IPv6 (HE tunnel) | Same as non-proxied A | +| Proxied CNAME | ~100 domains | `{tunnel_id}.cfargotunnel.com` | blog, hackmd, homepage, ntfy | +| Non-proxied A | ~35 domains | `176.12.22.76` (public IP) | mail, headscale, immich | +| Non-proxied AAAA | ~35 domains | IPv6 (HE tunnel) | Same as non-proxied A | | MX | 1 | `mail.viktorbarzin.me` | Inbound email | | TXT (SPF) | 1 | `v=spf1 include:mailgun.org -all` | Email authentication | | TXT (DKIM) | 4 | RSA keys (s1, mail, brevo1, brevo2) | Email signing | @@ -393,9 +409,9 @@ For internal `.viktorbarzin.lan` records: 3. Or add directly in Technitium web UI (`technitium.viktorbarzin.me`) For external `.viktorbarzin.me` records: -1. Add to `cloudflare_proxied_names` or `cloudflare_non_proxied_names` in `config.tfvars` -2. Run `scripts/tg apply -target=module.kubernetes_cluster.module.cloudflared` -3. For non-standard records (MX, TXT), add a `cloudflare_record` resource in `cloudflare.tf` +1. Add `dns_type = "proxied"` (or `"non-proxied"`) to the `ingress_factory` module call in the service stack +2. Run `scripts/tg apply` on the service stack — DNS record is auto-created +3. For non-standard records (MX, TXT), add a `cloudflare_record` resource in `stacks/cloudflared/modules/cloudflared/cloudflare.tf` ## Incident History diff --git a/modules/kubernetes/ingress_factory/main.tf b/modules/kubernetes/ingress_factory/main.tf index 5184a998..347608de 100644 --- a/modules/kubernetes/ingress_factory/main.tf +++ b/modules/kubernetes/ingress_factory/main.tf @@ -1,3 +1,14 @@ +terraform { + required_providers { + cloudflare = { + source = "cloudflare/cloudflare" + version = "~> 4" + } + kubernetes = { + source = "hashicorp/kubernetes" + } + } +} variable "name" { type = string } variable "service_name" { @@ -76,6 +87,38 @@ variable "anti_ai_scraping" { default = null # null = auto (enabled when not protected, disabled when protected) } +variable "dns_type" { + type = string + default = "none" + description = "Cloudflare DNS: 'proxied' (CNAME to tunnel), 'non-proxied' (A/AAAA to public IP), or 'none'" + validation { + condition = contains(["proxied", "non-proxied", "none"], var.dns_type) + error_message = "dns_type must be 'proxied', 'non-proxied', or 'none'." + } +} + +# Cloudflare config defaults — override via variables if these change. +# Source of truth: config.tfvars (cloudflare_zone_id, cloudflare_tunnel_id, public_ip, public_ipv6) +variable "cloudflare_zone_id" { + type = string + default = "fd2c5dd4efe8fe38958944e74d0ced6d" +} + +variable "cloudflare_tunnel_id" { + type = string + default = "75182cd7-bb91-4310-b961-5d8967da8b41" +} + +variable "public_ip" { + type = string + default = "176.12.22.76" +} + +variable "public_ipv6" { + type = string + default = "2001:470:6e:43d::2" +} + variable "homepage_group" { type = string default = null # auto-detect from namespace @@ -122,6 +165,8 @@ locals { lookup(local.ns_to_group, var.namespace, "Other") ) + dns_name = local.effective_host == var.root_domain ? "@" : replace(local.effective_host, ".${var.root_domain}", "") + homepage_defaults = var.homepage_enabled ? { "gethomepage.dev/enabled" = "true" "gethomepage.dev/name" = replace(replace(var.name, "-", " "), "_", " ") @@ -177,7 +222,9 @@ resource "kubernetes_ingress_v1" "proxied-ingress" { var.custom_content_security_policy != null ? "${var.namespace}-custom-csp-${var.name}@kubernetescrd" : null, ], var.extra_middlewares))) "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" - }, local.homepage_defaults, var.extra_annotations) + }, local.homepage_defaults, var.extra_annotations, + var.dns_type != "none" ? { "cloudflare.viktorbarzin.me/dns-type" = var.dns_type } : {} + ) } spec { @@ -255,3 +302,38 @@ resource "kubernetes_manifest" "custom_csp" { } } } + +# Cloudflare DNS records — created automatically when dns_type is set. +# Proxied: CNAME to Cloudflare tunnel. Non-proxied: A + AAAA to public IP. +resource "cloudflare_record" "proxied" { + count = var.dns_type == "proxied" ? 1 : 0 + name = local.dns_name + content = "${var.cloudflare_tunnel_id}.cfargotunnel.com" + proxied = true + ttl = 1 + type = "CNAME" + zone_id = var.cloudflare_zone_id + allow_overwrite = true +} + +resource "cloudflare_record" "non_proxied_a" { + count = var.dns_type == "non-proxied" ? 1 : 0 + name = local.dns_name + content = var.public_ip + proxied = false + ttl = 1 + type = "A" + zone_id = var.cloudflare_zone_id + allow_overwrite = true +} + +resource "cloudflare_record" "non_proxied_aaaa" { + count = var.dns_type == "non-proxied" ? 1 : 0 + name = local.dns_name + content = var.public_ipv6 + proxied = false + ttl = 1 + type = "AAAA" + zone_id = var.cloudflare_zone_id + allow_overwrite = true +} diff --git a/stacks/_template/main.tf.example b/stacks/_template/main.tf.example index 4623d0ee..c668292e 100644 --- a/stacks/_template/main.tf.example +++ b/stacks/_template/main.tf.example @@ -86,5 +86,6 @@ module "ingress" { namespace = "" name = "" tls_secret_name = var.tls_secret_name - protected = false # Set true to require Authentik login + dns_type = "proxied" # "proxied" (Cloudflare CDN), "non-proxied" (direct A/AAAA), or "none" + protected = false # Set true to require Authentik login } diff --git a/stacks/actualbudget/factory/main.tf b/stacks/actualbudget/factory/main.tf index 1d061984..a28a1c82 100644 --- a/stacks/actualbudget/factory/main.tf +++ b/stacks/actualbudget/factory/main.tf @@ -137,6 +137,7 @@ module "ingress" { namespace = "actualbudget" name = "budget-${var.name}" tls_secret_name = var.tls_secret_name + dns_type = "proxied" rybbit_site_id = "3e6b6b68088a" extra_annotations = var.homepage_annotations } diff --git a/stacks/affine/main.tf b/stacks/affine/main.tf index bf74087c..dcfe31ed 100644 --- a/stacks/affine/main.tf +++ b/stacks/affine/main.tf @@ -344,6 +344,7 @@ resource "kubernetes_service" "affine" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.affine.metadata[0].name name = "affine" tls_secret_name = var.tls_secret_name diff --git a/stacks/authentik/modules/authentik/main.tf b/stacks/authentik/modules/authentik/main.tf index 225daf7a..c053239e 100644 --- a/stacks/authentik/modules/authentik/main.tf +++ b/stacks/authentik/modules/authentik/main.tf @@ -67,6 +67,7 @@ resource "helm_release" "authentik" { module "ingress" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.authentik.metadata[0].name name = "authentik" service_name = "goauthentik-server" diff --git a/stacks/beads-server/main.tf b/stacks/beads-server/main.tf index 300c68cd..b2787bee 100644 --- a/stacks/beads-server/main.tf +++ b/stacks/beads-server/main.tf @@ -228,7 +228,7 @@ resource "kubernetes_deployment" "workbench" { for f in /static/chunks/pages/_app-*.js; do sed -i 's|http://localhost:9002/graphql|/graphql|g' "$f" done - echo "Patched GraphQL URL to /graphql" + echo "Patched GraphQL URL and store path" EOT ] volume_mount { @@ -249,6 +249,13 @@ resource "kubernetes_deployment" "workbench" { container { name = "workbench" image = "dolthub/dolt-workbench:latest" + command = ["sh", "-c", <<-EOT + # Patch GraphQL server to listen on 0.0.0.0 (IPv4) — Node 18+ defaults to IPv6 + sed -i 's|app.listen(9002)|app.listen(9002,"0.0.0.0")|g' /app/graphql-server/dist/main.js + # Start PM2 (the default entrypoint) + exec pm2-runtime /app/process.yml + EOT + ] port { name = "http" @@ -259,9 +266,14 @@ resource "kubernetes_deployment" "workbench" { container_port = 9002 } + env { + name = "NODE_OPTIONS" + value = "--dns-result-order=ipv4first" + } + volume_mount { name = "store" - mount_path = "/app/store" + mount_path = "/app/graphql-server/store" } volume_mount { name = "static-patched" @@ -361,6 +373,7 @@ module "tls_secret" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.beads.metadata[0].name name = "dolt-workbench" tls_secret_name = var.tls_secret_name diff --git a/stacks/blog/main.tf b/stacks/blog/main.tf index ac9a88d7..df55cd5c 100644 --- a/stacks/blog/main.tf +++ b/stacks/blog/main.tf @@ -110,6 +110,7 @@ module "ingress" { name = "blog" service_name = "blog" full_host = "viktorbarzin.me" + dns_type = "proxied" tls_secret_name = var.tls_secret_name rybbit_site_id = "da853a2438d0" extra_annotations = { diff --git a/stacks/changedetection/main.tf b/stacks/changedetection/main.tf index 328b5b39..d2113f4d 100644 --- a/stacks/changedetection/main.tf +++ b/stacks/changedetection/main.tf @@ -206,6 +206,7 @@ resource "kubernetes_service" "changedetection" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.changedetection.metadata[0].name name = "changedetection" tls_secret_name = var.tls_secret_name diff --git a/stacks/city-guesser/main.tf b/stacks/city-guesser/main.tf index 92e15e23..748871f3 100644 --- a/stacks/city-guesser/main.tf +++ b/stacks/city-guesser/main.tf @@ -87,6 +87,7 @@ resource "kubernetes_service" "city-guesser" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = "city-guesser" name = "city-guesser" tls_secret_name = var.tls_secret_name diff --git a/stacks/claude-memory/main.tf b/stacks/claude-memory/main.tf index 492943ea..e0f0078e 100644 --- a/stacks/claude-memory/main.tf +++ b/stacks/claude-memory/main.tf @@ -267,6 +267,7 @@ resource "kubernetes_service" "claude-memory" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.claude-memory.metadata[0].name name = "claude-memory" tls_secret_name = var.tls_secret_name diff --git a/stacks/cloudflared/modules/cloudflared/cloudflare.tf b/stacks/cloudflared/modules/cloudflared/cloudflare.tf index f7d25120..88d1c77d 100644 --- a/stacks/cloudflared/modules/cloudflared/cloudflare.tf +++ b/stacks/cloudflared/modules/cloudflared/cloudflare.tf @@ -58,15 +58,20 @@ resource "cloudflare_zero_trust_tunnel_cloudflared_config" "sof" { warp_routing { enabled = true } - dynamic "ingress_rule" { - for_each = toset(var.cloudflare_proxied_names) - content { - hostname = ingress_rule.value == "viktorbarzin.me" ? ingress_rule.value : "${ingress_rule.value}.viktorbarzin.me" - path = "/" - service = "https://10.0.20.200:443" - origin_request { - no_tls_verify = true - } + # Wildcard rule routes all subdomains through tunnel to Traefik. + # Traefik handles host-based routing via K8s Ingress resources. + ingress_rule { + hostname = "*.viktorbarzin.me" + service = "https://10.0.20.200:443" + origin_request { + no_tls_verify = true + } + } + ingress_rule { + hostname = "viktorbarzin.me" + service = "https://10.0.20.200:443" + origin_request { + no_tls_verify = true } } ingress_rule { diff --git a/stacks/crowdsec/modules/crowdsec/main.tf b/stacks/crowdsec/modules/crowdsec/main.tf index 6211871c..c0068124 100644 --- a/stacks/crowdsec/modules/crowdsec/main.tf +++ b/stacks/crowdsec/modules/crowdsec/main.tf @@ -256,6 +256,7 @@ resource "kubernetes_service" "crowdsec-web" { } module "ingress" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.crowdsec.metadata[0].name name = "crowdsec-web" protected = true diff --git a/stacks/cyberchef/main.tf b/stacks/cyberchef/main.tf index 4222ce93..221072aa 100644 --- a/stacks/cyberchef/main.tf +++ b/stacks/cyberchef/main.tf @@ -98,6 +98,7 @@ resource "kubernetes_service" "cyberchef" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.cyberchef.metadata[0].name name = "cc" tls_secret_name = var.tls_secret_name diff --git a/stacks/dashy/main.tf b/stacks/dashy/main.tf index 77295cfc..3b489f0a 100644 --- a/stacks/dashy/main.tf +++ b/stacks/dashy/main.tf @@ -120,6 +120,7 @@ resource "kubernetes_service" "dashy" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.dashy.metadata[0].name name = "dashy" tls_secret_name = var.tls_secret_name diff --git a/stacks/dawarich/main.tf b/stacks/dawarich/main.tf index 05f8ebe7..6033619d 100644 --- a/stacks/dawarich/main.tf +++ b/stacks/dawarich/main.tf @@ -364,6 +364,7 @@ resource "kubernetes_service" "dawarich" { # } module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.dawarich.metadata[0].name name = "dawarich" tls_secret_name = var.tls_secret_name diff --git a/stacks/dbaas/modules/dbaas/main.tf b/stacks/dbaas/modules/dbaas/main.tf index 3bac4d21..1d5cdcce 100644 --- a/stacks/dbaas/modules/dbaas/main.tf +++ b/stacks/dbaas/modules/dbaas/main.tf @@ -366,9 +366,99 @@ resource "helm_release" "mysql_cluster" { depends_on = [helm_release.mysql_operator] } -# Compatibility service: mysql.dbaas points at InnoDB Cluster mysqld pods -# When router is available it handles failover, but we fall back to direct -# mysqld access to avoid total outage during partial cluster failures +#### MYSQL — Standalone Bitnami (migration target) +# +# Standalone MySQL without Group Replication. Eliminates ~95 GB/day of GR +# write overhead (binlog, relay log, XCom cache) for databases totaling ~35 MB. +# Binary logging disabled entirely (skip-log-bin) since no replication needed. + +resource "helm_release" "mysql_standalone" { + namespace = kubernetes_namespace.dbaas.metadata[0].name + create_namespace = false + name = "mysql-standalone" + timeout = 600 + + repository = "oci://registry-1.docker.io/bitnamicharts" + chart = "mysql" + + values = [yamlencode({ + architecture = "standalone" + image = { + tag = "8.4" + } + + auth = { + rootPassword = var.dbaas_root_password + } + + primary = { + configuration = <<-EOT + [mysqld] + skip-name-resolve + mysql-native-password=ON + skip-log-bin + max_connections=80 + innodb_log_buffer_size=16777216 + innodb_flush_log_at_trx_commit=2 + innodb_io_capacity=100 + innodb_io_capacity_max=200 + innodb_redo_log_capacity=1073741824 + innodb_buffer_pool_size=1073741824 + innodb_flush_neighbors=1 + innodb_lru_scan_depth=256 + innodb_page_cleaners=1 + innodb_adaptive_flushing_lwm=10 + innodb_max_dirty_pages_pct=90 + innodb_max_dirty_pages_pct_lwm=10 + EOT + + persistence = { + enabled = true + storageClass = "proxmox-lvm-encrypted" + size = "5Gi" + annotations = { + "resize.topolvm.io/threshold" = "80%" + "resize.topolvm.io/increase" = "100%" + "resize.topolvm.io/storage_limit" = "30Gi" + } + } + + resources = { + requests = { + cpu = "250m" + memory = "1536Mi" + } + limits = { + memory = "2Gi" + } + } + + affinity = { + nodeAffinity = { + requiredDuringSchedulingIgnoredDuringExecution = { + nodeSelectorTerms = [{ + matchExpressions = [{ + key = "kubernetes.io/hostname" + operator = "NotIn" + values = ["k8s-node1"] + }] + }] + } + } + } + } + + metrics = { + enabled = false + } + })] +} + +# Compatibility service: mysql.dbaas points at InnoDB Cluster mysqld pods. +# Phase 3 cutover: switch selector to Bitnami standalone after dump/restore: +# "app.kubernetes.io/instance" = "mysql-standalone" +# "app.kubernetes.io/component" = "primary" +# and remove publish_not_ready_addresses + update depends_on. resource "kubernetes_service" "mysql" { metadata { name = var.cluster_master_service @@ -833,6 +923,7 @@ resource "kubernetes_service" "phpmyadmin" { } module "ingress" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.dbaas.metadata[0].name name = "pma" tls_secret_name = var.tls_secret_name @@ -1287,6 +1378,7 @@ resource "kubernetes_service" "pgadmin" { } module "ingress-pgadmin" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.dbaas.metadata[0].name name = "pgadmin" tls_secret_name = var.tls_secret_name diff --git a/stacks/ebook2audiobook/main.tf b/stacks/ebook2audiobook/main.tf index 8392922c..e6f43005 100644 --- a/stacks/ebook2audiobook/main.tf +++ b/stacks/ebook2audiobook/main.tf @@ -242,6 +242,7 @@ resource "kubernetes_service" "ebook2audiobook" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.ebook2audiobook.metadata[0].name name = "ebook2audiobook" tls_secret_name = var.tls_secret_name @@ -426,6 +427,7 @@ module "audiblez-web-ingress" { namespace = kubernetes_namespace.ebook2audiobook.metadata[0].name name = "audiblez-web" host = "audiblez" + dns_type = "non-proxied" tls_secret_name = var.tls_secret_name protected = true max_body_size = "500m" # Allow large EPUB uploads diff --git a/stacks/ebooks/main.tf b/stacks/ebooks/main.tf index ef14ec1e..01804056 100644 --- a/stacks/ebooks/main.tf +++ b/stacks/ebooks/main.tf @@ -374,6 +374,7 @@ resource "kubernetes_service" "calibre" { module "calibre_ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.ebooks.metadata[0].name name = "calibre" tls_secret_name = var.tls_secret_name @@ -494,6 +495,7 @@ resource "kubernetes_service" "annas-archive-stacks" { module "stacks_ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.ebooks.metadata[0].name name = "stacks" service_name = "annas-archive-stacks" @@ -644,6 +646,7 @@ resource "kubernetes_service" "audiobookshelf" { module "audiobookshelf_ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.ebooks.metadata[0].name name = "audiobookshelf" tls_secret_name = var.tls_secret_name @@ -904,6 +907,7 @@ resource "kubernetes_service" "book_search" { module "book_search_ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.ebooks.metadata[0].name name = "book-search" tls_secret_name = var.tls_secret_name diff --git a/stacks/echo/main.tf b/stacks/echo/main.tf index b1203cb3..bf4de105 100644 --- a/stacks/echo/main.tf +++ b/stacks/echo/main.tf @@ -94,6 +94,7 @@ resource "kubernetes_service" "echo" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.echo.metadata[0].name name = "echo" tls_secret_name = var.tls_secret_name diff --git a/stacks/excalidraw/main.tf b/stacks/excalidraw/main.tf index 2c8dd976..f6d0c19b 100644 --- a/stacks/excalidraw/main.tf +++ b/stacks/excalidraw/main.tf @@ -137,6 +137,7 @@ resource "kubernetes_service" "draw" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.excalidraw.metadata[0].name name = "draw" tls_secret_name = var.tls_secret_name diff --git a/stacks/f1-stream/main.tf b/stacks/f1-stream/main.tf index 2f2ac829..004b59a3 100644 --- a/stacks/f1-stream/main.tf +++ b/stacks/f1-stream/main.tf @@ -169,6 +169,7 @@ module "tls_secret" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.f1-stream.metadata[0].name name = "f1" tls_secret_name = var.tls_secret_name diff --git a/stacks/foolery/main.tf b/stacks/foolery/main.tf index d964de91..fd2e519a 100644 --- a/stacks/foolery/main.tf +++ b/stacks/foolery/main.tf @@ -57,6 +57,7 @@ resource "kubernetes_endpoints" "foolery" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.foolery.metadata[0].name name = "foolery" tls_secret_name = var.tls_secret_name diff --git a/stacks/forgejo/main.tf b/stacks/forgejo/main.tf index d17e4dfe..1ae871a4 100644 --- a/stacks/forgejo/main.tf +++ b/stacks/forgejo/main.tf @@ -153,6 +153,7 @@ resource "kubernetes_service" "forgejo" { } module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.forgejo.metadata[0].name name = "forgejo" tls_secret_name = var.tls_secret_name diff --git a/stacks/freedify/factory/main.tf b/stacks/freedify/factory/main.tf index fdaff5db..f6798d00 100755 --- a/stacks/freedify/factory/main.tf +++ b/stacks/freedify/factory/main.tf @@ -232,6 +232,7 @@ module "ingress" { namespace = "freedify" name = "music-${var.name}" tls_secret_name = var.tls_secret_name + dns_type = "non-proxied" protected = var.protected extra_annotations = var.extra_annotations } diff --git a/stacks/freshrss/main.tf b/stacks/freshrss/main.tf index 89de107c..2a839baf 100644 --- a/stacks/freshrss/main.tf +++ b/stacks/freshrss/main.tf @@ -207,6 +207,7 @@ resource "kubernetes_service" "freshrss" { } module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = "freshrss" name = "rss" service_name = "freshrss" diff --git a/stacks/frigate/main.tf b/stacks/frigate/main.tf index 9f8c036d..2f0f1330 100644 --- a/stacks/frigate/main.tf +++ b/stacks/frigate/main.tf @@ -276,6 +276,7 @@ resource "kubernetes_service" "frigate-rtsp" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.frigate.metadata[0].name name = "frigate" tls_secret_name = var.tls_secret_name diff --git a/stacks/hackmd/main.tf b/stacks/hackmd/main.tf index 79c5cfab..a6e2ffc7 100644 --- a/stacks/hackmd/main.tf +++ b/stacks/hackmd/main.tf @@ -183,6 +183,7 @@ resource "kubernetes_service" "hackmd" { } module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.hackmd.metadata[0].name name = "hackmd" tls_secret_name = var.tls_secret_name diff --git a/stacks/headscale/modules/headscale/main.tf b/stacks/headscale/modules/headscale/main.tf index 5fb0c280..ab78f7b9 100644 --- a/stacks/headscale/modules/headscale/main.tf +++ b/stacks/headscale/modules/headscale/main.tf @@ -291,6 +291,7 @@ resource "kubernetes_service" "headscale" { module "ingress" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.headscale.metadata[0].name name = "headscale" port = 8080 diff --git a/stacks/health/main.tf b/stacks/health/main.tf index 5d1c5b05..0b1c7758 100644 --- a/stacks/health/main.tf +++ b/stacks/health/main.tf @@ -166,6 +166,7 @@ resource "kubernetes_service" "health" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.health.metadata[0].name name = "health" tls_secret_name = var.tls_secret_name diff --git a/stacks/homepage/main.tf b/stacks/homepage/main.tf index d3e1fcda..3cd4d633 100644 --- a/stacks/homepage/main.tf +++ b/stacks/homepage/main.tf @@ -134,6 +134,7 @@ module "ingress" { namespace = kubernetes_namespace.homepage.metadata[0].name name = "homepage" host = "home" + dns_type = "proxied" service_name = kubernetes_service.cache_proxy.metadata[0].name tls_secret_name = var.tls_secret_name extra_annotations = { diff --git a/stacks/immich/frame.tf b/stacks/immich/frame.tf index 878a63bc..3e7e22aa 100644 --- a/stacks/immich/frame.tf +++ b/stacks/immich/frame.tf @@ -120,6 +120,7 @@ resource "kubernetes_service" "immich-frame" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = "immich" name = "highlights-immich" tls_secret_name = var.tls_secret_name diff --git a/stacks/immich/main.tf b/stacks/immich/main.tf index 89a3e823..d141edab 100644 --- a/stacks/immich/main.tf +++ b/stacks/immich/main.tf @@ -674,6 +674,7 @@ resource "kubernetes_service" "immich-machine-learning" { module "ingress-immich" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.immich.metadata[0].name name = "immich" service_name = "immich-server" diff --git a/stacks/insta2spotify/main.tf b/stacks/insta2spotify/main.tf index e5143fc2..9dd86586 100644 --- a/stacks/insta2spotify/main.tf +++ b/stacks/insta2spotify/main.tf @@ -228,6 +228,7 @@ resource "kubernetes_service" "insta2spotify" { # Main ingress — protected by Authentik (frontend) module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.insta2spotify.metadata[0].name name = "insta2spotify" tls_secret_name = var.tls_secret_name diff --git a/stacks/jsoncrack/main.tf b/stacks/jsoncrack/main.tf index b1c8c031..c0b2bc5c 100644 --- a/stacks/jsoncrack/main.tf +++ b/stacks/jsoncrack/main.tf @@ -78,6 +78,7 @@ resource "kubernetes_service" "jsoncrack" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.jsoncrack.metadata[0].name name = "json" tls_secret_name = var.tls_secret_name diff --git a/stacks/k8s-dashboard/main.tf b/stacks/k8s-dashboard/main.tf index 6e8d9acd..954a47e5 100644 --- a/stacks/k8s-dashboard/main.tf +++ b/stacks/k8s-dashboard/main.tf @@ -90,6 +90,7 @@ module "ingress" { name = "kubernetes-dashboard" service_name = "kubernetes-dashboard-kong-proxy" host = "k8s" + dns_type = "proxied" tls_secret_name = var.tls_secret_name protected = true backend_protocol = "HTTPS" diff --git a/stacks/k8s-portal/modules/k8s-portal/main.tf b/stacks/k8s-portal/modules/k8s-portal/main.tf index 53c375d6..7413d2bd 100644 --- a/stacks/k8s-portal/modules/k8s-portal/main.tf +++ b/stacks/k8s-portal/modules/k8s-portal/main.tf @@ -139,6 +139,7 @@ resource "kubernetes_service" "k8s_portal" { module "ingress" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.k8s_portal.metadata[0].name name = "k8s-portal" tls_secret_name = var.tls_secret_name diff --git a/stacks/kms/main.tf b/stacks/kms/main.tf index 754e0106..8d74b558 100644 --- a/stacks/kms/main.tf +++ b/stacks/kms/main.tf @@ -116,6 +116,7 @@ resource "kubernetes_service" "kms-web-page" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.kms.metadata[0].name name = "kms" tls_secret_name = var.tls_secret_name diff --git a/stacks/linkwarden/main.tf b/stacks/linkwarden/main.tf index 0453c508..382465b4 100644 --- a/stacks/linkwarden/main.tf +++ b/stacks/linkwarden/main.tf @@ -221,6 +221,7 @@ resource "kubernetes_service" "linkwarden" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.linkwarden.metadata[0].name name = "linkwarden" tls_secret_name = var.tls_secret_name diff --git a/stacks/mailserver/modules/mailserver/roundcubemail.tf b/stacks/mailserver/modules/mailserver/roundcubemail.tf index 62d11e4f..c6d819e0 100644 --- a/stacks/mailserver/modules/mailserver/roundcubemail.tf +++ b/stacks/mailserver/modules/mailserver/roundcubemail.tf @@ -258,6 +258,7 @@ resource "kubernetes_service" "roundcubemail" { module "ingress" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = "mailserver" name = "mail" service_name = "roundcubemail" diff --git a/stacks/matrix/main.tf b/stacks/matrix/main.tf index 48e81d20..62e2483c 100644 --- a/stacks/matrix/main.tf +++ b/stacks/matrix/main.tf @@ -217,6 +217,7 @@ resource "kubernetes_service" "matrix" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.matrix.metadata[0].name name = "matrix" tls_secret_name = var.tls_secret_name diff --git a/stacks/meshcentral/main.tf b/stacks/meshcentral/main.tf index 4adaab82..66dfc9cc 100644 --- a/stacks/meshcentral/main.tf +++ b/stacks/meshcentral/main.tf @@ -258,6 +258,7 @@ resource "kubernetes_service" "meshcentral" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.meshcentral.metadata[0].name name = "meshcentral" tls_secret_name = var.tls_secret_name diff --git a/stacks/n8n/main.tf b/stacks/n8n/main.tf index 8c7d5cf9..4e4f8de1 100644 --- a/stacks/n8n/main.tf +++ b/stacks/n8n/main.tf @@ -258,6 +258,7 @@ resource "kubernetes_service" "n8n" { } module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.n8n.metadata[0].name name = "n8n" tls_secret_name = var.tls_secret_name diff --git a/stacks/navidrome/main.tf b/stacks/navidrome/main.tf index 828103de..ef880824 100644 --- a/stacks/navidrome/main.tf +++ b/stacks/navidrome/main.tf @@ -221,6 +221,7 @@ resource "kubernetes_service" "navidrome" { } module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.navidrome.metadata[0].name name = "navidrome" tls_secret_name = var.tls_secret_name diff --git a/stacks/netbox/main.tf b/stacks/netbox/main.tf index 9896b9d0..e0612945 100644 --- a/stacks/netbox/main.tf +++ b/stacks/netbox/main.tf @@ -220,6 +220,7 @@ resource "kubernetes_service" "netbox" { } module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.netbox.metadata[0].name name = "netbox" tls_secret_name = var.tls_secret_name diff --git a/stacks/networking-toolbox/main.tf b/stacks/networking-toolbox/main.tf index e7387d26..5284df77 100644 --- a/stacks/networking-toolbox/main.tf +++ b/stacks/networking-toolbox/main.tf @@ -91,6 +91,7 @@ resource "kubernetes_service" "networking-toolbox" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.networking-toolbox.metadata[0].name name = "networking-toolbox" tls_secret_name = var.tls_secret_name diff --git a/stacks/nextcloud/main.tf b/stacks/nextcloud/main.tf index e8145710..f19080ab 100644 --- a/stacks/nextcloud/main.tf +++ b/stacks/nextcloud/main.tf @@ -216,6 +216,7 @@ module "nfs_nextcloud_backup_host" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.nextcloud.metadata[0].name name = "nextcloud" tls_secret_name = var.tls_secret_name diff --git a/stacks/novelapp/main.tf b/stacks/novelapp/main.tf index 43dcd97f..e5420752 100644 --- a/stacks/novelapp/main.tf +++ b/stacks/novelapp/main.tf @@ -211,6 +211,7 @@ resource "kubernetes_service" "novelapp" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.novelapp.metadata[0].name name = "novelapp" tls_secret_name = var.tls_secret_name diff --git a/stacks/ntfy/main.tf b/stacks/ntfy/main.tf index 0c1eff1c..d29251cd 100644 --- a/stacks/ntfy/main.tf +++ b/stacks/ntfy/main.tf @@ -181,6 +181,7 @@ resource "kubernetes_service" "ntfy" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.ntfy.metadata[0].name name = "ntfy" tls_secret_name = var.tls_secret_name diff --git a/stacks/ollama/main.tf b/stacks/ollama/main.tf index c952dc31..4104ecf5 100644 --- a/stacks/ollama/main.tf +++ b/stacks/ollama/main.tf @@ -258,6 +258,7 @@ resource "kubernetes_manifest" "ollama_api_basic_auth_middleware" { module "ollama-api-ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.ollama.metadata[0].name name = "ollama-api" service_name = "ollama" @@ -362,6 +363,7 @@ resource "kubernetes_service" "ollama-ui" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.ollama.metadata[0].name name = "ollama" service_name = "ollama-ui" diff --git a/stacks/onlyoffice/main.tf b/stacks/onlyoffice/main.tf index ad804251..7cf15a85 100644 --- a/stacks/onlyoffice/main.tf +++ b/stacks/onlyoffice/main.tf @@ -242,6 +242,7 @@ resource "kubernetes_service" "onlyoffice" { } module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.onlyoffice.metadata[0].name name = "onlyoffice" service_name = "onlyoffice-document-server" diff --git a/stacks/openclaw/main.tf b/stacks/openclaw/main.tf index d127d301..ae26459b 100644 --- a/stacks/openclaw/main.tf +++ b/stacks/openclaw/main.tf @@ -625,6 +625,7 @@ resource "kubernetes_service" "openclaw" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.openclaw.metadata[0].name name = "openclaw" tls_secret_name = var.tls_secret_name @@ -1199,6 +1200,7 @@ resource "kubernetes_service" "openlobster" { module "openlobster_ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.openclaw.metadata[0].name name = "openlobster" tls_secret_name = var.tls_secret_name diff --git a/stacks/owntracks/main.tf b/stacks/owntracks/main.tf index 9528bf99..99f50159 100644 --- a/stacks/owntracks/main.tf +++ b/stacks/owntracks/main.tf @@ -203,6 +203,7 @@ resource "kubernetes_service" "owntracks" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.owntracks.metadata[0].name name = "owntracks" tls_secret_name = var.tls_secret_name diff --git a/stacks/paperless-ngx/main.tf b/stacks/paperless-ngx/main.tf index c1ad86dd..49902b73 100644 --- a/stacks/paperless-ngx/main.tf +++ b/stacks/paperless-ngx/main.tf @@ -228,6 +228,7 @@ module "ingress" { name = "paperless-ngx" service_name = "paperless-ngx" host = "pdf" + dns_type = "proxied" tls_secret_name = var.tls_secret_name port = 80 extra_annotations = { diff --git a/stacks/phpipam/main.tf b/stacks/phpipam/main.tf index da3e2cce..b0ff1f54 100644 --- a/stacks/phpipam/main.tf +++ b/stacks/phpipam/main.tf @@ -226,6 +226,7 @@ resource "kubernetes_service" "phpipam" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.phpipam.metadata[0].name name = "phpipam" tls_secret_name = var.tls_secret_name diff --git a/stacks/plotting-book/main.tf b/stacks/plotting-book/main.tf index b155159f..f799badb 100644 --- a/stacks/plotting-book/main.tf +++ b/stacks/plotting-book/main.tf @@ -192,6 +192,7 @@ resource "kubernetes_service" "plotting-book" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.plotting-book.metadata[0].name name = "plotting-book" tls_secret_name = var.tls_secret_name diff --git a/stacks/poison-fountain/main.tf b/stacks/poison-fountain/main.tf index 06e72686..e01a2af0 100644 --- a/stacks/poison-fountain/main.tf +++ b/stacks/poison-fountain/main.tf @@ -205,6 +205,7 @@ module "ingress" { namespace = kubernetes_namespace.poison_fountain.metadata[0].name name = "poison-fountain" host = "poison" + dns_type = "non-proxied" port = 8080 tls_secret_name = var.tls_secret_name skip_default_rate_limit = true diff --git a/stacks/priority-pass/main.tf b/stacks/priority-pass/main.tf index 6f783f0b..7e5aaded 100644 --- a/stacks/priority-pass/main.tf +++ b/stacks/priority-pass/main.tf @@ -115,6 +115,7 @@ resource "kubernetes_service" "priority-pass" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = "priority-pass" name = "priority-pass" tls_secret_name = var.tls_secret_name diff --git a/stacks/privatebin/main.tf b/stacks/privatebin/main.tf index 00181c35..fb30938a 100644 --- a/stacks/privatebin/main.tf +++ b/stacks/privatebin/main.tf @@ -128,6 +128,7 @@ module "ingress" { namespace = kubernetes_namespace.privatebin.metadata[0].name name = "privatebin" host = "pb" + dns_type = "proxied" tls_secret_name = var.tls_secret_name rybbit_site_id = "3ae810b0476d" custom_content_security_policy = "script-src 'self' 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' https://rybbit.viktorbarzin.me" diff --git a/stacks/real-estate-crawler/main.tf b/stacks/real-estate-crawler/main.tf index 219ab1ee..bbff4211 100644 --- a/stacks/real-estate-crawler/main.tf +++ b/stacks/real-estate-crawler/main.tf @@ -326,6 +326,7 @@ resource "kubernetes_service" "realestate-crawler-api" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.realestate-crawler.metadata[0].name name = "wrongmove" service_name = "realestate-crawler-ui" @@ -343,6 +344,7 @@ module "ingress" { module "ingress-api" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.realestate-crawler.metadata[0].name name = "wrongmove-api" host = "wrongmove" diff --git a/stacks/resume/main.tf b/stacks/resume/main.tf index d794b06c..fcf7116c 100644 --- a/stacks/resume/main.tf +++ b/stacks/resume/main.tf @@ -342,6 +342,7 @@ resource "kubernetes_service" "resume" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.resume.metadata[0].name name = "resume" tls_secret_name = var.tls_secret_name diff --git a/stacks/reverse-proxy/modules/reverse_proxy/factory/main.tf b/stacks/reverse-proxy/modules/reverse_proxy/factory/main.tf index 1cbb149c..0108862b 100644 --- a/stacks/reverse-proxy/modules/reverse_proxy/factory/main.tf +++ b/stacks/reverse-proxy/modules/reverse_proxy/factory/main.tf @@ -1,3 +1,15 @@ +terraform { + required_providers { + cloudflare = { + source = "cloudflare/cloudflare" + version = "~> 4" + } + kubernetes = { + source = "hashicorp/kubernetes" + } + } +} + variable "name" {} variable "namespace" { default = "reverse-proxy" @@ -45,6 +57,31 @@ variable "skip_global_rate_limit" { type = bool default = false } +variable "dns_type" { + type = string + default = "none" + description = "Cloudflare DNS: 'proxied' (CNAME to tunnel), 'non-proxied' (A/AAAA to public IP), or 'none'" + validation { + condition = contains(["proxied", "non-proxied", "none"], var.dns_type) + error_message = "dns_type must be 'proxied', 'non-proxied', or 'none'." + } +} +variable "cloudflare_zone_id" { + type = string + default = "fd2c5dd4efe8fe38958944e74d0ced6d" +} +variable "cloudflare_tunnel_id" { + type = string + default = "75182cd7-bb91-4310-b961-5d8967da8b41" +} +variable "public_ip" { + type = string + default = "176.12.22.76" +} +variable "public_ipv6" { + type = string + default = "2001:470:6e:43d::2" +} resource "kubernetes_service" "proxied-service" { @@ -88,7 +125,9 @@ resource "kubernetes_ingress_v1" "proxied-ingress" { "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" "traefik.ingress.kubernetes.io/service.serversscheme" = var.backend_protocol == "HTTPS" ? "https" : null "traefik.ingress.kubernetes.io/service.serverstransport" = var.backend_protocol == "HTTPS" ? "traefik-insecure-skip-verify@kubernetescrd" : null - }, var.extra_annotations) + }, var.extra_annotations, + var.dns_type != "none" ? { "cloudflare.viktorbarzin.me/dns-type" = var.dns_type } : {} + ) } spec { @@ -166,3 +205,37 @@ resource "kubernetes_manifest" "custom_csp" { } } } + +# Cloudflare DNS records — created automatically when dns_type is set. +resource "cloudflare_record" "proxied" { + count = var.dns_type == "proxied" ? 1 : 0 + name = var.name + content = "${var.cloudflare_tunnel_id}.cfargotunnel.com" + proxied = true + ttl = 1 + type = "CNAME" + zone_id = var.cloudflare_zone_id + allow_overwrite = true +} + +resource "cloudflare_record" "non_proxied_a" { + count = var.dns_type == "non-proxied" ? 1 : 0 + name = var.name + content = var.public_ip + proxied = false + ttl = 1 + type = "A" + zone_id = var.cloudflare_zone_id + allow_overwrite = true +} + +resource "cloudflare_record" "non_proxied_aaaa" { + count = var.dns_type == "non-proxied" ? 1 : 0 + name = var.name + content = var.public_ipv6 + proxied = false + ttl = 1 + type = "AAAA" + zone_id = var.cloudflare_zone_id + allow_overwrite = true +} diff --git a/stacks/reverse-proxy/modules/reverse_proxy/main.tf b/stacks/reverse-proxy/modules/reverse_proxy/main.tf index 6498c21b..91726faa 100644 --- a/stacks/reverse-proxy/modules/reverse_proxy/main.tf +++ b/stacks/reverse-proxy/modules/reverse_proxy/main.tf @@ -26,6 +26,7 @@ module "tls_secret" { # https://pfsense.viktorbarzin.me/ module "pfsense" { source = "./factory" + dns_type = "proxied" name = "pfsense" external_name = "pfsense.viktorbarzin.lan" tls_secret_name = var.tls_secret_name @@ -53,6 +54,7 @@ module "pfsense" { # https://nas.viktorbarzin.me/ module "nas" { source = "./factory" + dns_type = "proxied" name = "nas" external_name = "nas.viktorbarzin.lan" port = 5001 @@ -74,6 +76,7 @@ module "nas" { # https://files.viktorbarzin.me/ module "nas-files" { source = "./factory" + dns_type = "non-proxied" name = "files" external_name = "nas.viktorbarzin.lan" port = 5001 @@ -89,6 +92,7 @@ module "nas-files" { # https://idrac.viktorbarzin.me/ module "idrac" { source = "./factory" + dns_type = "proxied" name = "idrac" external_name = "idrac.viktorbarzin.lan" port = 443 @@ -110,6 +114,7 @@ module "idrac" { # TODO: Not working yet module "tp-link-gateway" { source = "./factory" + dns_type = "proxied" name = "gw" external_name = "gw.viktorbarzin.lan" port = 443 @@ -124,6 +129,7 @@ module "tp-link-gateway" { # https://truenas.viktorbarzin.me/ module "truenas" { source = "./factory" + dns_type = "proxied" name = "truenas" external_name = "truenas.viktorbarzin.lan" port = 80 @@ -168,6 +174,7 @@ module "r730" { # https://proxmox.viktorbarzin.me/ module "proxmox" { source = "./factory" + dns_type = "proxied" name = "proxmox" external_name = "proxmox.viktorbarzin.lan" port = 8006 @@ -189,6 +196,7 @@ module "proxmox" { # https://docker.viktorbarzin.me/ (registry web UI) module "docker-registry-ui" { source = "./factory" + dns_type = "proxied" name = "docker" external_name = "docker-registry.viktorbarzin.lan" port = 8080 @@ -209,6 +217,7 @@ module "docker-registry-ui" { # https://registry.viktorbarzin.me/ (Docker CLI push/pull endpoint) module "docker-registry-cli" { source = "./factory" + dns_type = "non-proxied" name = "registry" external_name = "docker-registry.viktorbarzin.lan" port = 5050 @@ -228,6 +237,7 @@ module "docker-registry-cli" { # https://valchedrym.viktorbarzin.me/ module "valchedrym" { source = "./factory" + dns_type = "proxied" name = "valchedrym" external_name = "valchedrym.viktorbarzin.lan" tls_secret_name = var.tls_secret_name @@ -293,6 +303,7 @@ resource "kubernetes_manifest" "ha_sofia_rate_limit" { module "ha-sofia" { source = "./factory" + dns_type = "non-proxied" name = "ha-sofia" external_name = "ha-sofia.viktorbarzin.lan" port = 8123 @@ -317,6 +328,7 @@ module "ha-sofia" { # https://music-assistant.viktorbarzin.me/ module "music-assistant" { source = "./factory" + dns_type = "non-proxied" name = "music-assistant" external_name = "ha-sofia.viktorbarzin.lan" port = 8095 @@ -332,6 +344,7 @@ module "music-assistant" { # https://ha-london.viktorbarzin.me/ module "ha-london" { source = "./factory" + dns_type = "non-proxied" name = "ha-london" external_name = "ha-london.viktorbarzin.lan" port = 8123 @@ -351,6 +364,7 @@ module "ha-london" { # https://london.viktorbarzin.me/ module "london" { source = "./factory" + dns_type = "proxied" name = "london" external_name = "openwrt-london.viktorbarzin.lan" port = 443 @@ -374,6 +388,7 @@ module "london" { } module "pi-lights" { source = "./factory" + dns_type = "proxied" name = "pi" external_name = "ha-london.viktorbarzin.lan" port = 5000 @@ -401,6 +416,7 @@ module "pi-lights" { module "mbp14" { source = "./factory" + dns_type = "proxied" name = "mbp14" external_name = "mbp14.viktorbarzin.lan" port = 4020 diff --git a/stacks/rybbit/main.tf b/stacks/rybbit/main.tf index fbea90f6..a90b2569 100644 --- a/stacks/rybbit/main.tf +++ b/stacks/rybbit/main.tf @@ -543,6 +543,7 @@ resource "kubernetes_service" "rybbit-client" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.rybbit.metadata[0].name name = "rybbit" service_name = "rybbit-client" @@ -560,6 +561,7 @@ module "ingress" { module "ingress-api" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.rybbit.metadata[0].name name = "rybbit-api" host = "rybbit" diff --git a/stacks/send/main.tf b/stacks/send/main.tf index c0f9a1b2..84890ac0 100644 --- a/stacks/send/main.tf +++ b/stacks/send/main.tf @@ -158,6 +158,7 @@ resource "kubernetes_service" "send" { } module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.send.metadata[0].name name = "send" tls_secret_name = var.tls_secret_name diff --git a/stacks/servarr/aiostreams/main.tf b/stacks/servarr/aiostreams/main.tf index b0e1d63a..8855de65 100644 --- a/stacks/servarr/aiostreams/main.tf +++ b/stacks/servarr/aiostreams/main.tf @@ -130,6 +130,7 @@ resource "kubernetes_service" "aiostreams" { module "ingress" { source = "../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.aiostreams.metadata[0].name name = "aiostreams" tls_secret_name = var.tls_secret_name diff --git a/stacks/servarr/flaresolverr/main.tf b/stacks/servarr/flaresolverr/main.tf index 6ce06ae5..9b8f3f63 100644 --- a/stacks/servarr/flaresolverr/main.tf +++ b/stacks/servarr/flaresolverr/main.tf @@ -72,6 +72,7 @@ resource "kubernetes_service" "flaresolverr" { module "ingress" { source = "../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = "servarr" name = "flaresolverr" tls_secret_name = var.tls_secret_name diff --git a/stacks/servarr/lidarr/main.tf b/stacks/servarr/lidarr/main.tf index bb706cb7..9cc1f219 100644 --- a/stacks/servarr/lidarr/main.tf +++ b/stacks/servarr/lidarr/main.tf @@ -162,6 +162,7 @@ resource "kubernetes_service" "deemix" { module "ingress" { source = "../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = "servarr" name = "lidarr" tls_secret_name = var.tls_secret_name @@ -174,6 +175,7 @@ module "ingress" { module "ingress-deemix" { source = "../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = "servarr" name = "deemix" tls_secret_name = var.tls_secret_name diff --git a/stacks/servarr/listenarr/main.tf b/stacks/servarr/listenarr/main.tf index 6a1786e2..28a5afb3 100644 --- a/stacks/servarr/listenarr/main.tf +++ b/stacks/servarr/listenarr/main.tf @@ -124,6 +124,7 @@ resource "kubernetes_service" "listenarr" { module "ingress" { source = "../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = "servarr" name = "listenarr" tls_secret_name = var.tls_secret_name diff --git a/stacks/servarr/prowlarr/main.tf b/stacks/servarr/prowlarr/main.tf index 65c2b6a3..e0eabba4 100644 --- a/stacks/servarr/prowlarr/main.tf +++ b/stacks/servarr/prowlarr/main.tf @@ -152,6 +152,7 @@ resource "kubernetes_service" "prowlarr" { module "ingress" { source = "../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = "servarr" name = "prowlarr" tls_secret_name = var.tls_secret_name diff --git a/stacks/servarr/qbittorrent/main.tf b/stacks/servarr/qbittorrent/main.tf index 318ab0ce..b32f28ca 100644 --- a/stacks/servarr/qbittorrent/main.tf +++ b/stacks/servarr/qbittorrent/main.tf @@ -381,6 +381,7 @@ print(f"Global: connected={connected} dht={dht} dl_speed={dl_speed} ul_speed={ul module "ingress" { source = "../../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = "servarr" name = "qbittorrent" tls_secret_name = var.tls_secret_name diff --git a/stacks/servarr/soulseek/main.tf b/stacks/servarr/soulseek/main.tf index e9e0fa4d..80944191 100644 --- a/stacks/servarr/soulseek/main.tf +++ b/stacks/servarr/soulseek/main.tf @@ -105,6 +105,7 @@ resource "kubernetes_service" "soulseek" { module "ingress" { source = "../../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = "servarr" name = "soulseek" tls_secret_name = var.tls_secret_name diff --git a/stacks/speedtest/main.tf b/stacks/speedtest/main.tf index e6d2e255..be84e414 100644 --- a/stacks/speedtest/main.tf +++ b/stacks/speedtest/main.tf @@ -224,6 +224,7 @@ resource "kubernetes_service" "speedtest" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.speedtest.metadata[0].name name = "speedtest" tls_secret_name = var.tls_secret_name diff --git a/stacks/stirling-pdf/main.tf b/stacks/stirling-pdf/main.tf index 7e0ca55f..0f2f5c87 100644 --- a/stacks/stirling-pdf/main.tf +++ b/stacks/stirling-pdf/main.tf @@ -124,6 +124,7 @@ resource "kubernetes_service" "stirling-pdf" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.stirling-pdf.metadata[0].name name = "stirling-pdf" tls_secret_name = var.tls_secret_name diff --git a/stacks/tandoor/main.tf b/stacks/tandoor/main.tf index 0867ac1a..136cd6f4 100644 --- a/stacks/tandoor/main.tf +++ b/stacks/tandoor/main.tf @@ -244,6 +244,7 @@ resource "kubernetes_service" "tandoor" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.tandoor.metadata[0].name name = "tandoor" tls_secret_name = var.tls_secret_name diff --git a/stacks/technitium/modules/technitium/main.tf b/stacks/technitium/modules/technitium/main.tf index 0968582a..e5cd5a24 100644 --- a/stacks/technitium/modules/technitium/main.tf +++ b/stacks/technitium/modules/technitium/main.tf @@ -312,6 +312,7 @@ resource "kubernetes_service" "technitium_dns_internal" { module "ingress" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.technitium.metadata[0].name name = "technitium" tls_secret_name = var.tls_secret_name @@ -380,7 +381,7 @@ data "kubernetes_secret" "technitium_db_creds" { depends_on = [kubernetes_manifest.external_secret] } -# Grafana datasource for Technitium DNS query logs in MySQL +# Grafana datasource for Technitium DNS query logs in PostgreSQL resource "kubernetes_config_map" "grafana_technitium_datasource" { metadata { name = "grafana-technitium-datasource" @@ -393,13 +394,18 @@ resource "kubernetes_config_map" "grafana_technitium_datasource" { "technitium-datasource.yaml" = yamlencode({ apiVersion = 1 datasources = [{ - name = "Technitium MySQL" - type = "mysql" + name = "Technitium PostgreSQL" + type = "postgres" access = "proxy" - url = "${var.mysql_host}:3306" + url = "${var.postgresql_host}:5432" database = "technitium" user = "technitium" - uid = "technitium-mysql" + uid = "technitium-pg" + jsonData = { + sslmode = "disable" + postgresVersion = 1600 + timescaledb = false + } secureJsonData = { password = data.kubernetes_secret.technitium_db_creds.data["db_password"] } @@ -475,6 +481,11 @@ resource "kubernetes_cron_job_v1" "technitium_password_sync" { TOKEN=$$(curl -sf "http://technitium-web:5380/api/user/login?user=$$TECH_USER&pass=$$TECH_PASS" | grep -o '"token":"[^"]*"' | cut -d'"' -f4) if [ -z "$$TOKEN" ]; then echo "Login failed"; exit 1; fi + # Disable SQLite query logging (eliminates ~18 GB/day write amplification on encrypted PVC) + SQLITE_CONFIG="{\"enableLogging\":false,\"maxLogDays\":0,\"maxLogRecords\":0}" + curl -sf -X POST "http://technitium-web:5380/api/apps/config/set?token=$$TOKEN" --data-urlencode "name=Query Logs (Sqlite)" --data-urlencode "config=$$SQLITE_CONFIG" + echo "SQLite logging disabled on primary" + # Disable MySQL query logging MYSQL_CONFIG="{\"enableLogging\":false,\"maxQueueSize\":1000000,\"maxLogDays\":30,\"maxLogRecords\":0,\"databaseName\":\"technitium\",\"connectionString\":\"Server=mysql.dbaas.svc.cluster.local; Port=3306; Uid=technitium; Pwd=$$DB_PASSWORD;\"}" curl -sf -X POST "http://technitium-web:5380/api/apps/config/set?token=$$TOKEN" --data-urlencode "name=Query Logs (MySQL)" --data-urlencode "config=$$MYSQL_CONFIG" @@ -489,7 +500,17 @@ resource "kubernetes_cron_job_v1" "technitium_password_sync" { # Configure PG query logging PG_CONFIG="{\"enableLogging\":true,\"maxQueueSize\":1000000,\"maxLogDays\":90,\"maxLogRecords\":0,\"databaseName\":\"technitium\",\"connectionString\":\"Host=${var.postgresql_host}; Port=5432; Username=technitium; Password=$$DB_PASSWORD;\"}" curl -sf -X POST "http://technitium-web:5380/api/apps/config/set?token=$$TOKEN" --data-urlencode "name=Query Logs (Postgres)" --data-urlencode "config=$$PG_CONFIG" - echo "PG logging configured" + echo "PG logging configured on primary" + + # Disable SQLite on secondary and tertiary instances + for INST in http://technitium-secondary-web:5380 http://technitium-tertiary-web:5380; do + echo "Configuring $$INST" + R_TOKEN=$$(curl -sf "$$INST/api/user/login?user=$$TECH_USER&pass=$$TECH_PASS" | grep -o '"token":"[^"]*"' | cut -d'"' -f4) + if [ -z "$$R_TOKEN" ]; then echo "Login failed for $$INST, skipping"; continue; fi + curl -sf -X POST "$$INST/api/apps/config/set?token=$$R_TOKEN" --data-urlencode "name=Query Logs (Sqlite)" --data-urlencode "config=$$SQLITE_CONFIG" || echo "WARN: SQLite plugin not present on $$INST" + echo "SQLite logging disabled on $$INST" + done + echo "Password sync complete" EOT ] } diff --git a/stacks/terminal/main.tf b/stacks/terminal/main.tf index 97b94781..6368fd42 100644 --- a/stacks/terminal/main.tf +++ b/stacks/terminal/main.tf @@ -57,6 +57,7 @@ resource "kubernetes_endpoints" "terminal" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.terminal.metadata[0].name name = "terminal" tls_secret_name = var.tls_secret_name @@ -197,6 +198,7 @@ resource "kubernetes_manifest" "clipboard_strip_prefix" { module "ingress_ro" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.terminal.metadata[0].name name = "terminal-ro" tls_secret_name = var.tls_secret_name diff --git a/stacks/trading-bot/main.tf b/stacks/trading-bot/main.tf index 0a7230a7..103f773b 100644 --- a/stacks/trading-bot/main.tf +++ b/stacks/trading-bot/main.tf @@ -611,6 +611,7 @@ resource "kubernetes_service" "trading-bot-frontend" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.trading-bot.metadata[0].name name = "trading" service_name = "trading-bot-frontend" diff --git a/stacks/traefik/modules/traefik/main.tf b/stacks/traefik/modules/traefik/main.tf index 586e7f76..5b09a859 100644 --- a/stacks/traefik/modules/traefik/main.tf +++ b/stacks/traefik/modules/traefik/main.tf @@ -279,6 +279,7 @@ resource "kubernetes_service" "traefik_dashboard" { module "ingress" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.traefik.metadata[0].name name = "traefik" service_name = "traefik-dashboard" diff --git a/stacks/tuya-bridge/main.tf b/stacks/tuya-bridge/main.tf index 8e8ce404..9f9d5c16 100644 --- a/stacks/tuya-bridge/main.tf +++ b/stacks/tuya-bridge/main.tf @@ -152,6 +152,7 @@ resource "kubernetes_service" "tuya-bridge" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.tuya-bridge.metadata[0].name name = "tuya-bridge" tls_secret_name = var.tls_secret_name diff --git a/stacks/uptime-kuma/modules/uptime-kuma/main.tf b/stacks/uptime-kuma/modules/uptime-kuma/main.tf index a1b7bb21..fa6f157c 100644 --- a/stacks/uptime-kuma/modules/uptime-kuma/main.tf +++ b/stacks/uptime-kuma/modules/uptime-kuma/main.tf @@ -174,6 +174,7 @@ resource "kubernetes_service" "uptime-kuma" { } module "ingress" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.uptime-kuma.metadata[0].name name = "uptime" tls_secret_name = var.tls_secret_name diff --git a/stacks/url/main.tf b/stacks/url/main.tf index 4af5387d..8cf30ef0 100644 --- a/stacks/url/main.tf +++ b/stacks/url/main.tf @@ -280,6 +280,7 @@ resource "kubernetes_service" "shlink" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.shlink.metadata[0].name name = "url" service_name = "shlink" @@ -420,6 +421,7 @@ resource "kubernetes_service" "shlink-web" { module "ingress-web" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.shlink.metadata[0].name name = "shlink" service_name = "shlink-web" diff --git a/stacks/vault/main.tf b/stacks/vault/main.tf index 49ea1505..52e06953 100644 --- a/stacks/vault/main.tf +++ b/stacks/vault/main.tf @@ -223,6 +223,7 @@ resource "vault_identity_group_alias" "admins" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.vault.metadata[0].name name = "vault" service_name = "vault-active" diff --git a/stacks/vaultwarden/modules/vaultwarden/main.tf b/stacks/vaultwarden/modules/vaultwarden/main.tf index ecc44499..3c04c147 100644 --- a/stacks/vaultwarden/modules/vaultwarden/main.tf +++ b/stacks/vaultwarden/modules/vaultwarden/main.tf @@ -189,6 +189,7 @@ resource "kubernetes_service" "vaultwarden" { module "ingress" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.vaultwarden.metadata[0].name name = "vaultwarden" tls_secret_name = var.tls_secret_name diff --git a/stacks/vpa/modules/vpa/main.tf b/stacks/vpa/modules/vpa/main.tf index f1d7d4e4..71b2261b 100644 --- a/stacks/vpa/modules/vpa/main.tf +++ b/stacks/vpa/modules/vpa/main.tf @@ -105,6 +105,7 @@ resource "helm_release" "goldilocks" { # ----------------------------------------------------------------------------- module "ingress" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.vpa.metadata[0].name name = "goldilocks" service_name = "goldilocks-dashboard" diff --git a/stacks/wealthfolio/main.tf b/stacks/wealthfolio/main.tf index cafa1ccc..6288f484 100644 --- a/stacks/wealthfolio/main.tf +++ b/stacks/wealthfolio/main.tf @@ -193,6 +193,7 @@ resource "kubernetes_service" "wealthfolio" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.wealthfolio.metadata[0].name name = "wealthfolio" tls_secret_name = var.tls_secret_name diff --git a/stacks/webhook_handler/main.tf b/stacks/webhook_handler/main.tf index 74810790..3c822780 100644 --- a/stacks/webhook_handler/main.tf +++ b/stacks/webhook_handler/main.tf @@ -259,6 +259,7 @@ module "ingress" { namespace = kubernetes_namespace.webhook-handler.metadata[0].name name = "webhook-handler" host = "webhook" + dns_type = "non-proxied" tls_secret_name = var.tls_secret_name extra_annotations = { "gethomepage.dev/enabled" = "true" diff --git a/stacks/woodpecker/main.tf b/stacks/woodpecker/main.tf index 51af62b9..71cc6ee6 100644 --- a/stacks/woodpecker/main.tf +++ b/stacks/woodpecker/main.tf @@ -317,6 +317,7 @@ resource "kubernetes_cron_job_v1" "vault_secret_sync" { module "ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.woodpecker.metadata[0].name name = "ci" service_name = "woodpecker-server" diff --git a/stacks/xray/modules/xray/main.tf b/stacks/xray/modules/xray/main.tf index 12b47557..2540d6a0 100644 --- a/stacks/xray/modules/xray/main.tf +++ b/stacks/xray/modules/xray/main.tf @@ -210,6 +210,7 @@ resource "kubernetes_service" "xray-reality" { module "ingress_ws" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.xray.metadata[0].name name = "xray-ws" service_name = "xray" @@ -220,6 +221,7 @@ module "ingress_ws" { module "ingress_grpc" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.xray.metadata[0].name name = "xray-grpc" service_name = "xray" @@ -234,6 +236,7 @@ module "ingress_grpc" { module "ingress_vless" { source = "../../../../modules/kubernetes/ingress_factory" + dns_type = "proxied" namespace = kubernetes_namespace.xray.metadata[0].name name = "xray-vless" service_name = "xray" diff --git a/stacks/ytdlp/main.tf b/stacks/ytdlp/main.tf index 91b16f92..ee7f1d00 100644 --- a/stacks/ytdlp/main.tf +++ b/stacks/ytdlp/main.tf @@ -173,6 +173,7 @@ module "ingress" { name = "ytdlp" tls_secret_name = var.tls_secret_name host = "yt" + dns_type = "non-proxied" extra_annotations = { "gethomepage.dev/enabled" = "true" "gethomepage.dev/name" = "yt-dlp" @@ -347,6 +348,7 @@ resource "kubernetes_service" "yt_highlights" { module "highlights_ingress" { source = "../../modules/kubernetes/ingress_factory" + dns_type = "non-proxied" namespace = kubernetes_namespace.ytdlp.metadata[0].name name = "yt-highlights" tls_secret_name = var.tls_secret_name diff --git a/terragrunt.hcl b/terragrunt.hcl index 24413559..9315da4e 100644 --- a/terragrunt.hcl +++ b/terragrunt.hcl @@ -35,7 +35,7 @@ terraform { } } -# Generate kubernetes + helm providers for K8s stacks. +# Generate kubernetes + helm + cloudflare providers for all stacks. # The infra stack overrides this to add the proxmox provider. generate "k8s_providers" { path = "providers.tf" @@ -47,6 +47,10 @@ terraform { source = "hashicorp/vault" version = "~> 4.0" } + cloudflare = { + source = "cloudflare/cloudflare" + version = "~> 4" + } } } @@ -72,6 +76,25 @@ provider "vault" { EOF } +# Generate Cloudflare provider config (separate file to avoid conflicts +# with stacks that override providers.tf, e.g. infra stack). +# DNS records are created per-service via ingress_factory's dns_type param. +generate "cloudflare_provider" { + path = "cloudflare_provider.tf" + if_exists = "overwrite_terragrunt" + contents = <