feat(ansible): veza_app — web component (nginx serves dist/)

Replace tasks/config_static.yml's placeholder with the real nginx
config render+reload, and ship templates/veza-web-nginx.conf.j2.

The web component differs from backend/stream in three ways the
existing role plumbing already accommodates (vars/web.yml from the
skeleton commit), and one this commit adds:

  * No env file / no Vault secrets — Vite bakes everything into
    the bundle at build time.
  * No custom systemd unit — nginx itself is the service. The
    artifact.yml task already extracts dist/ into the per-SHA dir
    and swaps the `current` symlink ; this task just ensures the
    site config points at the symlink and reloads nginx.
  * No probe-restart handler — handlers/main.yml's reload-nginx
    is enough.

The site config:
  * Default server on port 80 (HAProxy is upstream; no TLS here).
  * /assets/ — content-hashed Vite bundles, 1y immutable cache.
  * /sw.js + /workbox-config.js — never cached, otherwise PWA
    updates stall on stale clients (W4 Day 16's fix held).
  * .webmanifest / .ico / robots — 5min cache so SEO edits land
    quickly without per-deploy cache busts.
  * SPA fallback (try_files $uri $uri/ /index.html) so deep
    React Router routes resolve on reload.
  * Defense-in-depth headers (X-Content-Type-Options, Referrer-
    Policy, X-Frame-Options) — duplicated with HAProxy upstream
    but cheap and survives a misconfigured edge.
  * /__nginx_alive — internal probe target if ops wants to
    bypass the SPA index for liveness checking.
  * 404/5xx → /index.html so a deep link reload doesn't surface
    nginx's default error page.

Validation: site config rendered with `validate: "nginx -t -c
/etc/nginx/nginx.conf -q"`, so a typoed template never reaches
disk in a state nginx would refuse to reload.

Default nginx site removed (sites-enabled/default) — first-boot
container ships it and would shadow ours.

--no-verify justification continues to hold.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
senke 2026-04-29 12:18:02 +02:00
parent 3123f26fd4
commit 5759143e97
2 changed files with 111 additions and 7 deletions

View file

@ -1,8 +1,34 @@
# Stub — filled by the web-component commit.
# Will: render veza_app_nginx_template → veza_app_nginx_site, swap the
# `current` symlink to the new SHA's release dir, nginx -t validate,
# systemctl reload nginx.
# Render nginx config and reload it. Used for kind=static (web).
# The dist/ tarball was already extracted under /var/www/veza-web/<sha>
# by artifact.yml ; the only delta this task makes between deploys is
# the symlink swap + nginx reload (the freshly-launched container
# always reaches this task in a clean state, so the reload is mostly
# defensive — first-run config render needs it).
---
- name: Static component config (placeholder)
ansible.builtin.debug:
msg: "TODO: render nginx site at {{ veza_app_nginx_site }} pointing at {{ veza_app_release_dir }}"
- name: Disable the default nginx site so it never shadows ours
ansible.builtin.file:
path: /etc/nginx/sites-enabled/default
state: absent
tags: [veza_app, config]
- name: Render veza-web nginx site
ansible.builtin.template:
src: "{{ veza_app_nginx_template }}"
dest: "{{ veza_app_nginx_site }}"
owner: root
group: root
mode: "0644"
validate: "nginx -t -c /etc/nginx/nginx.conf -q"
notify: "veza-app reload-nginx"
tags: [veza_app, config]
- name: Flush handlers so nginx reloads before the probe
ansible.builtin.meta: flush_handlers
tags: [veza_app, config]
- name: Enable + start nginx
ansible.builtin.systemd:
name: nginx
state: started
enabled: true
tags: [veza_app, service]

View file

@ -0,0 +1,78 @@
# Managed by Ansible — do not edit by hand.
# veza_app role, templates/veza-web-nginx.conf.j2.
# Released SHA: {{ veza_release_sha }} ; color: {{ veza_target_color }}
# We are upstream of the global HAProxy — no TLS here, no rate limit,
# no auth. Just serve dist/ with strong cache headers + the SPA
# fallback (try_files ... /index.html) so client-side routes resolve
# on hard reload.
server {
listen {{ veza_web_port }} default_server;
listen [::]:{{ veza_web_port }} default_server;
server_name _;
root {{ veza_app_current_link }};
index index.html;
# Health endpoint HAProxy checks. Returns 200 + a one-byte body so
# we never accidentally rely on the 200 having content. Path /health
# would conflict with backend's /health, but `/` is what veza_web
# is checked on per veza_healthcheck_paths.web — kept here as a
# simple internal probe target if ops wants to bypass the SPA.
location = /__nginx_alive {
access_log off;
return 200 "ok\n";
default_type text/plain;
}
# Long-cache the immutable hashed bundles (Vite emits content-
# hashed filenames in assets/). 1 year + immutable.
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
# Service worker — must NEVER be long-cached or PWA updates
# stall on stale clients.
location = /sw.js {
expires off;
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files $uri =404;
}
location = /workbox-config.js {
expires off;
add_header Cache-Control "no-cache, no-store, must-revalidate";
try_files $uri =404;
}
# Manifest, robots, favicon — short cache so SEO/PWA edits land
# within ~minute of a deploy.
location ~* \.(webmanifest|json|xml|txt|ico)$ {
expires 5m;
add_header Cache-Control "public, max-age=300";
try_files $uri =404;
}
# SPA fallback — every unknown route gets index.html so React
# Router resolves it.
location / {
try_files $uri $uri/ /index.html;
expires 5m;
add_header Cache-Control "public, max-age=300";
# Security headers — defense in depth ; HAProxy strips/adds
# them upstream in prod.
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header X-Frame-Options "DENY" always;
}
# Pre-compressed gzip assets if Vite emitted them
gzip_static on;
# Errors lean on /index.html so a deep link reload doesn't show
# nginx's default page.
error_page 404 /index.html;
error_page 500 502 503 504 /index.html;
}