chore(deploy): add manifest, systemd units, nginx config, deploy.sh
Wires up the prod deployment per architecture-doc conventions:
- api → nikola.kosherinata.internal, loopback bind 127.0.0.1:42424
(less-common port, registered with SELinux as http_port_t).
- worker → frootmig.kosherinata.internal, no listening port.
- web (static ui/dist + nginx server_name rob.tn) → nikola, with
/api/* reverse-proxied to the loopback API.
- db → existing magrathea cluster via mTLS, hostname-baked DATABASE_URL
rendered into /etc/moments/{api,worker}.env at deploy time.
Cert rotation: step-ca renews host certs every 24h; .path units watch
/etc/pki/tls/misc/<host>.pem and trigger systemctl restart of the
relevant service. Both binaries hold cert state in rustls and read
once at startup, so restart is the right reload semantics.
deploy.sh contract matches the architecture doc: positional env arg,
component list (or `all` / `default`), --dry-run support. Renders
config templates from `pass`, rsyncs over ssh+sudo, runs sysusers /
restorecon / semanage / systemctl / nginx -t idempotently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
9
asset/config/api.env.tmpl
Normal file
9
asset/config/api.env.tmpl
Normal file
@@ -0,0 +1,9 @@
|
||||
# /etc/moments/api.env — rendered by deploy.sh, do not edit on the host.
|
||||
# {{HOSTNAME}} resolves to the target host's FQDN at deploy time.
|
||||
|
||||
JOURNAL_STREAM=1
|
||||
RUST_LOG=info,sqlx=warn,tower_http=info
|
||||
|
||||
BIND_ADDR=127.0.0.1:42424
|
||||
|
||||
DATABASE_URL=postgres://moments_ro@magrathea.kosherinata.internal:5432/moments?sslmode=verify-full&sslrootcert=/etc/pki/ca-trust/source/anchors/root-internal.pem&sslcert=/etc/pki/tls/misc/{{HOSTNAME}}.pem&sslkey=/etc/pki/tls/private/{{HOSTNAME}}.pem
|
||||
27
asset/config/worker.env.tmpl
Normal file
27
asset/config/worker.env.tmpl
Normal file
@@ -0,0 +1,27 @@
|
||||
# /etc/moments/worker.env — rendered by deploy.sh, do not edit on the host.
|
||||
# {{HOSTNAME}} resolves to the target host's FQDN at deploy time.
|
||||
# {{GITHUB_TOKEN}} is resolved from `pass`; the rendered file lives in
|
||||
# /etc/moments/ chmod 0640 owned by root:moments.
|
||||
|
||||
JOURNAL_STREAM=1
|
||||
RUST_LOG=info,sqlx=warn
|
||||
|
||||
DATABASE_URL=postgres://moments_rw@magrathea.kosherinata.internal:5432/moments?sslmode=verify-full&sslrootcert=/etc/pki/ca-trust/source/anchors/root-internal.pem&sslcert=/etc/pki/tls/misc/{{HOSTNAME}}.pem&sslkey=/etc/pki/tls/private/{{HOSTNAME}}.pem
|
||||
|
||||
GITHUB_USER=grenade
|
||||
GITHUB_TOKEN={{GITHUB_TOKEN}}
|
||||
POLL_INTERVAL_SECS=600
|
||||
SEARCH_POLL_INTERVAL_SECS=86400
|
||||
|
||||
GITEA_HOST=git.lair.cafe
|
||||
GITEA_USER=grenade
|
||||
GITEA_POLL_INTERVAL_SECS=600
|
||||
|
||||
HG_HOST=hg-edge.mozilla.org
|
||||
HG_REPOS=build/puppet,build/tools,build/buildbot-configs
|
||||
HG_AUTHOR_TERMS=thijssen,grenade
|
||||
HG_POLL_INTERVAL_SECS=86400
|
||||
|
||||
BUGZILLA_HOST=bugzilla.mozilla.org
|
||||
BUGZILLA_EMAIL=rthijssen@mozilla.com
|
||||
BUGZILLA_POLL_INTERVAL_SECS=86400
|
||||
36
asset/manifest.yml
Normal file
36
asset/manifest.yml
Normal file
@@ -0,0 +1,36 @@
|
||||
app: moments
|
||||
environments:
|
||||
prod:
|
||||
components:
|
||||
api:
|
||||
hosts: [nikola.kosherinata.internal]
|
||||
config:
|
||||
bind: 127.0.0.1:42424
|
||||
db_role: moments_ro
|
||||
db_host: magrathea.kosherinata.internal
|
||||
db_port: 5432
|
||||
db_name: moments
|
||||
worker:
|
||||
hosts: [frootmig.kosherinata.internal]
|
||||
config:
|
||||
db_role: moments_rw
|
||||
db_host: magrathea.kosherinata.internal
|
||||
db_port: 5432
|
||||
db_name: moments
|
||||
github_user: grenade
|
||||
gitea_host: git.lair.cafe
|
||||
gitea_user: grenade
|
||||
hg_host: hg-edge.mozilla.org
|
||||
hg_repos: build/puppet,build/tools,build/buildbot-configs
|
||||
hg_author_terms: thijssen,grenade
|
||||
bugzilla_host: bugzilla.mozilla.org
|
||||
bugzilla_email: rthijssen@mozilla.com
|
||||
secrets:
|
||||
GITHUB_TOKEN: github.com/grenade/admin-token
|
||||
# GITEA_TOKEN, BUGZILLA_API_KEY: optional, omit unless required.
|
||||
web:
|
||||
hosts: [nikola.kosherinata.internal]
|
||||
config:
|
||||
server_name: rob.tn
|
||||
root: /var/www/moments
|
||||
api_upstream: http://127.0.0.1:42424
|
||||
65
asset/nginx/rob.tn.conf
Normal file
65
asset/nginx/rob.tn.conf
Normal file
@@ -0,0 +1,65 @@
|
||||
# /etc/nginx/conf.d/rob.tn.conf — rob.tn site config for moments.
|
||||
#
|
||||
# Static frontend out of /var/www/moments; /api/* reverse-proxied to the
|
||||
# moments-api binary on loopback. The UI fetches /api/v1/... so the strip
|
||||
# matches what Vite's dev proxy does (drop the /api prefix before sending
|
||||
# to axum, whose routes are mounted at /v1/*).
|
||||
|
||||
upstream moments_api {
|
||||
server 127.0.0.1:42424 max_fails=3 fail_timeout=30s;
|
||||
keepalive 8;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name rob.tn;
|
||||
|
||||
ssl_certificate /etc/pki/tls/misc/nikola.kosherinata.internal.pem;
|
||||
ssl_certificate_key /etc/pki/tls/private/nikola.kosherinata.internal.pem;
|
||||
|
||||
# Public forge — visitors are not on the internal mTLS mesh, so no
|
||||
# client-cert verification here. The X25519MLKEM768 default falls
|
||||
# back to classical curves for clients that don't speak PQ yet.
|
||||
ssl_protocols TLSv1.3;
|
||||
|
||||
root /var/www/moments;
|
||||
index index.html;
|
||||
|
||||
# Static SPA: serve the file if it exists, else fall back to index.html
|
||||
# so client-side routing works.
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-cache" always;
|
||||
}
|
||||
|
||||
# Asset bundles are content-hashed by Vite — safe to cache aggressively.
|
||||
location ~* \.(js|css|woff2?|ttf|eot|svg|png|jpg|jpeg|gif|ico|webp|avif)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, max-age=2592000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
# Strip /api so axum sees /v1/events, not /api/v1/events.
|
||||
rewrite ^/api/(.*)$ /$1 break;
|
||||
proxy_pass http://moments_api;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Connection "";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 30s;
|
||||
proxy_connect_timeout 5s;
|
||||
}
|
||||
|
||||
access_log /var/log/nginx/rob.tn.access.log;
|
||||
error_log /var/log/nginx/rob.tn.error.log;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name rob.tn;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
6
asset/systemd/moments-api-cert-reload.service
Normal file
6
asset/systemd/moments-api-cert-reload.service
Normal file
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
Description=Restart moments-api on host cert change
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/systemctl restart moments-api.service
|
||||
13
asset/systemd/moments-api-cert.path
Normal file
13
asset/systemd/moments-api-cert.path
Normal file
@@ -0,0 +1,13 @@
|
||||
[Unit]
|
||||
Description=Watch host cert for moments-api
|
||||
Documentation=https://git.lair.cafe/grenade/architecture
|
||||
|
||||
[Path]
|
||||
# Hostname is substituted at deploy time. step-ca rotates host certs every
|
||||
# 24h; rustls reads them at process start, so the API must restart on
|
||||
# rotation. Read-only public timeline — a few seconds of churn is fine.
|
||||
PathChanged=/etc/pki/tls/misc/{{HOSTNAME}}.pem
|
||||
Unit=moments-api-cert-reload.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
34
asset/systemd/moments-api.service
Normal file
34
asset/systemd/moments-api.service
Normal file
@@ -0,0 +1,34 @@
|
||||
[Unit]
|
||||
Description=moments read-only HTTP API
|
||||
Documentation=https://git.lair.cafe/grenade/moments
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=moments
|
||||
Group=moments
|
||||
EnvironmentFile=/etc/moments/api.env
|
||||
ExecStart=/usr/local/bin/moments-api
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
# Hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
SystemCallArchitectures=native
|
||||
ReadWritePaths=/var/lib/moments
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
6
asset/systemd/moments-worker-cert-reload.service
Normal file
6
asset/systemd/moments-worker-cert-reload.service
Normal file
@@ -0,0 +1,6 @@
|
||||
[Unit]
|
||||
Description=Restart moments-worker on host cert change
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/bin/systemctl restart moments-worker.service
|
||||
12
asset/systemd/moments-worker-cert.path
Normal file
12
asset/systemd/moments-worker-cert.path
Normal file
@@ -0,0 +1,12 @@
|
||||
[Unit]
|
||||
Description=Watch host cert for moments-worker
|
||||
Documentation=https://git.lair.cafe/grenade/architecture
|
||||
|
||||
[Path]
|
||||
# Worker holds a sqlx pool with rustls — restart on cert rotation. The
|
||||
# poller is idempotent, so dropping mid-poll is safe.
|
||||
PathChanged=/etc/pki/tls/misc/{{HOSTNAME}}.pem
|
||||
Unit=moments-worker-cert-reload.service
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
34
asset/systemd/moments-worker.service
Normal file
34
asset/systemd/moments-worker.service
Normal file
@@ -0,0 +1,34 @@
|
||||
[Unit]
|
||||
Description=moments ingestion worker
|
||||
Documentation=https://git.lair.cafe/grenade/moments
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=moments
|
||||
Group=moments
|
||||
EnvironmentFile=/etc/moments/worker.env
|
||||
ExecStart=/usr/local/bin/moments-worker
|
||||
Restart=on-failure
|
||||
RestartSec=10s
|
||||
|
||||
# Hardening
|
||||
NoNewPrivileges=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
PrivateTmp=true
|
||||
PrivateDevices=true
|
||||
ProtectKernelTunables=true
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
RestrictRealtime=true
|
||||
RestrictSUIDSGID=true
|
||||
LockPersonality=true
|
||||
MemoryDenyWriteExecute=true
|
||||
SystemCallArchitectures=native
|
||||
ReadWritePaths=/var/lib/moments
|
||||
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
2
asset/systemd/moments.sysusers.conf
Normal file
2
asset/systemd/moments.sysusers.conf
Normal file
@@ -0,0 +1,2 @@
|
||||
#Type Name ID GECOS Home directory Shell
|
||||
u moments - "moments service account" /var/lib/moments /usr/sbin/nologin
|
||||
Reference in New Issue
Block a user