Expose CV publicly at cv.eblu.me (#173)
All checks were successful
Deploy Fly.io Proxy / deploy (push) Successful in 1m57s

## Summary
- Add nginx server block for `cv.eblu.me` (static site, same pattern as docs)
- Add DNS CNAME record in Pulumi (`cv.eblu.me` → `blumeops-proxy.fly.dev`)
- Add `cv.eblu.me` cert to `fly-setup` mise task
- Tag CV Tailscale ingress with `tag:flyio-target` for ACL access
- Remove `/_error` test endpoint from docs proxy

## Deployment and Testing
- [ ] `argocd app set cv --revision cv/public-cv-eblu-me && argocd app sync cv`
- [ ] `fly certs add cv.eblu.me -a blumeops-proxy`
- [ ] `mise run fly-deploy`
- [ ] Verify proxy: `curl -I -H "Host: cv.eblu.me" https://blumeops-proxy.fly.dev/`
- [ ] `mise run dns-preview` then `mise run dns-up`
- [ ] Verify live: `curl -I https://cv.eblu.me`
- [ ] Merge, then `argocd app set cv --revision main && argocd app sync cv`

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/173
This commit is contained in:
Erich Blume 2026-02-12 14:05:00 -08:00
commit df372fccb6
8 changed files with 309 additions and 7 deletions

View file

@ -7,7 +7,7 @@ metadata:
annotations: annotations:
tailscale.com/proxy-class: "default" tailscale.com/proxy-class: "default"
tailscale.com/proxy-group: "ingress" tailscale.com/proxy-group: "ingress"
tailscale.com/tags: "tag:k8s" tailscale.com/tags: "tag:k8s,tag:flyio-target"
gethomepage.dev/enabled: "true" gethomepage.dev/enabled: "true"
gethomepage.dev/name: "CV" gethomepage.dev/name: "CV"
gethomepage.dev/group: "Apps" gethomepage.dev/group: "Apps"

View file

@ -0,0 +1,262 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: grafana-dashboard-cv-apm
namespace: monitoring
labels:
grafana_dashboard: "1"
data:
cv-apm.json: |
{
"annotations": { "list": [] },
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 1,
"id": null,
"links": [],
"panels": [
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "req/s",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "normal" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "reqps"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 16, "x": 0, "y": 0 },
"id": 1,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"targets": [
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "sum by (status) (rate(flyio_nginx_http_requests_total{host=\"cv.eblu.me\"}[5m]))", "legendFormat": "{{status}}", "refId": "A" }
],
"title": "Request Rate by Status",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 0.01 }, { "color": "red", "value": 0.05 }] },
"unit": "percentunit"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 8, "x": 16, "y": 0 },
"id": 2,
"options": {
"colorMode": "background",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "sum(rate(flyio_nginx_http_requests_total{host=\"cv.eblu.me\",status=~\"5..\"}[5m])) / sum(rate(flyio_nginx_http_requests_total{host=\"cv.eblu.me\"}[5m]))", "refId": "A" }
],
"title": "Error Rate (5xx)",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "red", "value": null }, { "color": "yellow", "value": 0.5 }, { "color": "green", "value": 0.8 }] },
"unit": "percentunit"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 16, "y": 4 },
"id": 3,
"options": {
"colorMode": "background",
"graphMode": "none",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "sum(rate(flyio_nginx_cache_requests_total{host=\"cv.eblu.me\",cache_status=\"HIT\"}[5m])) / sum(rate(flyio_nginx_cache_requests_total{host=\"cv.eblu.me\"}[5m]))", "refId": "A" }
],
"title": "Cache Hit Ratio",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "thresholds" },
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "reqps"
},
"overrides": []
},
"gridPos": { "h": 4, "w": 4, "x": 20, "y": 4 },
"id": 4,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "center",
"orientation": "auto",
"reduceOptions": { "calcs": ["lastNotNull"], "fields": "", "values": false },
"textMode": "auto"
},
"targets": [
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "sum(rate(flyio_nginx_http_requests_total{host=\"cv.eblu.me\"}[5m]))", "refId": "A" }
],
"title": "Current RPS",
"type": "stat"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "seconds",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 10,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "s"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
"id": 5,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "multi", "sort": "desc" }
},
"targets": [
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.50, sum by (le) (rate(flyio_nginx_http_request_duration_seconds_bucket{host=\"cv.eblu.me\"}[5m])))", "legendFormat": "p50", "refId": "A" },
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.90, sum by (le) (rate(flyio_nginx_http_request_duration_seconds_bucket{host=\"cv.eblu.me\"}[5m])))", "legendFormat": "p90", "refId": "B" },
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "histogram_quantile(0.99, sum by (le) (rate(flyio_nginx_http_request_duration_seconds_bucket{host=\"cv.eblu.me\"}[5m])))", "legendFormat": "p99", "refId": "C" }
],
"title": "Latency Percentiles",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisBorderShow": false,
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 20,
"gradientMode": "none",
"hideFrom": { "legend": false, "tooltip": false, "viz": false },
"insertNulls": false,
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": { "type": "linear" },
"showPoints": "never",
"spanNulls": false,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "Bps"
},
"overrides": []
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
"id": 6,
"options": {
"legend": { "calcs": ["mean", "max"], "displayMode": "table", "placement": "right", "showLegend": true },
"tooltip": { "mode": "single", "sort": "none" }
},
"targets": [
{ "datasource": { "type": "prometheus", "uid": "prometheus" }, "expr": "sum(rate(flyio_nginx_http_response_bytes_total{host=\"cv.eblu.me\"}[5m]))", "legendFormat": "Bandwidth", "refId": "A" }
],
"title": "Bandwidth",
"type": "timeseries"
},
{
"datasource": { "type": "loki", "uid": "loki" },
"gridPos": { "h": 8, "w": 24, "x": 0, "y": 16 },
"id": 7,
"options": {
"dedupStrategy": "none",
"enableLogDetails": true,
"prettifyLogMessage": false,
"showCommonLabels": false,
"showLabels": false,
"showTime": true,
"sortOrder": "Descending",
"wrapLogMessage": false
},
"targets": [
{ "datasource": { "type": "loki", "uid": "loki" }, "expr": "{instance=\"flyio-proxy\", job=\"flyio-nginx\"} |= \"cv.eblu.me\" | json | line_format \"{{.client_ip}} {{.request_method}} {{.request_uri}} {{.status}} cache={{.upstream_cache_status}} {{.request_time}}s\"", "refId": "A" }
],
"title": "Recent Access Logs",
"type": "logs"
}
],
"refresh": "30s",
"schemaVersion": 38,
"tags": ["cv", "flyio", "apm"],
"templating": { "list": [] },
"time": { "from": "now-6h", "to": "now" },
"timepicker": {},
"timezone": "",
"title": "CV APM",
"uid": "cv-apm",
"version": 1,
"weekStart": ""
}

View file

@ -17,6 +17,7 @@ resources:
- dashboards/configmap-postgresql.yaml - dashboards/configmap-postgresql.yaml
- dashboards/configmap-services.yaml - dashboards/configmap-services.yaml
- dashboards/configmap-zot.yaml - dashboards/configmap-zot.yaml
- dashboards/configmap-cv-apm.yaml
- dashboards/configmap-docs-apm.yaml - dashboards/configmap-docs-apm.yaml
- dashboards/configmap-flyio.yaml - dashboards/configmap-flyio.yaml
- dashboards/configmap-sifaka-disks.yaml - dashboards/configmap-sifaka-disks.yaml

View file

@ -0,0 +1 @@
Expose CV service publicly at cv.eblu.me via Fly.io proxy.

View file

@ -0,0 +1 @@
Remove `/_error` test endpoint from Fly.io nginx proxy.

View file

@ -54,11 +54,6 @@ http {
root /usr/share/nginx/html; root /usr/share/nginx/html;
internal; internal;
} }
location = /_error {
root /usr/share/nginx/html;
try_files /error.html =404;
}
location / { location / {
set $upstream_docs https://docs.tail8d86e.ts.net; set $upstream_docs https://docs.tail8d86e.ts.net;
proxy_pass $upstream_docs$request_uri; proxy_pass $upstream_docs$request_uri;
@ -85,6 +80,38 @@ http {
} }
# --- cv.eblu.me (static site) ---
server {
listen 8080;
server_name cv.eblu.me;
limit_req zone=general burst=20 nodelay;
error_page 502 503 504 /error.html;
location = /error.html {
root /usr/share/nginx/html;
internal;
}
location / {
set $upstream_cv https://cv.tail8d86e.ts.net;
proxy_pass $upstream_cv$request_uri;
proxy_ssl_verify off;
proxy_ssl_server_name on;
proxy_intercept_errors on;
proxy_cache services;
proxy_cache_valid 200 1d;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating;
proxy_cache_lock on;
proxy_cache_key $host$uri;
proxy_ignore_headers Cache-Control Set-Cookie;
add_header X-Cache-Status $upstream_cache_status;
}
}
# Catch-all: reject unknown hosts, but serve health check # Catch-all: reject unknown hosts, but serve health check
server { server {
listen 8080 default_server; listen 8080 default_server;

View file

@ -21,7 +21,7 @@ echo "IPs allocated"
# Add certs for all public domains (idempotent — fly ignores duplicates) # Add certs for all public domains (idempotent — fly ignores duplicates)
fly certs add docs.eblu.me -a "$APP" 2>/dev/null || true fly certs add docs.eblu.me -a "$APP" 2>/dev/null || true
# fly certs add wiki.eblu.me -a "$APP" 2>/dev/null || true # future services fly certs add cv.eblu.me -a "$APP" 2>/dev/null || true
echo "Certificates configured" echo "Certificates configured"
echo "Done. Run 'mise run fly-deploy' to deploy." echo "Done. Run 'mise run fly-deploy' to deploy."

View file

@ -67,9 +67,19 @@ docs_public = gandi.livedns.Record(
values=["blumeops-proxy.fly.dev."], values=["blumeops-proxy.fly.dev."],
) )
cv_public = gandi.livedns.Record(
"cv-public",
zone=domain,
name="cv",
type="CNAME",
ttl=300,
values=["blumeops-proxy.fly.dev."],
)
# ============== Exports ============== # ============== Exports ==============
pulumi.export("domain", domain) pulumi.export("domain", domain)
pulumi.export("wildcard_fqdn", f"*.{subdomain}.{domain}") pulumi.export("wildcard_fqdn", f"*.{subdomain}.{domain}")
pulumi.export("base_fqdn", f"{subdomain}.{domain}") pulumi.export("base_fqdn", f"{subdomain}.{domain}")
pulumi.export("target_ip", tailscale_ip) pulumi.export("target_ip", tailscale_ip)
pulumi.export("docs_public_fqdn", f"docs.{domain}") pulumi.export("docs_public_fqdn", f"docs.{domain}")
pulumi.export("cv_public_fqdn", f"cv.{domain}")