blumeops/pulumi/gandi
Erich Blume 64a78422b1
Some checks failed
Deploy Fly.io Proxy / deploy (push) Failing after 9s
Add Fly.io public reverse proxy for docs.eblu.me (#120)
## Summary

- Adds a Fly.io reverse proxy (`blumeops-proxy`) that tunnels public traffic to homelab services over Tailscale
- First service exposed: `docs.eblu.me` — the Quartz static docs site
- Includes Pulumi IaC for Tailscale auth key/ACLs and Gandi DNS CNAME
- Adds mise tasks (`fly-deploy`, `fly-setup`, `fly-shutoff`) and Forgejo CI workflow

## Key details

- Fly.io Firecracker VMs support TUN devices natively — no userspace networking needed
- Tailscale auth key is `preauthorized=True` to avoid device approval hangs on container restarts
- nginx caches aggressively for the static site; health check is on the default_server block
- ACLs restrict `tag:flyio-proxy` to `tag:k8s` on port 443 only
- DNS CNAME deployed and verified: `docs.eblu.me` → `blumeops-proxy.fly.dev`

## Test plan

- [x] `curl -sf https://blumeops-proxy.fly.dev/healthz` returns `ok`
- [x] `curl -I -H "Host: docs.eblu.me" https://blumeops-proxy.fly.dev/` returns 200 with `X-Cache-Status`
- [x] `curl -I https://docs.eblu.me/` returns 200 with valid Let's Encrypt cert
- [x] `dig forge.ops.eblu.me` still resolves to 100.98.163.89 (private services unaffected)
- [x] Set `FLY_DEPLOY_TOKEN` Forgejo Actions secret for CI auto-deploy

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/120
2026-02-08 02:36:19 -08:00
..
.gitignore Add Gandi DNS management via Pulumi (#54) 2026-01-25 08:15:46 -08:00
__main__.py Add Fly.io public reverse proxy for docs.eblu.me (#120) 2026-02-08 02:36:19 -08:00
Pulumi.eblu-me.yaml Add Gandi DNS management via Pulumi (#54) 2026-01-25 08:15:46 -08:00
Pulumi.yaml Add Gandi DNS management via Pulumi (#54) 2026-01-25 08:15:46 -08:00
pyproject.toml Add Gandi DNS management via Pulumi (#54) 2026-01-25 08:15:46 -08:00
README.md Add Gandi DNS docs and rewrite homepage intro (#115) 2026-02-07 21:02:10 -08:00
uv.lock Add Fly.io public reverse proxy for docs.eblu.me (#120) 2026-02-08 02:36:19 -08:00

Gandi DNS Management

This Pulumi project manages DNS records for eblu.me via Gandi LiveDNS.

What It Does

Creates DNS records that point *.ops.eblu.me to indri's Tailscale IP.

Why indri? indri hosts Caddy, the reverse proxy for all blumeops services. All *.ops.eblu.me requests route through Caddy, which proxies to the appropriate backend service (either on indri itself or in the k8s cluster).

Since Tailscale IPs (100.x.x.x) are not routable on the public internet, these DNS records effectively make services accessible only from within the tailnet, while still using real, resolvable DNS names.

The target IP is resolved dynamically from indri.tail8d86e.ts.net at deploy time, so if indri's Tailscale IP changes, just re-run the deployment.

Setup

cd pulumi/gandi
uv sync
pulumi stack select eblu-me  # or: pulumi stack init eblu-me

Authentication

This project requires a Gandi Personal Access Token (PAT) with LiveDNS permissions.

The PAT expires every 30 days and must be cycled manually.

Cycling the PAT

  1. Go to Gandi PAT Management

  2. Create a new PAT:

    • Name: blumeops-pulumi (or similar)
    • Expiration: 30 days (maximum is 90; shorter is fine if used rarely)
    • Permissions required:
      • Manage domain name technical configurations (required for DNS records)
      • See and renew domain names
    • Optional permissions (enabled but not strictly required):
      • See & download SSL certificates
      • Manage Cloud resources
      • See Cloud resources
      • View Organization
      • Deploy Web Hosting instances
      • Manage Web Hosting instances
      • See and renew Web Hosting instances
  3. Update 1Password:

    # Update the existing item with the new PAT value
    op item edit mco6ka3dc3rmw7zkg2dhia5d2m pat="<NEW_PAT_VALUE>" --vault vg6xf6vvfmoh5hqjjhlhbeoaie
    
  4. Delete the old PAT from Gandi admin console

Running with Authentication

The mise task handles fetching the PAT from 1Password:

mise run dns-up        # Preview and apply changes
mise run dns-preview   # Preview only

Or manually:

export GANDI_PERSONAL_ACCESS_TOKEN=$(op item get mco6ka3dc3rmw7zkg2dhia5d2m --field pat --reveal --vault vg6xf6vvfmoh5hqjjhlhbeoaie)
pulumi up

DNS Records Created

Record Type Value Purpose
*.ops.eblu.me A (indri's Tailscale IP) Wildcard for all services
ops.eblu.me A (indri's Tailscale IP) Base subdomain

Service Hostnames

Once Caddy is configured on indri, services will be accessible at:

  • forge.ops.eblu.me - Forgejo git server
  • registry.ops.eblu.me - Zot container registry
  • grafana.ops.eblu.me - Grafana dashboards
  • argocd.ops.eblu.me - ArgoCD
  • feed.ops.eblu.me - Miniflux RSS reader
  • pypi.ops.eblu.me - DevPI Python index
  • kiwix.ops.eblu.me - Kiwix offline content
  • tesla.ops.eblu.me - TeslaMate
  • torrent.ops.eblu.me - Transmission
  • prometheus.ops.eblu.me - Prometheus metrics
  • loki.ops.eblu.me - Loki logs