# Dev Artifacts Server # # Serves static build artifacts at dev.abaci.one: # - /smoke-reports/ - Playwright HTML reports from smoke tests # - /storybook/ - Component library documentation # - /coverage/ - Test coverage reports (future) # # Architecture: # - NFS-backed PVC shared between artifact producers and nginx # - nginx serves files read-only with directory listing enabled # - Smoke tests CronJob writes reports directly to filesystem # NFS PersistentVolume for dev artifacts resource "kubernetes_persistent_volume" "dev_artifacts" { metadata { name = "dev-artifacts-pv" labels = { type = "nfs" app = "dev-artifacts" } } spec { capacity = { storage = "10Gi" } access_modes = ["ReadWriteMany"] persistent_volume_reclaim_policy = "Retain" storage_class_name = "nfs" persistent_volume_source { nfs { server = var.nfs_server path = "/volume1/homes/antialias/projects/abaci.one/data/dev-artifacts" } } } } resource "kubernetes_persistent_volume_claim" "dev_artifacts" { metadata { name = "dev-artifacts" namespace = kubernetes_namespace.abaci.metadata[0].name } spec { access_modes = ["ReadWriteMany"] storage_class_name = "nfs" resources { requests = { storage = "10Gi" } } selector { match_labels = { type = "nfs" app = "dev-artifacts" } } } } # nginx ConfigMap for custom configuration resource "kubernetes_config_map" "dev_artifacts_nginx" { metadata { name = "dev-artifacts-nginx" namespace = kubernetes_namespace.abaci.metadata[0].name } data = { "default.conf" = <<-EOT server { listen 80; server_name _; root /usr/share/nginx/html; # Enable directory listing for subdirectories autoindex on; autoindex_exact_size off; autoindex_localtime on; # Serve index page at root location = / { root /usr/share/nginx/static; try_files /index.html =404; } # Serve static files with caching location / { try_files $uri $uri/ =404; # Cache static assets location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { expires 1d; add_header Cache-Control "public, immutable"; } # HTML files - no cache for fresh reports location ~* \.html$ { expires -1; add_header Cache-Control "no-store, no-cache, must-revalidate"; } } # Serve Storybook from local storage location /storybook/ { alias /usr/share/nginx/html/storybook/; try_files $uri $uri/ /storybook/index.html; } location = /storybook { return 301 /storybook/; } # Health check endpoint location /health { return 200 'ok'; add_header Content-Type text/plain; } } EOT "index.html" = <<-EOT Abaci Dev Portal

Abaci Dev Portal

Development resources and monitoring tools

Testing & QA
๐Ÿงช
Smoke Test Reports Live
Playwright E2E test results with screenshots, traces, and video recordings. Updated every 15 minutes.
/smoke-reports/
๐Ÿ“š
Storybook Live
Interactive component library with documentation, props tables, and live examples.
/storybook/
๐Ÿ“Š
Test Coverage Soon
Code coverage reports showing tested vs untested code paths.
/coverage/
Monitoring
๐Ÿ“ˆ
Grafana Live
Dashboards for application metrics, performance monitoring, and alerting.
grafana.dev.abaci.one
๐Ÿ”ฅ
Prometheus Live
Metrics storage and PromQL query interface for debugging and analysis.
prometheus.dev.abaci.one
๐ŸŸข
Status Page Live
Public uptime monitoring and incident status powered by Gatus.
status.abaci.one
Quick Links
๐Ÿงฎ
Production App
The live Abaci flashcards application.
abaci.one
๐Ÿ™
GitHub Repository
Source code, issues, and pull requests.
github.com/antialias/...
EOT } } # nginx Deployment resource "kubernetes_deployment" "dev_artifacts" { metadata { name = "dev-artifacts" namespace = kubernetes_namespace.abaci.metadata[0].name labels = { app = "dev-artifacts" } } spec { replicas = 1 selector { match_labels = { app = "dev-artifacts" } } template { metadata { labels = { app = "dev-artifacts" } } spec { container { name = "nginx" image = "nginx:1.25-alpine" port { container_port = 80 } volume_mount { name = "artifacts" mount_path = "/usr/share/nginx/html" read_only = true } volume_mount { name = "nginx-config" mount_path = "/etc/nginx/conf.d/default.conf" sub_path = "default.conf" read_only = true } volume_mount { name = "nginx-config" mount_path = "/usr/share/nginx/static/index.html" sub_path = "index.html" read_only = true } resources { requests = { memory = "32Mi" cpu = "10m" } limits = { memory = "64Mi" cpu = "100m" } } liveness_probe { http_get { path = "/health" port = 80 } initial_delay_seconds = 5 period_seconds = 30 } readiness_probe { http_get { path = "/health" port = 80 } initial_delay_seconds = 2 period_seconds = 10 } } volume { name = "artifacts" persistent_volume_claim { claim_name = kubernetes_persistent_volume_claim.dev_artifacts.metadata[0].name } } volume { name = "nginx-config" config_map { name = kubernetes_config_map.dev_artifacts_nginx.metadata[0].name } } } } } } # Service for nginx resource "kubernetes_service" "dev_artifacts" { metadata { name = "dev-artifacts" namespace = kubernetes_namespace.abaci.metadata[0].name } spec { selector = { app = "dev-artifacts" } port { port = 80 target_port = 80 } type = "ClusterIP" } } # Ingress for dev.abaci.one resource "kubernetes_ingress_v1" "dev_artifacts" { metadata { name = "dev-artifacts" namespace = kubernetes_namespace.abaci.metadata[0].name annotations = { "cert-manager.io/cluster-issuer" = var.use_staging_certs ? "letsencrypt-staging" : "letsencrypt-prod" "traefik.ingress.kubernetes.io/router.entrypoints" = "websecure" "traefik.ingress.kubernetes.io/router.middlewares" = "${kubernetes_namespace.abaci.metadata[0].name}-hsts@kubernetescrd" } } spec { ingress_class_name = "traefik" tls { hosts = ["dev.${var.app_domain}"] secret_name = "dev-artifacts-tls" } rule { host = "dev.${var.app_domain}" http { path { path = "/" path_type = "Prefix" backend { service { name = kubernetes_service.dev_artifacts.metadata[0].name port { number = 80 } } } } } } } depends_on = [null_resource.cert_manager_issuers] } # HTTP to HTTPS redirect for dev subdomain resource "kubernetes_ingress_v1" "dev_artifacts_http_redirect" { metadata { name = "dev-artifacts-http-redirect" namespace = kubernetes_namespace.abaci.metadata[0].name annotations = { "traefik.ingress.kubernetes.io/router.entrypoints" = "web" "traefik.ingress.kubernetes.io/router.middlewares" = "${kubernetes_namespace.abaci.metadata[0].name}-redirect-https@kubernetescrd" } } spec { ingress_class_name = "traefik" rule { host = "dev.${var.app_domain}" http { path { path = "/" path_type = "Prefix" backend { service { name = kubernetes_service.dev_artifacts.metadata[0].name port { number = 80 } } } } } } } }