Proxmox: node1-3 → 10.0.19.101-103 Talos cp1-3 → 10.0.19.1-3, workers → 10.0.19.4-6 K8s endpoint → VIP https://10.0.19.10:6443 Ansible: prom → 10.0.19.31, db → 10.0.19.30 Talos: added VIP block to controlplane.yaml base config Promtail: Loki URL → 10.0.19.31 Docs: all references updated, talos4 removed
10 KiB
CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Repository Overview
This is a multi-environment infrastructure-as-code repository managing:
- Ansible: Configuration management for servers across multiple environments (VNTX, home, app servers)
- OpenTofu: Proxmox/Talos VM provisioning and Porkbun domain NS management
- Home Lab: Server configurations with Tailscale integration
Common Commands
Proxmox VM Provisioning
Infrastructure VMs are provisioned using OpenTofu and configured via Butane/Ignition (Flatcar) or Ansible (traditional Linux):
# Infrastructure VMs are managed by OpenTofu (tofu/proxmox.tf)
cd tofu && tofu plan && tofu apply
# Post-provision configuration via Ansible
cd ansible && source .envrc
# Configure BIND9 nameserver at 204.110.191.222
ansible-playbook playbook.yml --tags provision,bind9
Flatcar Linux VMs (managed by lucidsolns/flatcar-vm/proxmox module):
- No manual template needed — module downloads the Flatcar image automatically
- VM definition:
tofu/proxmox.tf(moduleflatcar_resolver, VMID 110) - Configuration:
tofu/flatcar-resolver.bu(Butane → Ignition) - Config files:
tofu/unbound.conf(rendered from group_vars),tofu/blocklist.rpz - Static IP: 10.0.0.30/24, 8GB RAM, 2 cores, UEFI boot
- Default user:
core(passwordless sudo) - Unbound runs as a Docker container (
mvance/unbound) with config + blocklist mounted - DoH endpoint on 127.0.0.1:8080 for future Caddy proxy
- To deploy: update
flatcar-resolver.buor config files, thentofu apply - Proxmox storage
localmust havesnippetsandimportcontent types enabled (/etc/pve/storage.cfg)
### Ansible Operations
```bash
# Run main playbook
cd ansible && ./run.sh
# Bootstrap new host
make bootstrap-init HOSTNAME=hostname ANSIBLE_HOST=ip_address
# Run specific playbook
ansible-playbook general.yml
# Run with specific tags
ansible-playbook -t caddy general.yml
# Gather facts from all hosts
make facts
OpenTofu Operations
IMPORTANT: Use OpenTofu (tofu) instead of terraform for all operations.
cd tofu
tofu init
tofu plan
tofu apply
# Format terraform files before committing
tofu fmt
Talos Operations (Home Cluster)
# Change to talos directory
cd /Users/graham/dev/infra/talos
# Check cluster health
talosctl --talosconfig talosconfig health
# View cluster nodes
kubectl get nodes
# View all pods
kubectl get pods -A
# Check talos services
talosctl --talosconfig talosconfig service
# View logs
talosctl --talosconfig talosconfig logs -f kubelet
Architecture
Directory Structure
-
ansible/- Ansible playbooks and roles for server configurationroles/- Reusable roles (base, caddy, docker, resolvers, etc.)group_vars/- Group-specific variableshost_vars/- Host-specific variables- Main playbooks:
playbook.yml,general.yml,bootstrap.yml
-
tofu/- OpenTofu infrastructure management- Proxmox/Talos VM provisioning (main.tf, talos_vms.tf)
- Porkbun domain NS management (dns.tf)
- Provider:
marcfrederick/porkbun ~> 1.3 - Credentials via
PORKBUN_API_KEYandPORKBUN_SECRET_API_KEYenv vars - Manages NS records for all Porkbun domains (as393837, cloudflare, or porkbun default NS)
- Provider:
- Backend: Local state (committed to repo)
-
home/- Home lab configurationscluster/- Kubernetes application deploymentsargocd/- ArgoCD install + Application manifests (app-of-apps viaroot-app.yaml)aprs/- APRS.me deployment manifests (legacy; live manifests in theaprs.merepo'sk8s/)
Key Infrastructure Services
Monitoring: Icinga2 with MariaDB backend DNS: Unbound resolvers, PowerDNS for reverse zones VPN: Tailscale integration across environments Web Proxy: Caddy for reverse proxy and SSL termination
Security Notes
- Ansible user (UID 10001) with passwordless sudo
- SSH keys fetched from GitHub for authentication
- Tailscale requires
TAILSCALE_KEYenvironment variable - OpenTofu credentials should use environment variables (PORKBUN_API_KEY, PORKBUN_SECRET_API_KEY)
Development Workflow
Adding New Hosts
- Add host to
ansible/hostsinventory - Create host_vars file if needed
- Run bootstrap:
make bootstrap-init HOSTNAME=x ANSIBLE_HOST=y - Apply configuration:
ansible-playbook -l hostname general.yml
DNS Zone Changes (BIND9)
- Edit zone YAML in
ansible/group_vars/bind9_servers/<domain>.yml - Run ansible playbook to deploy zone changes
Domain NS Changes (Porkbun Registrar)
- Edit
tofu/dns.tf— update the domain's NS in thelocal.domainsmap - Run
tofu fmtto format - Review with
tofu plan - Apply with
tofu apply
Testing
Ansible
# Syntax check
ansible-playbook --syntax-check playbook.yml
# Dry run
ansible-playbook --check playbook.yml
# Test on specific host
ansible-playbook -l hostname playbook.yml
Terraform
tofu validate
tofu plan
Recent Infrastructure Updates
Talos Home Cluster (10.0.19.1-6)
- Purpose: Primary home lab Kubernetes cluster
- Cluster Name: home-cluster
- Kubernetes Version: v1.36.1
- Control Plane Nodes: 3 (10.0.19.1-3)
- Worker Nodes: 3 (10.0.19.4-6)
- API Endpoint: https://10.0.19.10:6443
- Proxmox Cluster: "home" (node1, node2, node3 at 10.0.19.101-103)
- VM Disks: local-lvm on each Proxmox node
- Persistent Storage: TBD (Longhorn was removed — evaluating alternatives)
- CNI: Flannel
- Talos Extensions (workers only):
iscsi-toolsv0.2.0util-linux-tools2.41.2
- Installer image:
factory.talos.dev/installer/613e1592b2da41ae5e265e8789429f22e121aab91cb4deb6bc3c0b6262961245:v1.13.3 - Control plane nodes use the stock
ghcr.io/siderolabs/installer:v1.13.3 - Deployment: Managed via OpenTofu in
/tofu/ - Configuration:
/talos/directorytalosconfig- Talosctl client configsecrets.yaml- Cluster secretscontrolplane.yaml/worker.yaml- Base node configsworker.yaml/controlplane.yaml- Base node configs (applied with per-node patches)patches/*.yaml- Per-node patches (hostname, disk overrides)
- GitOps: ArgoCD (replaced Flux on 2026-04-29). See
home/cluster/argocd/and the GitOps section below. - Applications: deployed via ArgoCD
Applicationmanifests inhome/cluster/argocd/apps/- APRS.me (namespace
aprs, https://aprs.me) — sourced fromcodeberg.org/gmcintire/aprs.me.git./k8s - gridmap (namespace
gridmap) — sourced fromcodeberg.org/gmcintire/gridmap.git./k8s - prop (namespace
prop) — sourced fromcodeberg.org/gmcintire/prop.git./k8s - towerops (namespace
towerops) — sourced fromcodeberg.org/gmcintire/towerops-web.git./k8s
- APRS.me (namespace
- Documentation: See
/talos/README.mdand/cluster/README.mdfor detailed operations - Config Notes:
install.extraKernelArgsandinstall.grubUseUKICmdlinecannot be used together- Worker disks with existing data (e.g. old Ceph LVM) must be wiped before Talos will partition them:
talosctl reset --user-disks-to-wipe /dev/sdb --wipe-mode user-disks --reboot
GitOps via ArgoCD (home-cluster)
- Install:
home/cluster/argocd/install/— upstream ArgoCD manifests + a Kustomize patch enablingserver.insecure=true(cloudflared terminates TLS) andreposerver.git.modules.enabled=false(theaprs.merepo has a stale submodule pin that breaks fetch otherwise) - App-of-apps:
home/cluster/argocd/root-app.yamlwatcheshome/cluster/argocd/apps/in this repo and reconciles all child Applications. Add a new YAML toapps/and push — Argo picks it up. - Repo creds:
forgejo-mcintire-credsSecret inargocdns is a credential template forhttps://codeberg.org/gmcintire— username/password copied fromflux-system/forgejo-git-credentials. - UI:
argo.w5isp.comvia cloudflared tunnel →argocd-server.argocd.svc.cluster.local:80. Initial admin password is inargocd/argocd-initial-admin-secret. - Image auto-update: Flux's
ImagePolicy/ImageUpdateAutomationis gone. Annotations like# {"$imagepolicy": "flux-system:prop"}in app repo manifests are inert. To restore, installargocd-image-updater.
Caddy server: w5isp.w5isp.com (204.110.191.200)
- Purpose: Single-node server running Debian 13
- Features:
- Web services with automatic SSL
- Single public IP handles all services
DNS Configuration
- Photos: photos.w5isp.com → 204.110.191.212 (Caddy) → 100.107.11.77:2283
- Mailcow: mcintire.me mail records → mail.w5isp.com (sync.w5isp.com at 204.110.191.216)
- Full mail setup with MX, SPF, DKIM, DMARC records
- Autodiscover/Autoconfig for mail clients
- Backup MX: mail.nsnw.ca (priority 20)
- Note: sync.w5isp.com is the Mailcow server (not using Caddy proxy)
Code Guidelines
Communication Practices
- Do not ever reply with "you're right". Just fix the issue
- Never say you're right, just accept it and move on
Notes for Claude
- As you learn new things, keep claude.md updated
Cluster
- home-cluster (Talos): 10.0.19.1-6 — only Kubernetes cluster. External traffic enters via the
cloudflaredtunnel deployment in thecloudflarednamespace; per-hostname routing rules live in Cloudflare's dashboard, not in the cluster.
Application Deployment Patterns
Standard App Deployment Process
- App repos contain their own k8s manifests in
./k8s/ - ArgoCD
Applicationinhome/cluster/argocd/apps/<name>.yamlpoints at the repo - Push the app repo → Argo auto-syncs (when
automated.selfHeal=trueis set) - Use a CSI storage driver for persistent volumes (set default StorageClass)
- Use ClusterIP services + Cloudflare tunnel for external access
Common Issues & Solutions
- PodSecurity violations: Set namespace to privileged:
kubectl label namespace [ns] pod-security.kubernetes.io/enforce=privileged --overwrite - Database connectivity: Use short service names (
postgresnotpostgres.namespace.svc.cluster.local) - Init container permissions: Required for volume ownership fixes
- Service DNS: Services accessible via
service-namewithin same namespace - Argo can't fetch a repo with a submodule: confirm
reposerver.git.modules.enabled=falseis set onargocd-cmd-params-cm(theaprs.merepo had a stale submodule pin that broke clones) - Argo refuses repo for "out-of-bounds symlinks": the repo has a tracked symlink pointing outside the working tree (e.g. into
/nix/store/...). Fix in the source repo — Argo has no override.