boma/Makefile
sjat 50b6445bdd feat(reverse_proxy): Caddy role (Gandi DNS-01, on-host image build, route catalog)
Implements the Caddy reverse proxy role (ADR-024): builds boma/caddy-gandi:latest
on-host (caddy-dns/gandi plugin), renders Caddyfile from route catalog, brings
Compose project up. Adds community.docker to requirements.yml, production group_vars,
and a caddy-image Makefile target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 17:36:58 +02:00

196 lines
8.8 KiB
Makefile

# Ansible + Terraform homelab monorepo — Makefile
# All operations go through these targets. Never run ansible-playbook or terraform directly.
VENV := .venv
PYTHON := $(VENV)/bin/python
PIP := $(VENV)/bin/pip
ANSIBLE := $(VENV)/bin/ansible
PLAYBOOK_BIN := $(VENV)/bin/ansible-playbook
GALAXY := $(VENV)/bin/ansible-galaxy
LINT := $(VENV)/bin/ansible-lint
MOLECULE := $(VENV)/bin/molecule
# Vault password is resolved via ansible.cfg (vault_password_file); no flag needed.
VAULT_ARGS :=
# Default vault file for edit-vault / check-vault (override with VAULT=<path>).
VAULT ?= inventories/production/group_vars/all/vault.yml
INVENTORY := -i inventories/production/
TF := terraform
TF_ENV ?= staging
MOLECULE_IMAGE := forgejo.nyumbani.baobab.band/sjat/molecule-debian13:latest
MOLECULE_DOCKERFILE := .docker/molecule-debian13/Dockerfile
# For TF_ENV=offsite, source the Hetzner token from the vault into the environment
# (rbw must be unlocked). Read in-memory; never written to a tfvars file (CLAUDE.md).
ifeq ($(TF_ENV),offsite)
TF_TOKEN_ENV := TF_VAR_hcloud_token="$$($(ANSIBLE)-vault view inventories/production/group_vars/all/vault.yml | $(PYTHON) -c 'import sys, yaml; print(yaml.safe_load(sys.stdin)["vault"]["hetzner"]["token"])')"
else
TF_TOKEN_ENV :=
endif
.DEFAULT_GOAL := help
.PHONY: help setup collections lint test test-all check deploy encrypt decrypt \
edit-vault check-vault new-role \
tf-init tf-plan tf-apply tf-output tf-inventory tf-inventory-offsite \
molecule-image molecule-image-push \
caddy-image
help:
@echo ""
@echo "Ansible homelab — available targets:"
@echo ""
@echo " make setup Create venv and install Python deps"
@echo " make collections Install Ansible collections"
@echo " make lint Run yamllint + ansible-lint"
@echo " make test ROLE=<name> Run Molecule tests for a role"
@echo " make test-all Run Molecule tests for all roles"
@echo " make check PLAYBOOK=<name> [LIMIT=<host>] [TAGS=<tags>] Dry-run a playbook (check mode)"
@echo " make deploy PLAYBOOK=<name> [LIMIT=<host>] [TAGS=<tags>] Run a playbook against production"
@echo " make edit-vault [VAULT=<path>] Edit the vault in nvim (auto re-encrypts + checks)"
@echo " make check-vault [VAULT=<path>] Validate vault structure (values masked)"
@echo " make encrypt FILE=<path> Encrypt a vault file"
@echo " make decrypt FILE=<path> Decrypt a vault file"
@echo " make new-role NAME=<name> Scaffold a new role"
@echo ""
@echo " make tf-init [TF_ENV=staging] Initialise Terraform providers"
@echo " make tf-plan [TF_ENV=staging] Show Terraform plan"
@echo " make tf-apply [TF_ENV=staging] Apply Terraform changes"
@echo " make tf-output [TF_ENV=staging] Print Terraform outputs as JSON"
@echo " make tf-inventory [TF_ENV=staging] Regenerate Ansible inventory from Terraform outputs"
@echo " make tf-inventory-offsite Generate offsite_hosts inventory (askari) into inventories/production/"
@echo ""
@echo " TF_ENV defaults to 'staging'. Use TF_ENV=production for production."
@echo ""
@echo " make molecule-image Build the Molecule test image locally"
@echo " make molecule-image-push Push the test image to the Forgejo registry"
@echo " make caddy-image Build the custom Caddy image (caddy-dns/gandi) locally"
@echo ""
# ── Environment setup ─────────────────────────────────────────────────────────
setup:
python3 -m venv $(VENV)
$(PIP) install --upgrade pip
$(PIP) install -r requirements.txt
@echo "Venv ready. Activate with: source $(VENV)/bin/activate"
collections:
$(GALAXY) collection install -r requirements.yml --upgrade
# ── Linting ───────────────────────────────────────────────────────────────────
lint:
$(VENV)/bin/yamllint .
$(LINT)
$(PYTHON) scripts/check-tags.py
# ── Testing ───────────────────────────────────────────────────────────────────
test:
ifndef ROLE
$(error ROLE is required: make test ROLE=<rolename>)
endif
cd roles/$(ROLE) && PATH="$(CURDIR)/$(VENV)/bin:$$PATH" molecule test
test-all:
@for role in roles/*/; do \
echo "── Testing $$role ──"; \
cd $$role && PATH="$(CURDIR)/$(VENV)/bin:$$PATH" molecule test; cd ../..; \
done
# ── Playbook execution ────────────────────────────────────────────────────────
check:
ifndef PLAYBOOK
$(error PLAYBOOK is required: make check PLAYBOOK=<name>)
endif
$(PLAYBOOK_BIN) $(INVENTORY) $(VAULT_ARGS) $(if $(LIMIT),--limit $(LIMIT)) $(if $(TAGS),--tags $(TAGS)) --check --diff playbooks/$(PLAYBOOK).yml
deploy:
ifndef PLAYBOOK
$(error PLAYBOOK is required: make deploy PLAYBOOK=<name>)
endif
$(PLAYBOOK_BIN) $(INVENTORY) $(VAULT_ARGS) $(if $(LIMIT),--limit $(LIMIT)) $(if $(TAGS),--tags $(TAGS)) playbooks/$(PLAYBOOK).yml
# ── Vault ─────────────────────────────────────────────────────────────────────
# Streamlined edit: ansible-vault edit decrypts to a temp file, opens nvim, and
# re-encrypts on :wq (abort with :cq) — no plaintext ever lands in the work tree.
# Then validate structure. Override the file with VAULT=<path>.
edit-vault:
EDITOR=nvim $(ANSIBLE)-vault edit $(VAULT)
@$(PYTHON) scripts/check-vault.py $(VAULT)
check-vault:
@$(PYTHON) scripts/check-vault.py $(VAULT)
encrypt:
ifndef FILE
$(error FILE is required: make encrypt FILE=<path>)
endif
$(ANSIBLE)-vault encrypt $(FILE)
decrypt:
ifndef FILE
$(error FILE is required: make decrypt FILE=<path>)
endif
$(ANSIBLE)-vault decrypt $(FILE)
# ── Molecule test image ───────────────────────────────────────────────────────
molecule-image:
docker build -t $(MOLECULE_IMAGE) -f $(MOLECULE_DOCKERFILE) .
molecule-image-push: molecule-image
docker push $(MOLECULE_IMAGE)
caddy-image:
docker build -t boma/caddy-gandi:latest -f roles/reverse_proxy/files/Dockerfile roles/reverse_proxy/files/
# ── Terraform ─────────────────────────────────────────────────────────────────
tf-init:
$(TF_TOKEN_ENV) $(TF) -chdir=terraform/environments/$(TF_ENV) init
tf-plan:
$(TF_TOKEN_ENV) $(TF) -chdir=terraform/environments/$(TF_ENV) plan
tf-apply:
$(TF_TOKEN_ENV) $(TF) -chdir=terraform/environments/$(TF_ENV) apply
tf-output:
$(TF_TOKEN_ENV) $(TF) -chdir=terraform/environments/$(TF_ENV) output -json
tf-inventory:
ifndef TF_ENV
$(error TF_ENV is required: make tf-inventory TF_ENV=<staging|production>)
endif
$(TF) -chdir=terraform/environments/$(TF_ENV) output -json \
| $(PYTHON) scripts/tf_to_inventory.py > inventories/$(TF_ENV)/hosts.yml
@echo "Inventory written to inventories/$(TF_ENV)/hosts.yml"
tf-inventory-offsite:
$(TF_TOKEN_ENV) $(TF) -chdir=terraform/environments/offsite output -json \
| $(PYTHON) scripts/tf_to_inventory.py > inventories/production/offsite.yml
@echo "Offsite inventory written to inventories/production/offsite.yml"
# ── Role scaffolding ──────────────────────────────────────────────────────────
new-role:
ifndef NAME
$(error NAME is required: make new-role NAME=<rolename>)
endif
mkdir -p roles/$(NAME)/tasks roles/$(NAME)/handlers roles/$(NAME)/defaults \
roles/$(NAME)/templates roles/$(NAME)/files roles/$(NAME)/meta \
roles/$(NAME)/molecule/default
echo "---" > roles/$(NAME)/tasks/main.yml
echo "---" > roles/$(NAME)/handlers/main.yml
echo "---" > roles/$(NAME)/defaults/main.yml
echo "---" > roles/$(NAME)/meta/main.yml
printf '# %s\n\nRole description here.\n' "$(NAME)" > roles/$(NAME)/README.md
cp .scaffold/molecule.yml roles/$(NAME)/molecule/default/molecule.yml
sed 's/ROLE_NAME_PLACEHOLDER/$(NAME)/g' .scaffold/converge.yml > roles/$(NAME)/molecule/default/converge.yml
cp .scaffold/verify.yml roles/$(NAME)/molecule/default/verify.yml
@echo "Role $(NAME) scaffolded at roles/$(NAME)/"
@echo "Next: fill in meta/main.yml, defaults/main.yml, tasks/main.yml, README.md"