infra/modules/kubernetes/ingress_factory/main.tf
2026-03-29 01:04:11 +02:00

257 lines
7 KiB
HCL

variable "name" { type = string }
variable "service_name" {
type = string
default = null # defaults to name
}
variable "host" {
type = string
default = null
}
variable "namespace" { type = string }
variable "external_name" {
type = string
default = null
}
variable "port" {
default = "80"
}
variable "tls_secret_name" {}
variable "backend_protocol" {
default = "HTTP"
}
variable "protected" {
type = bool
default = false
}
variable "ingress_path" {
type = list(string)
default = ["/"]
}
variable "max_body_size" {
type = string
default = "50m"
}
variable "extra_annotations" {
default = {}
}
variable "ssl_redirect" {
default = true
type = bool
}
variable "allow_local_access_only" {
default = false
type = bool
}
variable "root_domain" {
default = "viktorbarzin.me"
type = string
}
variable "rybbit_site_id" {
default = null
type = string
}
variable "custom_content_security_policy" {
type = string
default = null
}
variable "exclude_crowdsec" {
type = bool
default = false
}
variable "full_host" {
type = string
default = null
}
variable "extra_middlewares" {
type = list(string)
default = []
}
variable "skip_default_rate_limit" {
type = bool
default = false
}
variable "anti_ai_scraping" {
type = bool
default = null # null = auto (enabled when not protected, disabled when protected)
}
variable "homepage_group" {
type = string
default = null # auto-detect from namespace
}
variable "homepage_enabled" {
type = bool
default = true
}
locals {
effective_host = var.full_host != null ? var.full_host : "${var.host != null ? var.host : var.name}.${var.root_domain}"
effective_anti_ai = var.anti_ai_scraping != null ? var.anti_ai_scraping : !var.protected
ns_to_group = {
monitoring = "Infrastructure"
prometheus = "Infrastructure"
technitium = "Infrastructure"
traefik = "Infrastructure"
metallb-system = "Infrastructure"
kyverno = "Infrastructure"
authentik = "Identity & Security"
crowdsec = "Identity & Security"
woodpecker = "Development & CI"
forgejo = "Development & CI"
immich = "Media & Entertainment"
frigate = "Smart Home"
home-assistant = "Smart Home"
ollama = "AI & Data"
dbaas = "Infrastructure"
servarr = "Media & Entertainment"
navidrome = "Media & Entertainment"
nextcloud = "Productivity"
n8n = "Automation"
changedetection = "Automation"
finance = "Finance & Personal"
homepage = "Core Platform"
reverse-proxy = "Smart Home"
mailserver = "Infrastructure"
}
homepage_group = coalesce(
var.homepage_group,
lookup(local.ns_to_group, var.namespace, "Other")
)
homepage_defaults = var.homepage_enabled ? {
"gethomepage.dev/enabled" = "true"
"gethomepage.dev/name" = replace(replace(var.name, "-", " "), "_", " ")
"gethomepage.dev/group" = local.homepage_group
"gethomepage.dev/href" = "https://${local.effective_host}"
"gethomepage.dev/icon" = "${replace(var.name, "-", "")}.png"
} : {}
}
resource "kubernetes_service" "proxied-service" {
count = var.external_name == null ? 0 : 1
metadata {
name = var.name
namespace = var.namespace
labels = {
"app" = var.name
}
}
spec {
type = var.external_name != null ? "ExternalName" : "ClusterIP"
external_name = var.name
port {
name = "${var.name}-web"
port = var.port
protocol = "TCP"
target_port = var.port
}
}
}
resource "kubernetes_ingress_v1" "proxied-ingress" {
metadata {
name = var.name
namespace = var.namespace
annotations = merge({
"traefik.ingress.kubernetes.io/router.middlewares" = join(",", compact(concat([
"traefik-retry@kubernetescrd",
"traefik-error-pages@kubernetescrd",
var.skip_default_rate_limit ? null : "traefik-rate-limit@kubernetescrd",
var.custom_content_security_policy == null ? "traefik-csp-headers@kubernetescrd" : null,
var.exclude_crowdsec ? null : "traefik-crowdsec@kubernetescrd",
local.effective_anti_ai ? "traefik-ai-bot-block@kubernetescrd" : null,
local.effective_anti_ai ? "traefik-anti-ai-headers@kubernetescrd" : null,
local.effective_anti_ai ? "traefik-strip-accept-encoding@kubernetescrd" : null,
local.effective_anti_ai ? "traefik-anti-ai-trap-links@kubernetescrd" : null,
var.protected ? "traefik-authentik-forward-auth@kubernetescrd" : null,
var.allow_local_access_only ? "traefik-local-only@kubernetescrd" : null,
var.rybbit_site_id != null ? "traefik-strip-accept-encoding@kubernetescrd" : null,
var.rybbit_site_id != null ? "${var.namespace}-rybbit-analytics-${var.name}@kubernetescrd" : null,
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)
}
spec {
ingress_class_name = "traefik"
tls {
hosts = [local.effective_host]
secret_name = var.tls_secret_name
}
rule {
host = local.effective_host
http {
dynamic "path" {
for_each = var.ingress_path
content {
path = path.value
backend {
service {
name = var.service_name != null ? var.service_name : var.name
port {
number = var.port
}
}
}
}
}
}
}
}
}
# Rybbit analytics middleware (rewrite-body plugin with content-type filtering) - created per service when rybbit_site_id is set
resource "kubernetes_manifest" "rybbit_analytics" {
count = var.rybbit_site_id != null ? 1 : 0
manifest = {
apiVersion = "traefik.io/v1alpha1"
kind = "Middleware"
metadata = {
name = "rybbit-analytics-${var.name}"
namespace = var.namespace
}
spec = {
plugin = {
rewrite-body = {
rewrites = [{
regex = "</head>"
replacement = "<script src=\"https://rybbit.viktorbarzin.me/api/script.js\" data-site-id=\"${var.rybbit_site_id}\" defer></script></head>"
}]
monitoring = {
types = ["text/html"]
}
}
}
}
}
}
# Custom CSP headers middleware - created per service when custom_content_security_policy is set
resource "kubernetes_manifest" "custom_csp" {
count = var.custom_content_security_policy != null ? 1 : 0
manifest = {
apiVersion = "traefik.io/v1alpha1"
kind = "Middleware"
metadata = {
name = "custom-csp-${var.name}"
namespace = var.namespace
}
spec = {
headers = {
contentSecurityPolicy = var.custom_content_security_policy
}
}
}
}