Add project orientation and contributor docs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9a8181ef18
commit
19d93d32dc
5 changed files with 386 additions and 0 deletions
23
AGENTS.md
Normal file
23
AGENTS.md
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Guidance for AI coding agents
|
||||
|
||||
**Read `CLAUDE.md` first — it is the authoritative, detailed guide for this repo.**
|
||||
This file exists so that non-Claude tools find the same rules; `CLAUDE.md` is
|
||||
canonical. Also read **`STATUS.md`** to learn what actually exists versus what is
|
||||
only designed — much of the ADR-described design is not built yet.
|
||||
|
||||
## Non-negotiables (full detail in CLAUDE.md)
|
||||
|
||||
- **Verify before claiming done.** Run `make lint` and the relevant `make check` /
|
||||
`make test`, and report the real output. Never assert success you haven't observed.
|
||||
- **Never edit generated files** (e.g. `inventories/*/hosts.yml`). Edit the source
|
||||
(`terraform/environments/<env>/main.tf`) and regenerate with `make tf-inventory`.
|
||||
Generated files carry a header saying so.
|
||||
- **Secrets only in `vault.yml`** files — never plaintext elsewhere. Never read,
|
||||
print, or commit `.vault_pass`.
|
||||
- **No `make deploy` / `make tf-apply`** without running `make check` / `make tf-plan`
|
||||
first and showing the output.
|
||||
- **Before deleting or overwriting a file you did not create, read it first** and
|
||||
surface what you find rather than proceeding blind.
|
||||
- **Check `STATUS.md`** before assuming a role, provider, or pipeline exists.
|
||||
- **Git**: `main` must always work; branch for sweeping changes. Commit your work in
|
||||
logical units with imperative ≤72-char subjects and a `Co-Authored-By` trailer.
|
||||
167
CLAUDE.md
Normal file
167
CLAUDE.md
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
# CLAUDE.md — Ansible homelab monorepo
|
||||
|
||||
This file is read by Claude Code at the start of every session.
|
||||
Keep it dense and command-focused. Verbose detail lives in `docs/`.
|
||||
|
||||
> **Before assuming a role, provider, or pipeline exists, check `STATUS.md`.**
|
||||
> Much of the design in `docs/decisions/` is intended, not yet built (e.g. the
|
||||
> `base`/`docker_host` roles are currently empty; Terraform is not `init`ed).
|
||||
|
||||
---
|
||||
|
||||
## Project in one paragraph
|
||||
|
||||
Homelab infrastructure automation for a Proxmox cluster running 2–5 Debian 13 VMs.
|
||||
All hosts share a hardened base configuration. Each host runs a defined set of Docker
|
||||
services deployed via Compose files rendered from Ansible templates. Ansible runs from
|
||||
a dedicated control VM. CI runs on Forgejo Actions (self-hosted).
|
||||
|
||||
Full design rationale: `docs/decisions/`
|
||||
|
||||
---
|
||||
|
||||
## Key commands
|
||||
|
||||
| Action | Command |
|
||||
|-------------------------------|--------------------------------------------------|
|
||||
| Lint everything | `make lint` |
|
||||
| Test a single role | `make test ROLE=<name>` |
|
||||
| Test all roles | `make test-all` |
|
||||
| Check mode (dry run) | `make check PLAYBOOK=<name>` |
|
||||
| Deploy a playbook | `make deploy PLAYBOOK=<name>` |
|
||||
| Scaffold a new role | `make new-role NAME=<name>` |
|
||||
| Encrypt a vault file | `make encrypt FILE=<path>` |
|
||||
| Decrypt a vault file | `make decrypt FILE=<path>` |
|
||||
| Install Python deps | `make setup` |
|
||||
| Install Ansible collections | `make collections` |
|
||||
| Initialise Terraform | `make tf-init [TF_ENV=staging]` |
|
||||
| Terraform plan | `make tf-plan [TF_ENV=staging]` |
|
||||
| Terraform apply | `make tf-apply [TF_ENV=staging]` |
|
||||
| Regenerate Ansible inventory | `make tf-inventory TF_ENV=<staging\|production>` |
|
||||
|
||||
**Always `tf-plan` before `tf-apply`. Always `check` before `deploy`. Never skip lint.**
|
||||
|
||||
`TF_ENV` defaults to `staging`. Always specify `TF_ENV=production` explicitly for production.
|
||||
|
||||
---
|
||||
|
||||
## Ansible conventions
|
||||
|
||||
- **FQCN always**: `ansible.builtin.template`, never `template`
|
||||
- **Tags**: every task must have at least one tag; playbooks support `--tags` filtering
|
||||
- **Handlers**: use `listen:` topic strings, not direct name references
|
||||
- **Variables**: `rolename__varname` double-underscore namespace for role defaults
|
||||
- **No inline vars in playbooks**: use `group_vars/` or `host_vars/` only
|
||||
- **Loops**: prefer `loop:` over `with_items:`
|
||||
- **Conditionals**: prefer `true`/`false` over `yes`/`no`
|
||||
|
||||
---
|
||||
|
||||
## Secrets
|
||||
|
||||
- Encrypted files are always named `vault.yml`, sitting alongside `vars.yml`
|
||||
- Never put plaintext secrets in any file not named `vault.yml`
|
||||
- Vault password file: `.vault_pass` (gitignored — obtain via secure channel)
|
||||
- To edit a vault file: `make decrypt FILE=<path>`, edit, `make encrypt FILE=<path>`
|
||||
|
||||
---
|
||||
|
||||
## Role conventions
|
||||
|
||||
- Every role must have `molecule/default/` scenario targeting Debian 13
|
||||
- Every role must have a populated `README.md`
|
||||
- Every role must have `meta/main.yml` filled in
|
||||
- Role names: `snake_case`, descriptive nouns (`base`, `docker_host`, `reverse_proxy`)
|
||||
- Use `make new-role NAME=<name>` to scaffold — never create role structure by hand
|
||||
|
||||
---
|
||||
|
||||
## Inventory structure
|
||||
|
||||
```
|
||||
inventories/
|
||||
production/ # live hosts — edit with care
|
||||
hosts.yml
|
||||
group_vars/
|
||||
all/ # applies to every host
|
||||
vars.yml
|
||||
vault.yml
|
||||
control/ # the control node (baseline config only)
|
||||
docker_hosts/ # hosts running Docker services
|
||||
proxmox_hosts/ # Proxmox nodes themselves
|
||||
host_vars/ # per-host overrides
|
||||
staging/ # safe to run freely
|
||||
```
|
||||
|
||||
Host groups: `all`, `control`, `docker_hosts`, `proxmox_hosts`
|
||||
|
||||
(`control` holds the one manually-provisioned control node — see ADR-009.)
|
||||
|
||||
---
|
||||
|
||||
## Git conventions
|
||||
|
||||
Single-contributor, trunk-based (no merge requests / approval gates):
|
||||
|
||||
- `main` is the trunk and must always work — small, safe changes commit straight to it
|
||||
- Branch for sweeping or AI-driven changes you want to review as one diff or be able
|
||||
to abandon: `role/<name>`, `fix/<description>`, `feat/<description>`,
|
||||
`chore/<description>`; merge to `main` when reviewed, then delete the branch
|
||||
- Run `make lint` (and `make test` for touched roles) before committing
|
||||
- Commit in logical units; imperative subject ≤72 chars
|
||||
- AI agents commit their own work in logical units with a `Co-Authored-By` trailer
|
||||
- Push to the Forgejo `origin` often — it is the off-machine backup
|
||||
- Never commit secrets; a `vault.yml` must be `$ANSIBLE_VAULT`-encrypted (pre-commit
|
||||
enforces this, plus gitleaks secret scanning)
|
||||
|
||||
---
|
||||
|
||||
## Dependencies policy
|
||||
|
||||
- **No Galaxy roles** — all roles are local; never add a Galaxy role to `requirements.yml`
|
||||
- **Collections on demand** — only add a collection when a task in a committed role
|
||||
uses a module from it; add a comment in `requirements.yml` naming the module(s) used
|
||||
- Full rationale: `docs/decisions/003-toolchain.md` (Collections and roles policy)
|
||||
|
||||
---
|
||||
|
||||
## Terraform conventions
|
||||
|
||||
- Terraform owns VM existence only — nothing inside a VM, and no DNS records
|
||||
- Internal DNS is entirely Ansible (the `dns` role renders the zone from inventory)
|
||||
- OPNsense is entirely Ansible; do not reach for a Terraform OPNsense provider
|
||||
- Environments are separate directories (`staging/`, `production/`), not workspaces
|
||||
- Secrets via `TF_VAR_*` env vars only — never in `.tfvars` files
|
||||
- `terraform.tfvars.example` is tracked; `terraform.tfvars` is gitignored
|
||||
- `.terraform.lock.hcl` is tracked (pins provider versions)
|
||||
- Full rationale: `docs/decisions/006-terraform.md`
|
||||
|
||||
---
|
||||
|
||||
## What Claude must not do without explicit instruction
|
||||
|
||||
- Run `make deploy` — always run `make check` first and show output
|
||||
- Run `make tf-apply` — always run `make tf-plan` first and show output
|
||||
- Modify `inventories/production/hosts.yml` directly — regenerate via `make tf-inventory`
|
||||
- Edit vault-encrypted files directly — decrypt first, re-encrypt after
|
||||
- Push to `main` branch
|
||||
- Add a collection to `requirements.yml` without a specific module need in existing role tasks
|
||||
|
||||
---
|
||||
|
||||
## Further reading
|
||||
|
||||
| Topic | File |
|
||||
|------------------------|---------------------------------------|
|
||||
| Architecture overview | `docs/decisions/001-architecture.md` |
|
||||
| Security baseline | `docs/decisions/002-security.md` |
|
||||
| Toolchain choices | `docs/decisions/003-toolchain.md` |
|
||||
| Docker & Compose model | `docs/decisions/004-docker-model.md` |
|
||||
| Bootstrapping hosts | `docs/decisions/005-bootstrapping.md` |
|
||||
| Terraform | `docs/decisions/006-terraform.md` |
|
||||
| Network topology | `docs/decisions/007-network.md` |
|
||||
| Testing methodology | `docs/decisions/008-testing.md` |
|
||||
| TF ↔ Ansible handoff | `docs/decisions/009-provisioning-handoff.md` |
|
||||
| Adding a new role | `docs/runbooks/new-role.md` |
|
||||
| Adding a new host | `docs/runbooks/new-host.md` |
|
||||
| Rotating vault secrets | `docs/runbooks/rotate-secrets.md` |
|
||||
59
CONTRIBUTING.md
Normal file
59
CONTRIBUTING.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
# Contributing
|
||||
|
||||
## Conventions
|
||||
|
||||
- All Ansible modules use FQCN: `ansible.builtin.template`, not `template`
|
||||
- Every task has a `name:` that reads as a sentence and at least one tag
|
||||
- Role variables use `rolename__varname` double-underscore namespace
|
||||
- No plain text secrets outside `vault.yml` files
|
||||
|
||||
## Branching
|
||||
|
||||
Single-contributor, trunk-based:
|
||||
|
||||
- `main` is the trunk and must always work. Small, self-contained changes commit
|
||||
straight to `main`.
|
||||
- Use a short-lived branch for sweeping or AI-driven changes you want to review as
|
||||
one diff or be able to abandon: `role/<name>`, `fix/<description>`,
|
||||
`feat/<description>`, `chore/<description>`. Merge to `main` when it looks right,
|
||||
then delete the branch.
|
||||
- Run `make lint` (and `make test` for touched roles) before committing.
|
||||
- Commit messages: imperative mood, ≤72-char subject; commit in logical units.
|
||||
- Push to Forgejo often — it is the off-machine backup.
|
||||
|
||||
## Adding a role
|
||||
|
||||
Follow the runbook: `docs/runbooks/new-role.md`
|
||||
|
||||
Always use `make new-role NAME=<name>` to scaffold — never create structure by hand.
|
||||
|
||||
## Secrets
|
||||
|
||||
Vault password is shared via a secure channel (password manager).
|
||||
Never commit `.vault_pass`. Never put secrets in non-`vault.yml` files.
|
||||
|
||||
See `docs/runbooks/rotate-secrets.md` for rotation procedures.
|
||||
|
||||
## Generated files
|
||||
|
||||
Some files are produced by tooling and must not be hand-edited — change the source
|
||||
and regenerate. Each generated file carries a header saying so.
|
||||
|
||||
| Generated file | Source of truth | Regenerate with |
|
||||
|---|---|---|
|
||||
| `inventories/<env>/hosts.yml` | `terraform/environments/<env>/main.tf` (`local.vms`) | `make tf-inventory TF_ENV=<env>` |
|
||||
|
||||
Exception: the control node is added to `hosts.yml` by hand — see
|
||||
`docs/runbooks/new-host.md`.
|
||||
|
||||
## Testing
|
||||
|
||||
Before opening a merge request:
|
||||
|
||||
```bash
|
||||
make lint
|
||||
make test ROLE=<affected-role>
|
||||
make check PLAYBOOK=site
|
||||
```
|
||||
|
||||
All three must pass cleanly.
|
||||
86
README.md
Normal file
86
README.md
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
# Ansible homelab
|
||||
|
||||
Infrastructure automation for a Proxmox-based homelab running primarily Debian 13 VMs
|
||||
with Docker services. Stable, secure, and fully managed via Ansible.
|
||||
|
||||
## Quick start (control node)
|
||||
|
||||
```bash
|
||||
git clone <repo-url> ~/ansible
|
||||
cd ~/ansible
|
||||
|
||||
# Create venv and install dependencies
|
||||
make setup
|
||||
make collections
|
||||
|
||||
# Place vault password (obtain via secure channel)
|
||||
echo "your-vault-password" > .vault_pass
|
||||
chmod 600 .vault_pass
|
||||
|
||||
# Verify setup
|
||||
make lint
|
||||
```
|
||||
|
||||
## Common operations
|
||||
|
||||
| What | Command |
|
||||
| --------------------- | ------------------------------ |
|
||||
| Lint everything | `make lint` |
|
||||
| Dry-run site playbook | `make check PLAYBOOK=site` |
|
||||
| Deploy everything | `make deploy PLAYBOOK=site` |
|
||||
| Test a role | `make test ROLE=base` |
|
||||
| Scaffold a new role | `make new-role NAME=myservice` |
|
||||
|
||||
See `Makefile` for the full list of targets.
|
||||
|
||||
## Project structure
|
||||
|
||||
```
|
||||
.
|
||||
├── CLAUDE.md # Claude Code session context
|
||||
├── Makefile # All operations go through here
|
||||
├── ansible.cfg # Project-scoped Ansible config
|
||||
├── requirements.txt # Python dependencies
|
||||
├── requirements.yml # Ansible collections
|
||||
│
|
||||
├── docs/
|
||||
│ ├── decisions/ # Architecture decision records (ADRs)
|
||||
│ └── runbooks/ # Step-by-step operational procedures
|
||||
│
|
||||
├── inventories/
|
||||
│ ├── production/ # Live hosts — edit carefully
|
||||
│ └── staging/ # Test hosts — safe to run freely
|
||||
│
|
||||
├── playbooks/ # Orchestration playbooks
|
||||
│ ├── site.yml # Full standard state
|
||||
│ └── bootstrap.yml # First-run new host setup
|
||||
│
|
||||
├── roles/ # Ansible roles
|
||||
│ ├── base/ # OS baseline applied to all hosts
|
||||
│ └── docker_host/ # Docker runtime setup
|
||||
│
|
||||
├── terraform/ # VM provisioning + infra DNS (see ADR-006/009)
|
||||
│ ├── modules/ # Reusable modules (proxmox_vm)
|
||||
│ └── environments/ # Per-env state: staging/, production/
|
||||
│
|
||||
└── scripts/ # Helper scripts (tf_to_inventory.py)
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
- **Current state (built vs planned): `STATUS.md`** — read this before assuming
|
||||
something exists; the ADRs describe intent, not necessarily reality.
|
||||
- AI agents: `AGENTS.md` (points to `CLAUDE.md`, the authoritative guide)
|
||||
- Architecture: `docs/decisions/001-architecture.md`
|
||||
- Security baseline: `docs/decisions/002-security.md`
|
||||
- Toolchain decisions: `docs/decisions/003-toolchain.md`
|
||||
- Docker model: `docs/decisions/004-docker-model.md`
|
||||
- Bootstrapping: `docs/decisions/005-bootstrapping.md`
|
||||
- Terraform: `docs/decisions/006-terraform.md`
|
||||
- Network topology: `docs/decisions/007-network.md`
|
||||
- Testing methodology: `docs/decisions/008-testing.md`
|
||||
- Terraform ↔ Ansible handoff: `docs/decisions/009-provisioning-handoff.md`
|
||||
|
||||
## Contributing
|
||||
|
||||
See `CONTRIBUTING.md` for conventions, branching strategy, and how to add roles.
|
||||
51
STATUS.md
Normal file
51
STATUS.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
# Project status — what's real vs planned
|
||||
|
||||
This repo is partly aspirational: the ADRs in `docs/decisions/` describe the
|
||||
*intended* design, and some of it is **not built yet**. This file is the ground
|
||||
truth. **Before relying on a role, provider, or pipeline existing, check here.**
|
||||
If something is listed as "designed, not built", do not assume it works.
|
||||
|
||||
_Last reviewed: 2026-05-30._
|
||||
|
||||
## Real and working today
|
||||
|
||||
| Thing | State |
|
||||
|---|---|
|
||||
| `playbooks/bootstrap.yml` | Works — self-contained (installs Python, creates the `ansible` user + sudoers) |
|
||||
| `scripts/tf_to_inventory.py` | Works — stdlib only; `terraform output -json` → `hosts.yml` |
|
||||
| `.docker/molecule-debian13/Dockerfile` | Present — custom Molecule test image (ADR-008) |
|
||||
| `docs/decisions/*`, `docs/runbooks/*` | Current and mutually reconciled |
|
||||
| `Makefile`, lint config (`.ansible-lint`, `.yamllint`), `.gitignore` | Present and used |
|
||||
| `git` (local) | Initialized — trunk-based on `main`. Off-machine remote (Forgejo) being set up separately. |
|
||||
| Pre-commit hooks | Configured: lint, gitleaks, vault-encryption guard. Activate with `pre-commit install` after `make setup`. |
|
||||
| Terraform HCL (`terraform/`) | Written (proxmox VM module + envs) — but never run; see below |
|
||||
|
||||
## Scaffolded but empty — NOT implemented
|
||||
|
||||
| Thing | State |
|
||||
|---|---|
|
||||
| `roles/base/` | Empty directory. `site.yml` references it, but it applies nothing. |
|
||||
| `roles/docker_host/` | Empty directory. Same. |
|
||||
| `inventories/*/hosts.yml` | Placeholder stubs (commented examples); regenerated by `make tf-inventory` once Terraform has hosts |
|
||||
| `inventories/production/group_vars/{docker_hosts,proxmox_hosts}/` | Empty dirs |
|
||||
|
||||
So `make deploy PLAYBOOK=site` currently does effectively nothing — the roles it
|
||||
calls are empty.
|
||||
|
||||
## Designed but not built
|
||||
|
||||
| Thing | Designed in | Notes |
|
||||
|---|---|---|
|
||||
| `dns` role (renders the internal zone) | ADR-007 / ADR-009 | Does not exist. Internal DNS ownership is assigned to it by design. |
|
||||
| Terraform actually provisioning | ADR-006 / ADR-009 | Never `terraform init`ed: no `.terraform.lock.hcl`, no state, no real `local.vms` entries |
|
||||
| CI (Forgejo Actions) | ADR-003 / ADR-008 | Pipeline described; not implemented |
|
||||
| Level 2 / 3 testing (staging, `askari` smoke) | ADR-008 | Depends on real VMs / `askari`, which don't exist yet |
|
||||
| Per-service roles | ADR-004 | Model defined; no service roles built |
|
||||
| Forgejo remote + CI | ADR-003 / ADR-008 | Local git is live; pushing to `git.baobab.band` and Actions CI are being set up |
|
||||
|
||||
## Keeping this honest
|
||||
|
||||
Update this file whenever you build, stub, or remove something. It is the first
|
||||
place an AI tool or new contributor should look to learn what they can actually
|
||||
rely on. When a row moves from "designed" to "working", move it up — don't leave
|
||||
stale optimism here.
|
||||
Loading…
Add table
Reference in a new issue