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:
parent
3123f26fd4
commit
5759143e97
2 changed files with 111 additions and 7 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in a new issue