Consolidates nixos, docker, raspi, and debian-configuration into a single infrastructure-as-code repo. Includes: - ansible/: full inventory + proxmox-hardening, freeipa, and raspberrypi roles (converted from debian-configuration bash scripts) - terraform/: Proxmox VMs, Dynu DNS, Pi-hole (decommissioned stub), Docker container catalog — migrated from docker/infrastructure/terraform/ - stacks/docker/, stacks/raspi/, nixos/: placeholder READMEs pending git subtree population (see implementation plan) - docs/: internal MkDocs site with architecture, network topology, runbooks, and drift-detection guide; external sanitized site - scripts/: drift-detect.sh, docs-build.sh, install-hooks.sh, check-secrets.sh - CI: secret-scan (push/PR), drift-detect (daily), docs-build (on change) - Pi-hole removed throughout — DNS is FreeIPA, DHCP is router See docs/internal/implementation-plan.md for the phased rollout after pushing to Gitea. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UvNjoxTWEDkhXsd1Dq2ETP
12 KiB
Implementation Plan
Phased rollout of the infrastructure mono-repo. Work through these phases in order
after the initial push to Gitea. Each phase is independently completeable — the repo
is usable after Phase 0 even if later phases aren't done yet.
Phase 0: Initial push and git history migration
Goal: Get the repo to Gitea and bring in existing repos with their commit histories.
0.1 — Create Gitea repo and push
cd /home/wayne/repos/infrastructure
git add -A
git commit -m "Initial infrastructure mono-repo scaffold"
# Create the repo on Gitea first (via web UI), then:
git remote add origin git@gitea.lan.ddnsgeek.com:wayne/infrastructure.git
git push -u origin main
0.2 — Migrate existing repos via git subtree
git subtree merges another repo's history into a subdirectory. Run these in order.
--squash collapses history into one merge commit (cleaner). Remove --squash if you
want full per-commit history (makes the log noisier but preserves full blame).
# Main docker stack → stacks/docker/
git subtree add --prefix=stacks/docker /home/wayne/repos/docker main --squash
git push
# Raspberry Pi stack → stacks/raspi/
git subtree add --prefix=stacks/raspi /home/wayne/repos/raspi main --squash
git push
Note:
debian-configurationis NOT imported via subtree — it's being dissolved. Its content has been converted into Ansible roles inansible/roles/. The original repo should be archived in Gitea (Settings → Danger Zone → Archive) after Phase 3 is complete.
0.3 — Install pre-commit hook locally
./scripts/install-hooks.sh
0.4 — Configure Gitea CI secrets
In the Gitea repo → Settings → Secrets, add:
| Secret name | Value |
|---|---|
PROXMOX_ENDPOINT |
https://pve1.sweet.home:8006/ |
PROXMOX_API_TOKEN_ID |
Terraform API token ID (create in PVE if not exists) |
PROXMOX_API_TOKEN_SECRET |
Terraform API token secret |
DYNU_API_KEY |
Dynu API key |
ANSIBLE_SSH_KEY |
Private SSH key for Ansible connections (base64 or raw PEM) |
GOTIFY_URL |
https://gotify.lan.ddnsgeek.com |
GOTIFY_TOKEN |
Gotify app token |
Creating a Proxmox API token for Terraform (if not already done):
# On pve1 as root:
pveum user add terraform@pve --comment "Terraform service account"
pveum role add TerraformRole --privs "VM.Allocate VM.Clone VM.Config.CDROM VM.Config.CPU VM.Config.Cloudinit VM.Config.Disk VM.Config.HWType VM.Config.Memory VM.Config.Network VM.Config.Options VM.Monitor VM.Audit VM.PowerMgmt Datastore.AllocateSpace Datastore.Audit Pool.Allocate Sys.Audit Sys.Console Sys.Modify"
pveum aclmod / -user terraform@pve -role TerraformRole
pveum user token add terraform@pve tf --privsep=0
# Copy the token secret — it's shown only once
Phase 1: Ansible — verify connectivity and run check mode
Goal: Confirm Ansible can reach all managed hosts and produce clean check-mode output.
1.1 — Install Ansible and collections
pip install ansible
cd ansible
ansible-galaxy collection install -r collections/requirements.yml
1.2 — Verify inventory and connectivity
cd ansible
# List all hosts
ansible all --list-hosts
# Ping everything
ansible-playbook playbooks/ping.yml
Fix any unreachable hosts (SSH key, hostname, ansible_user) before proceeding.
1.3 — Run check mode against each host group
# Proxmox nodes
ansible-playbook playbooks/proxmox.yml --check --diff --limit proxmox
# FreeIPA (check only — install tasks are gated by ipa_installed check)
ansible-playbook playbooks/freeipa.yml --check --diff
# Raspberry Pi
ansible-playbook playbooks/raspi.yml --check --diff
Expected result: check mode should show either "no changes" (already hardened) or
show exactly the diffs you'd expect. If it shows unexpected changes, review the task
and update the role defaults before applying.
1.4 — Apply to pve-test first (sandbox validation)
ansible-playbook playbooks/proxmox.yml --limit pve-test.sweet.home
Verify the node is still reachable and the PVE web UI works before applying to pve1.
1.5 — Apply to production hosts
# Confirm check mode looks clean, then:
ansible-playbook playbooks/proxmox.yml --limit pve1.sweet.home
ansible-playbook playbooks/raspi.yml
Phase 2: Terraform — import existing infrastructure state
Goal: Bring existing Proxmox VMs and DNS records under Terraform state management, enabling drift detection.
2.1 — Configure credentials
export TF_VAR_proxmox_endpoint="https://pve1.sweet.home:8006/"
export TF_VAR_proxmox_api_token_id="terraform@pve!tf"
export TF_VAR_proxmox_api_token_secret="<token>"
Or create terraform/proxmox/terraform.tfvars (this file is git-ignored):
proxmox_endpoint = "https://pve1.sweet.home:8006/"
proxmox_api_token_id = "terraform@pve!tf"
proxmox_api_token_secret = "<token>"
2.2 — Initialise Terraform workspaces
cd terraform/proxmox && terraform init
cd ../dns && terraform init
2.3 — Verify existing resource blocks match live state
The terraform/proxmox/ workspace already has resource blocks for existing VMs
(docker.tf, nix-cache.tf, server-nixos.tf, etc.) — these were generated from
live state previously. Verify they're accurate:
cd terraform/proxmox
# Check if state is already populated (it may not be on first run)
terraform state list
# If state is empty, import each existing VM:
terraform import proxmox_virtual_environment_vm.docker pve/qemu/103
terraform import proxmox_virtual_environment_vm.nix-cache pve/qemu/105
terraform import proxmox_virtual_environment_vm.server-nixos pve/qemu/104
# Add imports for any other VMs not yet in state
# VMID reference: check 'qm list' on pve1
2.4 — Run plan and reconcile any drift
terraform plan
If plan shows changes, the .tf resource block differs from live state.
Either update the .tf file to match (if the live state is correct) or apply
to enforce the declared state. Do not apply to production VMs without reviewing
every planned change.
2.5 — DNS workspace
cd terraform/dns
# Configure Dynu credentials if not already set:
export TF_VAR_dynu_api_key="<api_key>"
terraform plan
The DNS workspace already has the existing records imported. Verify no drift.
2.6 — Verify drift detection works end-to-end
Manually introduce a small change in the PVE UI (e.g. add a VM note) and run:
./scripts/drift-detect.sh --terraform
Confirm it detects the change. Revert it or update the .tf to match.
Phase 3: NixOS flake migration
Goal: Move the nixos repo into this mono-repo while keeping the flake fully functional.
3.1 — Import with git subtree
git subtree add --prefix=nixos /home/wayne/repos/nixos main --squash
git push
3.2 — Update the remote flake URL on all NixOS hosts
The Switch-nix and Test-nix aliases on NixOS hosts reference the old Gitea URL.
Update nixos/variables.nix (the flakeUrl variable) to point at the new path:
# Old:
flakeUrl = "git+ssh://gitea.lan.ddnsgeek.com/wayne/nixos";
# New:
flakeUrl = "git+ssh://gitea.lan.ddnsgeek.com/wayne/infrastructure?dir=nixos";
Deploy the change to the docker LXC first (lowest risk), then remaining hosts.
3.3 — Validate flake builds from subdirectory
nix build ./nixos#proxmox-docker
nix build ./nixos#proxmox-nix-cache
3.4 — Archive the old nixos repo
Once all hosts are rebuilding successfully from the new URL, archive the old repo:
Gitea → wayne/nixos → Settings → Danger Zone → Archive Repository.
Phase 4: Activate drift detection CI
Goal: Daily automated drift checks running in Gitea CI with Gotify notifications.
4.1 — Verify secrets are configured (from Phase 0.4)
Trigger the drift-detect workflow manually from Gitea CI → Actions → Drift Detection.
4.2 — Confirm first successful run
Review the step summary in Gitea. Expected outcome on a clean run:
- Terraform: "No changes. Infrastructure matches configuration."
- Ansible:
changed=0on all hosts
4.3 — Test the notification path
Temporarily modify a .tf resource (without applying) and trigger a manual run.
Confirm the Gotify notification arrives.
Phase 5: Documentation pipeline
Goal: Auto-generated service catalog and host inventory, with both internal and external sites building on every push.
5.1 — Install docs dependencies
pip install mkdocs mkdocs-material jinja2 pyyaml
5.2 — Test local build
./scripts/docs-build.sh
./scripts/docs-build.sh --serve # preview at http://localhost:8000
5.3 — Implement service catalog generator
Create scripts/generate-service-catalog.py:
- Parses all
docker-compose.ymlfiles understacks/ - Extracts service name, image, Traefik domain (from labels), network memberships
- Outputs
docs/generated/service-catalog.md
Template for the output:
# Service Catalog
Auto-generated from compose files in stacks/.
| Service | Image | Domain | Stack |
|---------|-------|--------|-------|
| traefik | traefik:latest | — | stacks/docker/core/traefik |
| nextcloud-webapp | nextcloud:production | nextcloud.lan.ddnsgeek.com | stacks/docker/apps/nextcloud |
...
5.4 — Configure docs deploy target
Set these Gitea secrets (from Phase 0.4):
PAGES_SSH_KEY— key for rsync to the docs serverPAGES_HOST— user@host for rsyncPAGES_PATH_INTERNAL— path on the docs server for internal site
Or use Gitea Pages if self-hosted Gitea supports it.
Phase 6: Cleanup and archive
Goal: Retire the old repos cleanly.
6.1 — Verify nothing still references old repos
- All NixOS hosts rebuilding from
infrastructure?dir=nixos✓ - Docker stack deployed from
stacks/docker/✓ - Ansible roles replacing debian-configuration scripts ✓
6.2 — Archive deprecated repos
Archive these repos on Gitea (makes them read-only, preserves history):
wayne/debian-configuration— content dissolved intoansible/roles/wayne/docker— content migrated tostacks/docker/wayne/raspi— content migrated tostacks/raspi/wayne/nixos— migrated toinfrastructure/nixos/
Do NOT delete them — they have git history that may be useful for reference.
6.3 — Update any external references
- CLAUDE.md files in the old repos — add a note pointing to the new mono-repo
- Any README links, bookmarks, or documentation that references the old repo URLs
Ongoing: Adding new infrastructure
Adding a new Proxmox VM
- Add a resource block in
terraform/proxmox/<name>.tf - Run
terraform plan→terraform apply - Add the host to
ansible/inventory/hosts.yml - If NixOS: add to
nixos/hosts/<name>/host.nix, run./scripts/sync-host-keys.sh - Deploy:
nixos-rebuild switch --flake ./nixos#<target> --target-host <hostname>
Adding a new Docker service
- Add a
docker-compose.ymlunderstacks/docker/apps/<service>/orstacks/docker/monitoring/<service>/ - Add Traefik labels, networks, secrets (following existing service patterns)
- Update
stacks/docker/default-environment.envif new env vars are needed - Test:
./services-up.sh config(validates interpolation), then./services-up.sh up -d <service>
Rotating credentials
- Proxmox API token: create new, update Gitea secrets, delete old
- Ansible SSH key: replace
ANSIBLE_SSH_KEYsecret, re-add public key to all hosts - Dynu API key: update
DYNU_API_KEYsecret in Gitea - NixOS SOPS secrets: use
nixos/scripts/rotate-admin-key.sh