Restructure Pulumi into separate projects for Tailscale and Gandi DNS
- 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>
This commit is contained in:
parent
af3536bc17
commit
cdeda4856f
16 changed files with 183 additions and 3 deletions
10
mise-tasks/dns-preview
Executable file
10
mise-tasks/dns-preview
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
#MISE description="Preview DNS changes to eblu.me with Pulumi"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GANDI_PERSONAL_ACCESS_TOKEN=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get mco6ka3dc3rmw7zkg2dhia5d2m --fields pat --reveal)
|
||||
export GANDI_PERSONAL_ACCESS_TOKEN
|
||||
|
||||
cd "$(dirname "$0")/../pulumi/gandi"
|
||||
pulumi preview "$@"
|
||||
10
mise-tasks/dns-up
Executable file
10
mise-tasks/dns-up
Executable file
|
|
@ -0,0 +1,10 @@
|
|||
#!/usr/bin/env bash
|
||||
#MISE description="Apply DNS changes to eblu.me with Pulumi"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
GANDI_PERSONAL_ACCESS_TOKEN=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get mco6ka3dc3rmw7zkg2dhia5d2m --fields pat --reveal)
|
||||
export GANDI_PERSONAL_ACCESS_TOKEN
|
||||
|
||||
cd "$(dirname "$0")/../pulumi/gandi"
|
||||
pulumi up --yes "$@"
|
||||
|
|
@ -9,5 +9,5 @@ TAILSCALE_OAUTH_CLIENT_SECRET=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w
|
|||
export TAILSCALE_OAUTH_CLIENT_SECRET
|
||||
export TAILSCALE_TAILNET="tail8d86e.ts.net"
|
||||
|
||||
cd "$(dirname "$0")/../pulumi"
|
||||
cd "$(dirname "$0")/../pulumi/tailscale"
|
||||
pulumi preview "$@"
|
||||
|
|
|
|||
|
|
@ -9,5 +9,5 @@ TAILSCALE_OAUTH_CLIENT_SECRET=$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get w
|
|||
export TAILSCALE_OAUTH_CLIENT_SECRET
|
||||
export TAILSCALE_TAILNET="tail8d86e.ts.net"
|
||||
|
||||
cd "$(dirname "$0")/../pulumi"
|
||||
pulumi up "$@"
|
||||
cd "$(dirname "$0")/../pulumi/tailscale"
|
||||
pulumi up --yes "$@"
|
||||
|
|
|
|||
1
pulumi/gandi/.gitignore
vendored
Normal file
1
pulumi/gandi/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
.venv/
|
||||
6
pulumi/gandi/Pulumi.eblu-me.yaml
Normal file
6
pulumi/gandi/Pulumi.eblu-me.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
---
|
||||
config:
|
||||
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"
|
||||
7
pulumi/gandi/Pulumi.yaml
Normal file
7
pulumi/gandi/Pulumi.yaml
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
name: blumeops-dns
|
||||
runtime:
|
||||
name: python
|
||||
options:
|
||||
toolchain: uv
|
||||
description: DNS configuration for eblu.me via Gandi LiveDNS
|
||||
92
pulumi/gandi/README.md
Normal file
92
pulumi/gandi/README.md
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
# 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 (`100.98.163.89`).
|
||||
|
||||
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.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
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](https://admin.gandi.net/organizations/1db8d76a-f729-11ed-b8d1-00163e94b645/account/pat)
|
||||
|
||||
2. Create a new PAT:
|
||||
- Name: `blumeops-pulumi` (or similar)
|
||||
- Expiration: 30 days (maximum)
|
||||
- Permissions: **Manage domain name technical configurations** (under Domains)
|
||||
|
||||
3. Update 1Password:
|
||||
```bash
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
mise run dns-up # Preview and apply changes
|
||||
mise run dns-preview # Preview only
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
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 | 100.98.163.89 | Wildcard for all services |
|
||||
| `ops.eblu.me` | A | 100.98.163.89 | Base subdomain |
|
||||
|
||||
## Service Hostnames
|
||||
|
||||
Once Caddy is configured, 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
|
||||
|
||||
## Changing the Target IP
|
||||
|
||||
If indri's Tailscale IP changes, update `Pulumi.eblu-me.yaml`:
|
||||
|
||||
```yaml
|
||||
config:
|
||||
blumeops-dns:tailscale_ip: "NEW_IP_HERE"
|
||||
```
|
||||
|
||||
Then run `mise run dns-up` to apply.
|
||||
49
pulumi/gandi/__main__.py
Normal file
49
pulumi/gandi/__main__.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
"""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
|
||||
- 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 pulumi
|
||||
import pulumiverse_gandi as gandi
|
||||
|
||||
# Get configuration
|
||||
config = pulumi.Config()
|
||||
domain = config.require("domain") # eblu.me
|
||||
subdomain = config.require("subdomain") # ops
|
||||
tailscale_ip = config.require("tailscale_ip") # 100.98.163.89
|
||||
|
||||
# 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)
|
||||
5
pulumi/gandi/pyproject.toml
Normal file
5
pulumi/gandi/pyproject.toml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
[project]
|
||||
name = "blumeops-dns"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.11"
|
||||
dependencies = ["pulumi>=3.0.0", "pulumiverse-gandi>=2.3.0"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue