From 250a058627084c4c3a533ada29dbe42747afa2b5 Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Thu, 19 Mar 2026 23:14:27 +0000 Subject: [PATCH] feat(traefik): add custom error pages with tarampampam/error-pages Deploy error-pages service to show themed error pages instead of raw Traefik 502/503/504 responses. Adds catch-all IngressRoute (priority 1) for 404 on unknown hosts. Only 5xx intercepted to avoid breaking JSON APIs. --- modules/kubernetes/ingress_factory/main.tf | 1 + .../platform/modules/traefik/error-pages.tf | 168 ++++++++++++++++++ stacks/traefik/modules/traefik/error-pages.tf | 168 ++++++++++++++++++ 3 files changed, 337 insertions(+) create mode 100644 stacks/platform/modules/traefik/error-pages.tf create mode 100644 stacks/traefik/modules/traefik/error-pages.tf diff --git a/modules/kubernetes/ingress_factory/main.tf b/modules/kubernetes/ingress_factory/main.tf index 9a86a429..f90408e9 100644 --- a/modules/kubernetes/ingress_factory/main.tf +++ b/modules/kubernetes/ingress_factory/main.tf @@ -111,6 +111,7 @@ resource "kubernetes_ingress_v1" "proxied-ingress" { 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", diff --git a/stacks/platform/modules/traefik/error-pages.tf b/stacks/platform/modules/traefik/error-pages.tf new file mode 100644 index 00000000..13eab3c2 --- /dev/null +++ b/stacks/platform/modules/traefik/error-pages.tf @@ -0,0 +1,168 @@ +# Custom error pages using tarampampam/error-pages +# Serves themed error pages for 5xx errors and catch-all 404 for unknown hosts + +resource "kubernetes_deployment" "error_pages" { + metadata { + name = "error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + labels = { + app = "error-pages" + } + } + + spec { + replicas = 2 + strategy { + type = "RollingUpdate" + rolling_update { + max_unavailable = 0 + max_surge = 1 + } + } + selector { + match_labels = { + app = "error-pages" + } + } + template { + metadata { + labels = { + app = "error-pages" + } + annotations = { + "diun.enable" = "true" + } + } + spec { + topology_spread_constraint { + max_skew = 1 + topology_key = "kubernetes.io/hostname" + when_unsatisfiable = "DoNotSchedule" + label_selector { + match_labels = { + app = "error-pages" + } + } + } + container { + name = "error-pages" + image = "ghcr.io/tarampampam/error-pages:3" + + port { + container_port = 8080 + } + + env { + name = "TEMPLATE_NAME" + value = "shuffle" + } + + liveness_probe { + http_get { + path = "/healthz" + port = 8080 + } + initial_delay_seconds = 3 + period_seconds = 10 + } + readiness_probe { + http_get { + path = "/healthz" + port = 8080 + } + initial_delay_seconds = 2 + period_seconds = 5 + } + + resources { + requests = { + cpu = "5m" + memory = "32Mi" + } + limits = { + memory = "32Mi" + } + } + } + } + } + } + + lifecycle { + ignore_changes = [spec[0].template[0].spec[0].dns_config] + } +} + +resource "kubernetes_service" "error_pages" { + metadata { + name = "error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + labels = { + app = "error-pages" + } + } + + spec { + selector = { + app = "error-pages" + } + port { + name = "http" + port = 8080 + target_port = 8080 + } + } +} + +# Errors middleware — intercepts 5xx from backends and serves themed error pages +resource "kubernetes_manifest" "middleware_error_pages" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "Middleware" + metadata = { + name = "error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + } + spec = { + errors = { + status = ["500-504"] + service = { + name = "error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + port = 8080 + } + query = "/{status}" + } + } + } + + depends_on = [helm_release.traefik, kubernetes_service.error_pages] +} + +# Catch-all IngressRoute — serves 404 for unknown hosts (lowest priority) +resource "kubernetes_manifest" "ingressroute_catchall" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "IngressRoute" + metadata = { + name = "catchall-error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + } + spec = { + entryPoints = ["websecure"] + routes = [{ + match = "HostRegexp(`.+`)" + kind = "Rule" + priority = 1 + services = [{ + name = "error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + port = 8080 + }] + }] + tls = {} + } + } + + depends_on = [helm_release.traefik, kubernetes_service.error_pages] +} diff --git a/stacks/traefik/modules/traefik/error-pages.tf b/stacks/traefik/modules/traefik/error-pages.tf new file mode 100644 index 00000000..13eab3c2 --- /dev/null +++ b/stacks/traefik/modules/traefik/error-pages.tf @@ -0,0 +1,168 @@ +# Custom error pages using tarampampam/error-pages +# Serves themed error pages for 5xx errors and catch-all 404 for unknown hosts + +resource "kubernetes_deployment" "error_pages" { + metadata { + name = "error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + labels = { + app = "error-pages" + } + } + + spec { + replicas = 2 + strategy { + type = "RollingUpdate" + rolling_update { + max_unavailable = 0 + max_surge = 1 + } + } + selector { + match_labels = { + app = "error-pages" + } + } + template { + metadata { + labels = { + app = "error-pages" + } + annotations = { + "diun.enable" = "true" + } + } + spec { + topology_spread_constraint { + max_skew = 1 + topology_key = "kubernetes.io/hostname" + when_unsatisfiable = "DoNotSchedule" + label_selector { + match_labels = { + app = "error-pages" + } + } + } + container { + name = "error-pages" + image = "ghcr.io/tarampampam/error-pages:3" + + port { + container_port = 8080 + } + + env { + name = "TEMPLATE_NAME" + value = "shuffle" + } + + liveness_probe { + http_get { + path = "/healthz" + port = 8080 + } + initial_delay_seconds = 3 + period_seconds = 10 + } + readiness_probe { + http_get { + path = "/healthz" + port = 8080 + } + initial_delay_seconds = 2 + period_seconds = 5 + } + + resources { + requests = { + cpu = "5m" + memory = "32Mi" + } + limits = { + memory = "32Mi" + } + } + } + } + } + } + + lifecycle { + ignore_changes = [spec[0].template[0].spec[0].dns_config] + } +} + +resource "kubernetes_service" "error_pages" { + metadata { + name = "error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + labels = { + app = "error-pages" + } + } + + spec { + selector = { + app = "error-pages" + } + port { + name = "http" + port = 8080 + target_port = 8080 + } + } +} + +# Errors middleware — intercepts 5xx from backends and serves themed error pages +resource "kubernetes_manifest" "middleware_error_pages" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "Middleware" + metadata = { + name = "error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + } + spec = { + errors = { + status = ["500-504"] + service = { + name = "error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + port = 8080 + } + query = "/{status}" + } + } + } + + depends_on = [helm_release.traefik, kubernetes_service.error_pages] +} + +# Catch-all IngressRoute — serves 404 for unknown hosts (lowest priority) +resource "kubernetes_manifest" "ingressroute_catchall" { + manifest = { + apiVersion = "traefik.io/v1alpha1" + kind = "IngressRoute" + metadata = { + name = "catchall-error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + } + spec = { + entryPoints = ["websecure"] + routes = [{ + match = "HostRegexp(`.+`)" + kind = "Rule" + priority = 1 + services = [{ + name = "error-pages" + namespace = kubernetes_namespace.traefik.metadata[0].name + port = 8080 + }] + }] + tls = {} + } + } + + depends_on = [helm_release.traefik, kubernetes_service.error_pages] +}