Add Pulumi for tailnet IaC management (#15)

## Summary
- Manage tail8d86e.ts.net ACLs, tags, and DNS via Pulumi + Python
- State stored in Pulumi Cloud (free tier) to avoid circular dependency
- OAuth authentication via 1Password for secure credential management
- New mise tasks: `tailnet-preview`, `tailnet-up`

## Architecture
Two-layer approach:
- **Layer 1 (Pulumi)**: Tailnet-wide config (ACLs, tags, DNS)
- **Layer 2 (Ansible)**: Node-local `tailscale serve` config (unchanged)

## Test plan
- [x] Exported current ACL from Tailscale API
- [x] Imported existing ACL into Pulumi state
- [x] Verified `mise run tailnet-preview` shows no changes
- [x] Verified `mise run tailnet-up` applies successfully

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

Reviewed-on: https://forge.tail8d86e.ts.net/eblume/blumeops/pulls/15
This commit is contained in:
Erich Blume 2026-01-15 20:55:25 -08:00
commit 3f4e40f3ae
13 changed files with 231 additions and 0 deletions

View file

@ -0,0 +1,25 @@
---
- name: Get current tailscale serve status
ansible.builtin.command: tailscale serve status --json
register: serve_status
changed_when: false
- name: Configure HTTPS services
ansible.builtin.command: >
tailscale serve --service="{{ item.name }}"
--https={{ item.https.port }} {{ item.https.upstream }}
loop: "{{ tailscale_services }}"
when: item.https is defined
register: https_result
changed_when: "'already serving' not in https_result.stderr | default('')"
failed_when: false
- name: Configure TCP services
ansible.builtin.command: >
tailscale serve --service="{{ item.name }}"
--tcp={{ item.tcp.port }} {{ item.tcp.upstream }}
loop: "{{ tailscale_services }}"
when: item.tcp is defined
register: tcp_result
changed_when: "'already serving' not in tcp_result.stderr | default('')"
failed_when: false