Add Gandi DNS management via Pulumi #54

Merged
eblume merged 2 commits from feature/gandi-dns-pulumi into main 2026-01-25 08:15:46 -08:00
Owner

Summary

  • Restructure Pulumi into separate projects: pulumi/tailscale/ and pulumi/gandi/
  • Add Gandi LiveDNS management for eblu.me domain
  • Create wildcard DNS record *.ops.eblu.me → indri's Tailscale IP (100.98.163.89)
  • Add mise tasks: dns-up, dns-preview
  • Update tailnet-up to pass --yes by default
  • Document PAT cycling process (expires every 30 days)

Background

This enables using real DNS names (*.ops.eblu.me) that resolve to Tailscale IPs,
which allows containers and other systems to resolve services without depending on
MagicDNS. Since Tailscale IPs (100.x.x.x) are not publicly routable, services remain
tailnet-only while using standard DNS.

Deployment and Testing

  • Run cd pulumi/gandi && uv sync to install dependencies
  • Run cd pulumi/gandi && pulumi stack init eblu-me to create stack
  • Run mise run dns-preview to verify configuration
  • Run mise run dns-up to apply DNS records
  • Verify with dig +short test.ops.eblu.me returns 100.98.163.89

🤖 Generated with Claude Code

## Summary - Restructure Pulumi into separate projects: `pulumi/tailscale/` and `pulumi/gandi/` - Add Gandi LiveDNS management for `eblu.me` domain - Create wildcard DNS record `*.ops.eblu.me` → indri's Tailscale IP (100.98.163.89) - Add mise tasks: `dns-up`, `dns-preview` - Update `tailnet-up` to pass `--yes` by default - Document PAT cycling process (expires every 30 days) ## Background This enables using real DNS names (`*.ops.eblu.me`) that resolve to Tailscale IPs, which allows containers and other systems to resolve services without depending on MagicDNS. Since Tailscale IPs (100.x.x.x) are not publicly routable, services remain tailnet-only while using standard DNS. ## Deployment and Testing - [ ] Run `cd pulumi/gandi && uv sync` to install dependencies - [ ] Run `cd pulumi/gandi && pulumi stack init eblu-me` to create stack - [ ] Run `mise run dns-preview` to verify configuration - [ ] Run `mise run dns-up` to apply DNS records - [ ] Verify with `dig +short test.ops.eblu.me` returns `100.98.163.89` 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Move Tailscale ACL management to pulumi/tailscale/
- Add new Gandi DNS project at pulumi/gandi/ for eblu.me management
- Create wildcard DNS record *.ops.eblu.me pointing to indri's Tailscale IP
- Add mise tasks: dns-up, dns-preview
- Update tailnet-up/preview to use new path and add --yes flag
- Document PAT cycling process (expires every 30 days)

This enables using real DNS names (*.ops.eblu.me) that resolve to Tailscale
IPs, allowing containers to resolve services without MagicDNS dependency.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@ -0,0 +3,4 @@
blumeops-dns:domain: eblu.me
blumeops-dns:subdomain: ops
# indri's Tailscale IP - only routable within tailnet
blumeops-dns:tailscale_ip: "100.98.163.89"
Author
Owner

Is there a way to have pulumi determine this ip dynamically at up-time? Even better; is there a way to set this so that it doesn't need to route to a static ip that might change from tailscale? Either way, instead of just calling it indri's tailscale ip, call it "indri (reverse proxy via caddy)'s Tailscale IP" to make it clear that we're only targeting indri because it hosts our reverse proxy.

Is there a way to have pulumi determine this ip dynamically at up-time? Even better; is there a way to set this so that it doesn't need to route to a static ip that might change from tailscale? Either way, instead of just calling it indri's tailscale ip, call it "indri (reverse proxy via caddy)'s Tailscale IP" to make it clear that we're only targeting indri because it hosts our reverse proxy.
@ -0,0 +4,4 @@
## What It Does
Creates DNS records that point `*.ops.eblu.me` to indri's Tailscale IP (`100.98.163.89`).
Author
Owner

similarly here, make sure we call out that the reason we are targeting indri is because it's hosting a reverse proxy (caddy is the plan). The day may come where we host it on a different host.

similarly here, make sure we call out that the reason we are targeting indri is because it's hosting a reverse proxy (caddy is the plan). The day may come where we host it on a different host.
@ -0,0 +31,4 @@
2. Create a new PAT:
- Name: `blumeops-pulumi` (or similar)
- Expiration: 30 days (maximum)
- Permissions: **Manage domain name technical configurations** (under Domains)
Author
Owner

I've also enabled the following:

  • See & download SSL certificates
  • Manage domain name technical configurations
  • See and renew domain names
  • Manage Cloud resources
  • See Cloud resources
  • View Organization
  • Deploy Web Hosting instances
  • Manage Web Hosting instances
  • See and renew Web Hosting instances

(If any of those were a mistake to include for security reasons feel free to remove them from this README)

I've also enabled the following: - See & download SSL certificates - Manage domain name technical configurations - See and renew domain names - Manage Cloud resources - See Cloud resources - View Organization - Deploy Web Hosting instances - Manage Web Hosting instances - See and renew Web Hosting instances (If any of those were a mistake to include for security reasons feel free to remove them from this README)
@ -0,0 +82,4 @@
## Changing the Target IP
If indri's Tailscale IP changes, update `Pulumi.eblu-me.yaml`:
Author
Owner

See the previous comment about maybe making this dynamic

See the previous comment about maybe making this dynamic
@ -0,0 +17,4 @@
config = pulumi.Config()
domain = config.require("domain") # eblu.me
subdomain = config.require("subdomain") # ops
tailscale_ip = config.require("tailscale_ip") # 100.98.163.89
Author
Owner

See previous comments about maybe making this dynamic

See previous comments about maybe making this dynamic
- Resolve indri's Tailscale IP dynamically via MagicDNS at deploy time
- Add BLUMEOPS_REVERSE_PROXY_IP env var override for break-glass scenarios
- Remove hardcoded IP from stack config
- Clarify that indri hosts Caddy (the reverse proxy) in all docs
- Update PAT permissions list with actual permissions enabled

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
eblume merged commit b08faa50cc into main 2026-01-25 08:15:46 -08:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
eblume/blumeops!54
No description provided.