## 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) Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/54
61 lines
1.9 KiB
Python
61 lines
1.9 KiB
Python
"""Pulumi program to manage eblu.me DNS via Gandi LiveDNS.
|
|
|
|
This program manages DNS records for blumeops infrastructure:
|
|
- Wildcard record for *.ops.eblu.me pointing to indri's Tailscale IP
|
|
- indri hosts Caddy as the reverse proxy for all services
|
|
- This allows services to be accessed via real DNS names while remaining
|
|
tailnet-only (Tailscale IPs are not publicly routable)
|
|
|
|
Authentication:
|
|
Set GANDI_PERSONAL_ACCESS_TOKEN environment variable.
|
|
See README.md for PAT management instructions.
|
|
"""
|
|
|
|
import os
|
|
import socket
|
|
|
|
import pulumi
|
|
import pulumiverse_gandi as gandi
|
|
|
|
# Get configuration
|
|
config = pulumi.Config()
|
|
domain = config.require("domain") # eblu.me
|
|
subdomain = config.require("subdomain") # ops
|
|
|
|
# Resolve indri's Tailscale IP dynamically via MagicDNS
|
|
# This script runs on the tailnet, so we can resolve the hostname directly.
|
|
# indri hosts Caddy, which reverse-proxies all services.
|
|
# Break-glass: set BLUMEOPS_REVERSE_PROXY_IP env var to override DNS resolution
|
|
REVERSE_PROXY_HOST = "indri.tail8d86e.ts.net"
|
|
tailscale_ip = os.environ.get("BLUMEOPS_REVERSE_PROXY_IP") or socket.gethostbyname(
|
|
REVERSE_PROXY_HOST
|
|
)
|
|
|
|
# Wildcard A record for *.ops.eblu.me
|
|
# Points to indri's Tailscale IP, which is only routable within the tailnet.
|
|
# This allows containers and other systems to resolve real DNS names
|
|
# while keeping services private to the tailnet.
|
|
wildcard_record = gandi.livedns.Record(
|
|
"ops-wildcard",
|
|
zone=domain,
|
|
name=f"*.{subdomain}",
|
|
type="A",
|
|
ttl=300,
|
|
values=[tailscale_ip],
|
|
)
|
|
|
|
# Base subdomain record (ops.eblu.me) - same IP
|
|
base_record = gandi.livedns.Record(
|
|
"ops-base",
|
|
zone=domain,
|
|
name=subdomain,
|
|
type="A",
|
|
ttl=300,
|
|
values=[tailscale_ip],
|
|
)
|
|
|
|
# ============== Exports ==============
|
|
pulumi.export("domain", domain)
|
|
pulumi.export("wildcard_fqdn", f"*.{subdomain}.{domain}")
|
|
pulumi.export("base_fqdn", f"{subdomain}.{domain}")
|
|
pulumi.export("target_ip", tailscale_ip)
|