Mon infra as codé avec ansible, merci claude
  • Shell 81.8%
  • Jinja 9.1%
  • Makefile 5.1%
  • Dockerfile 2.9%
  • Python 1.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
chipster e91ba15186
All checks were successful
deploy-lescuretie / deploy-lescuretie (push) Successful in 1m39s
deploy / deploy (push) Successful in 2m21s
tailscale: sysctl dans /etc/sysctl.d, Debian 13 ignore /etc/sysctl.conf au boot
Le fix b371684 écrivait ip_nonlocal_bind dans /etc/sysctl.conf (défaut du
module), que systemd-sysctl ne lit plus sous Trixie : valeur à 0 au reboot du
2026-09-26, mosquitto de nouveau mort 24 h. Même correction pour ip_forward.
2026-09-27 20:13:37 +02:00
.forgejo/workflows deploy: injecte GLITCHTIP_SECRET_KEY / GLITCHTIP_DB_PASSWORD depuis les Forgejo Secrets 2026-09-10 19:17:26 +02:00
ansible tailscale: sysctl dans /etc/sysctl.d, Debian 13 ignore /etc/sysctl.conf au boot 2026-09-27 20:13:37 +02:00
docs docs: synthese des arbitrages et points ouverts des deux chantiers CV 2026-09-18 00:24:24 +02:00
scripts monitoring: timer systemd quotidien pour la recette CV, notif ntfy sur échec 2026-09-17 21:52:05 +02:00
stacks timothy-cv: bump submodule (fixes C3/I2/I5/I1, C2, I6, M1/M2) 2026-09-18 00:13:38 +02:00
.ansible-lint feat(nginx): auto-disable vhosts dont le cert LE manque (anti-crashloop) 2026-05-02 19:00:44 +02:00
.gitignore secu: .gitignore stacks/**/.env (FIX: scrub .env du dernier commit, secrets à rotate) 2026-05-12 14:05:02 +02:00
.gitmodules timothy-cv: stack du site CV (submodule + compose durci) 2026-09-17 20:11:36 +02:00
CLAUDE.md cv: retire les en-têtes dupliqués, recette téléphone réelle et cert TLS vérifié 2026-09-17 20:50:11 +02:00
Makefile feat(makefile): target deploy-lescuretie 2026-08-01 21:57:07 +02:00
README.md docs: README + CLAUDE.md décrivent le setup multi-host 2026-08-02 13:32:28 +02:00

infra

Infrastructure as code pour les serveurs des maisons A et C :

  • Maison A (Argenteuil) — ThinkCentre Lenovo, host de prod complet (foundation + reverse-proxy + stacks non-HA + runner Forgejo). Voir playbooks/site.yml.
  • Maison C (Lescuretie) — Raspberry Pi 4B 4 GB, foundation seule (common + docker + tailscale), destiné à recevoir HA/Z2M/MQTT via le repo Homeassistant. Voir playbooks/site-lescuretie.yml.

Maison B (au fieu) est gérée par un autre repo (tofu-maison, OpenTofu) — Proxmox + LXC Headscale.

Schéma

Topologie réseau & services

flowchart TB
    Net((Internet))
    LE[/Let's Encrypt/]
    NtfySh[/ntfy.sh<br/>APNs upstream/]
    Anthropic[/Anthropic API/]
    DERP[/Tailscale DERP relays<br/>traversée NAT/]
    PeerB[Maison B<br/>tailscale up]
    Devices[Devices roaming<br/>laptop/phone]
    Box["Box internet maison A<br/>port-forward 80, 443"]
    MCU[MCU Ender 3<br/>USB CH340]
    Sonoff[Sonoff Zigbee USB]

    Net -->|TCP 80, 443| Box
    Net <-.->|HTTPS| DERP

    subgraph TC["ThinkCentre — Debian 13"]
      direction TB

      subgraph HOST["network mode: host"]
        TS["tailscaled<br/>100.64.0.1"]
        HA["Home Assistant<br/>:8123"]
        HABR["ha-bridge<br/>127.0.0.1:8765"]
        KLIPPER["klipper<br/>privileged"]
        CROWS["crowsnest<br/>(profile camera)"]
        Runner["forgejo-runner<br/>systemd"]
      end

      subgraph WEB["réseau Docker  ’web’  (172.20.0.0/16)"]
        NGINX["nginx + certbot<br/>:80 :443"]
        MARIE[mariepro]
        CHESS["chessia<br/>:8000"]
        NTFY["ntfy<br/>:80"]
        MOSQ["mosquitto<br/>:1883"]
        Z2M[zigbee2mqtt]
        MOON["moonraker<br/>:7125"]
        MAIN["mainsail<br/>:80"]
        RunnerScribe["forgejo-runner-scribe<br/>Docker — scribe-linux:host<br/>+ scribe-ci:docker://node"]
      end

      SCRIBE[("/srv/scribe-updates/<br/>bundles + manifests")]
      DATA[("/srv/stacks/&lt;name&gt;/<br/>+ /srv/letsencrypt/")]
    end

    Box --> NGINX
    KLIPPER -.- MCU
    Z2M -.- Sonoff
    PeerB <-.->|tailscale| DERP
    Devices <-.->|tailscale| DERP
    DERP <-.->|tailscale| TS

    NGINX -->|marie.afanderivera.fr| MARIE
    NGINX -->|chess.timothy.greil.fr| CHESS
    NGINX -->|ntfy.timothy.greil.fr| NTFY
    NGINX -->|home.timothy.greil.fr<br/>host.docker.internal:8123| HA
    NGINX -->|print.timothy.greil.fr<br/>LAN+Tailscale only| MOON
    NGINX -->|print.timothy.greil.fr| MAIN
    NGINX -->|print.timothy.greil.fr<br/>/webcam/| CROWS
    NGINX -->|scribe.timothy.greil.fr<br/>static updater endpoint| SCRIBE

    HA <-->|mqtt 127.0.0.1:1883| MOSQ
    Z2M <-->|mqtt mosquitto:1883| MOSQ
    HA -->|http 127.0.0.1:8765| HABR
    MOON <-->|uds| KLIPPER

    NGINX -.->|webroot challenge| LE
    NTFY -.->|relay pour iOS| NtfySh
    CHESS -.-> Anthropic
    HABR -.->|claude CLI| Anthropic

    NGINX -.-> DATA
    MARIE -.-> DATA
    CHESS -.-> DATA
    NTFY -.-> DATA
    MOSQ -.-> DATA
    KLIPPER -.-> DATA

    classDef infra fill:#e3f2fd,stroke:#1976d2,color:#0d47a1;
    classDef ha    fill:#fff3e0,stroke:#f57c00,color:#e65100;
    classDef print fill:#f3e5f5,stroke:#8e24aa,color:#4a148c;
    classDef ext   fill:#f5f5f5,stroke:#9e9e9e,color:#424242,stroke-dasharray:4 3;
    classDef data  fill:#ede7f6,stroke:#5e35b1,color:#311b92;
    class NGINX,MARIE,CHESS,NTFY,TS,Runner,RunnerScribe infra;
    class HA,HABR,MOSQ,Z2M ha;
    class KLIPPER,MOON,MAIN,CROWS print;
    class Net,LE,NtfySh,Anthropic,PeerB,Box,DERP,Devices,MCU,Sonoff ext;
    class DATA data;

Légende :

  • 🔵 services du repo Infra (foundation OS + reverse-proxy + stacks non-HA + client Tailscale)
  • 🟠 services du repo Homeassistant-timothy (HA + Z2M + MQTT + ha-bridge)
  • 🟣 services Klipper (compose dans Infra, configs dans submodule Printer.git)
  • ⚪ ressources externes (LE, ntfy.sh, Anthropic, DERP relays, peers VPN, box opérateur, hardware USB)
  • 🟣 stockage persistant sur le host (/srv/stacks/<name>/data/ + certs LE)
  • Flèches pleines : trafic applicatif (HTTP, MQTT, uds, etc.)
  • Flèches pointillées : dépendances externes / hardware / écritures disque / VPN

CI/CD

flowchart LR
    Dev[git push origin main]
    Forgejo[Forgejo Actions<br/>deploy.yml]
    Runner["forgejo-runner<br/>thinkcentre:host"]
    Ansible["ansible-playbook site.yml<br/>(common, docker, nginx,<br/>stacks, tailscale)"]
    Compose["docker compose up -d<br/>par stack /srv/stacks/&lt;name&gt;/"]
    Secrets[/"Forgejo Actions Secrets:<br/>TAILSCALE_AUTH_KEY, CHESSIA_*"/]
    Submod[/"Submodules<br/>infra/stacks/mariepro/src → Mariewebsite.git<br/>infra/stacks/chessia/src → chessia.git<br/>infra/stacks/klipper/src → Printer.git"/]

    Dev --> Forgejo
    Forgejo -->|inject env| Secrets
    Forgejo --> Runner
    Submod -.->|checkout recursive| Runner
    Runner --> Ansible
    Ansible --> Compose

    classDef ext fill:#f5f5f5,stroke:#9e9e9e,color:#424242,stroke-dasharray:4 3;
    classDef step fill:#e8f5e9,stroke:#2e7d32,color:#1b5e20;
    class Dev,Secrets,Submod ext;
    class Forgejo,Runner,Ansible,Compose step;

Stack

  • OS : Debian 13 Trixie
  • Orchestration : Ansible (playbooks idempotents) + docker-compose
  • Reverse-proxy : nginx (containerisé) + certbot, terminaison TLS directe
  • VPN : client Tailscale uniquement. Le control server (Headscale) vit chez maison B (https://headscale.greil.fr, géré par le repo tofu-maison). Tous les peers (ThinkCentre, devices roaming, peer maison B et futures maisons) s'y enregistrent.
  • CI/CD : Forgejo Actions, runner natif systemd sur le ThinkCentre
  • Secrets : Forgejo Actions Secrets (injectés dans l'env du job, lus par Ansible via lookup('env', …))

Périmètre de ce repo

Foundation OS + reverse-proxy + services "non-HA" :

Service Type Public ?
nginx + certbot Docker compose oui (80/443)
ntfy Docker compose oui (ntfy.timothy.greil.fr)
ChessIA Docker compose (build depuis submodule chessia.git) oui
Marie (mariepro) Docker compose (nginx:alpine static, build depuis submodule Mariewebsite) oui
Klipper / Mainsail / Moonraker / Crowsnest Docker compose (configs depuis submodule Printer.git) non — print.timothy.greil.fr LAN+Tailscale only
Beszel Docker compose non — LAN-only sur :8090 (métriques host + containers, alertes)
Dozzle Docker compose non — LAN-only sur :8888 (logs containers live)
CrowdSec Docker compose non — agent observer (parse logs nginx + sshd, pas d'enforcement en phase 1)
Tailscale systemd tailscaled (apt) non — outbound only, s'enregistre sur Headscale chez maison B
Forgejo runner systemd natif —

Hors périmètre (vivent dans le repo Homeassistant-timothy) : Home Assistant, Zigbee2MQTT, Mosquitto MQTT, ha-bridge.

Les deux repos partagent le réseau Docker web créé par ce repo.

Layout

infra/
├── ansible/
│   ├── ansible.cfg
│   ├── inventory.yml                  # 2 groupes : argenteuil (thinkcentre, local), lescuretie (raspi, ssh via tailnet)
│   ├── requirements.yml               # collections (community.docker, community.general)
│   ├── playbooks/
│   │   ├── site.yml                   # convergence Argenteuil (ThinkCentre) — CI deploy.yml
│   │   ├── site-lescuretie.yml        # convergence Lescuretie (Pi 4B) — CI deploy-lescuretie.yml
│   │   └── bootstrap.yml              # one-shot manuel pour Debian vierge (ThinkCentre)
│   ├── roles/
│   │   ├── common/                    # apt, packages base, sudoers, sshd
│   │   ├── docker/                    # engine + compose plugin + réseau "web"
│   │   ├── runner/                    # Forgejo runner natif systemd (Argenteuil uniquement)
│   │   ├── nginx/                     # déploie stack nginx + reload sur changement de conf (Argenteuil uniquement)
│   │   ├── stacks/                    # déploie chaque compose dans stacks/*/ (Argenteuil uniquement)
│   │   └── tailscale/                 # apt repo officiel + tailscaled + register sur Headscale (maison B)
│   └── group_vars/
│       ├── all/vars.yml               # variables non-secrètes partagées (paths, domaines, pubkeys SSH)
│       └── lescuretie/vars.yml        # overrides Lescuretie (advertise-routes 192.168.117.0/24)
├── stacks/
│   ├── nginx/                         # nginx:alpine + certbot, vhosts par domaine
│   ├── ntfy/                          # binwiederhier/ntfy:latest
│   ├── chessia/                       # build depuis submodule src/ (chessia.git)
│   ├── mariepro/                      # Dockerfile nginx:alpine + COPY src/ (Mariewebsite)
│   ├── beszel/                        # henrygd/beszel hub + agent (métriques)
│   ├── dozzle/                        # amir20/dozzle (logs containers)
│   ├── crowdsec/                      # crowdsecurity/crowdsec (IDS, phase 1 observer)
│   └── klipper/                       # mkuf/klipper + mkuf/moonraker + mainsail-crew/*
│       └── src/                       # submodule Printer.git (configs)
├── scripts/
│   └── migrate-from-rpi.sh            # rsync runtime data RPi → ThinkCentre au D-day
├── .forgejo/workflows/
│   ├── deploy.yml                     # push main → converge ThinkCentre (site.yml)
│   └── deploy-lescuretie.yml          # push main + path filter → converge Pi Lescuretie (site-lescuretie.yml, ssh via tailnet)
├── Makefile
└── docs/
    ├── migration.md                   # runbook D-day RPi → ThinkCentre
    └── runbook.md                     # opérations courantes

Workflows

Bootstrap initial (one-shot, depuis le laptop)

Sur un ThinkCentre fraîchement installé en Debian 13 :

make bootstrap HOST=<ip-thinkcentre> RUNNER_UUID=<uuid> RUNNER_TOKEN=<token>

RUNNER_UUID et RUNNER_TOKEN sont retournés ensemble par Site Administration → Actions → Runners → Create new Runner (Forgejo 15+ crée le runner directement côté backend et affiche les credentials persistants).

Cela exécute ansible/playbooks/bootstrap.yml qui :

  1. Durcit SSH (PermitRootLogin no, password auth off après ajout de la clé)
  2. Crée le user chipster avec sudo
  3. Installe les paquets de base (vim curl git ufw fail2ban borgbackup wireguard)
  4. Installe Docker engine + compose plugin
  5. Crée le réseau Docker web
  6. Installe le Forgejo runner natif (systemd) et l'enregistre

Déploiement continu

  • Argenteuil — tout push sur main déclenche deploy.yml → runner local exécute make deploy (= ansible-playbook playbooks/site.yml).
  • Lescuretie — un push touchant les fichiers pertinents (ansible/inventory.yml, group_vars/{all,lescuretie}/**, roles/{common,docker,tailscale}/**, playbooks/site-lescuretie.yml, Makefile) déclenche deploy-lescuretie.yml → même runner, exécute make deploy-lescuretie en SSHant vers le Pi via tailnet. Les autres pushs ne re-convergent pas Lescuretie (évite les runs stériles).

Bootstrap Lescuretie (one-shot, en local sur le Pi)

Pour un Pi Debian vierge à Lescuretie, avant que la CI puisse prendre le relais :

# Sur le Pi lui-même :
TAILSCALE_AUTH_KEY='hskey-auth-...' \
  ansible-playbook -i ansible/inventory.yml \
  -c local -e ansible_host=127.0.0.1 -l lescuretie \
  ansible/playbooks/site-lescuretie.yml

La pre-auth key est générée sur le Headscale de maison B (headscale --user <ID> preauthkeys create --reusable=false --expiration 2h). Une fois le Pi enrôlé et le subnet 192.168.117.0/24 approuvé côté Headscale (headscale nodes approve-routes -i <ID> -r 192.168.117.0/24), les convergences suivantes passent par la CI (Option B — runner Argenteuil SSH via tailnet).

Secrets

Stockés dans Forgejo Actions Secrets. Consommés par .forgejo/workflows/deploy.yml.

User-level (chipster, réutilisables par tous ses repos qui publient sur ntfy) :

  • Variable NTFY_SERVER — https://ntfy.timothy.greil.fr
  • Variable NTFY_USER — login d'auth ntfy (publish-only)
  • Secret NTFY_PASSWORD — password ntfy

Repo-level (chipster/Infra uniquement) :

  • TAILSCALE_AUTH_KEY — pre-auth key argenteuil fournie par Mat (Headscale chez maison B)
  • CHESSIA_CLAUDE_CREDENTIALS_JSON — OAuth Claude Code (abonnement Max) du conteneur ChessIA
  • CHESSIA_NTFY_TOPIC — topic ntfy spécifique à ChessIA (ex: chessia-diag)

RUNNER_UUID + RUNNER_TOKEN sont les credentials persistants retournés par Forgejo lors du Create new Runner (Site Admin → Actions → Runners). Ils sont passés au bootstrap et stockés dans Forgejo Actions Secrets pour que la convergence CI puisse re-templater config.yaml à l'identique (idempotent).

Documents

  • docs/migration.md — runbook D-day pour migrer RPi → ThinkCentre
  • docs/runbook.md — opérations courantes (renew cert manuel, ajouter un vhost, debug Tailscale)