Entwickeln
App-Referenz
Alle Felder der iac.json, die Variablen und Hilfsrollen von iac-core, Tests, CI und häufige Fehler.
So läuft eine Installation
Alle Wege – Container, Image-Build, erster Start einer VM, update, Übernahme – landen beim
selben Playbook im Gast:
Proxmox-Host Gast (LXC, VM oder Image-Build)
──────────── ───────────────────────────────────────────────────────
ct.sh ──── pct exec ──────────▶ bin/iac-install → ansible-playbook site.yml
vm.sh ──── fertiges Image ────▶ (lief beim Image-Build im chroot, beim ersten Boot erneut)
update → Debian-Upgrade → bin/iac-install (iac_mode=update)
adopt.sh → tasks/adopt_check.yml → Backup
→ bin/iac-install (iac_mode=adopt)
site.yml:
iac_facts iac.json, Instanz, Einstellungen, Secrets, Module
iac_baseline SSH, Grundpakete, Firewall-Grundregeln, update
iac_modules PostgreSQL, Valkey, …
DEINE APP tasks/main.yml ← das schreibst du
iac_finish Handler (Neustarts) → Firewall aus "ports" → "services" starten
→ "health" prüfen → /etc/iac/instance.json
ct.sh, vm.sh und adopt.sh liegen nur in iac-core (entry/). Die Website liefert sie für jede
App mit deren Namen aus. adopt.sh gibt es, sobald die App tasks/adopt_check.yml hat.
iac.json
| Feld | Bedeutung |
|---|---|
name | Kleinbuchstaben, Ziffern, -. Das Repo heißt iac-<name>, die Einzeiler /<name>/ct.sh |
title, description | Name und ein Satz für die Website |
website | Projektseite der Software |
upstream | woher die Version kommt: {"type": "debian", "repo": "<paket>"}, {"type": "github-release", "repo": "<owner/repo>"}, {"type": "url", "url": "https://…"} (erste Zeile = Version) oder {"type": "other", "version": "…"}. Mit "major_lock": true bleibt update in der installierten Major-Version |
supports | lxc, vm (mit VM-Image), docker (mit docker-compose.yml im Repo) |
defaults | Ressourcen je Plattform: cpu, ram (MiB), disk (GiB). Der Boot-Test nutzt den VM-RAM (höchstens 2,5 GiB) |
image.size | Größe der Root-Partition im VM-Image, z. B. "8GiB" (Standard 3GiB); die Disk wächst beim Anlegen |
ports | daraus entsteht die Firewall: port, proto (tcp/udp), description, access (public = alle, private = nur private Netze) |
services | systemd-Units der App: iac-core aktiviert und startet sie, der Smoke-Test prüft sie |
health | url, die nach jeder Installation antworten muss; status = erlaubte HTTP-Codes (Standard [200]) |
secrets | pro Instanz einmal erzeugt, nie im Image: Name oder {"name": …, "length": 32} → iac_settings.<name> |
settings | Einstellungen beim Anlegen (var_<name>): name, title, description, secret → iac_settings.<name> |
modules | nur, was die App braucht: Text = Pflichtmodul, {"name": …, "default": true} = vorausgewählt. Alle anderen Module sind ohnehin wählbar (Module) |
adopt.backup | Pfade, die adopt.sh vor der Übernahme sichert |
access | Hinweis nach dem Anlegen, {ip} wird ersetzt |
icon | Logo auf der Website: Slug bei selfh.st/icons (Standard: name) |
hidden | true = erscheint nicht auf der Website |
Das vollständige Schema: iac-core/schema/iac.schema.json.
Variablen
Diese Variablen setzt iac-core, deine Tasks dürfen sie lesen:
| Variable | Inhalt |
|---|---|
iac_build | true im Image-Build (chroot: kein Dienst läuft, keine Secrets, keine Datenbank) |
iac_mode | install, update oder adopt |
iac_platform | lxc oder vm |
iac_settings | Einstellungen und Secrets der Instanz, z. B. iac_settings.meineapp_db_password |
iac_modules_active | aktive Module |
iac_json | die ganze iac.json |
iac_app_version_pin | Version, die der Image-Build vorgibt (sonst leer) |
iac_upstream_latest | neueste Version laut upstream (wird erst bei Bedarf abgefragt) |
Alle mit Erklärung: Kopf von iac-core/ansible/site.yml.
Versionen
Debian-Pakete: Mit "upstream": {"type": "debian", "repo": "<paket>"} liest iac-core die
Version selbst aus. Die App muss nichts tun.
Alles andere (GitHub-Release, eigene Download-Seite): Die Rolle iac_version entscheidet, was
installiert wird. Du sagst ihr nur, wie sie die installierte Version erfährt:
- name: Version to install
ansible.builtin.include_role:
name: iac_version
vars:
iac_version_command: /usr/local/bin/meineapp --version
iac_version_regex: 'v?([0-9.]+)' # optional: erste Gruppe = Version
- name: "Install meineapp {{ iac_version_target }}"
when: iac_version_target != iac_version_installed
block:
- name: Download (checksum from the release)
ansible.builtin.get_url:
url: "https://github.com/hersteller/meineapp/releases/download/v{{ iac_version_target }}/meineapp.tar.gz"
dest: /tmp/meineapp.tar.gz
checksum: "sha256:https://github.com/hersteller/meineapp/releases/download/v{{ iac_version_target }}/SHA256SUMS"
mode: "0644"
# … entpacken, installieren, notify: Restart meineapp
| Lage | iac_version_target |
|---|---|
| nicht installiert | Version des Images (iac_app_version_pin) oder die neueste |
update (und Übernahme) | die neueste, wenn sie neuer ist – mit "major_lock": true nur innerhalb der Major-Version, eine neue Major-Version wird gemeldet |
| sonst | die installierte |
iac_version_installed ist leer, solange nichts installiert ist. Die Version für
instance.json setzt die Rolle selbst. Vorbilder: iac-traefik (Binary, Update mit Rollback),
iac-netbox (Release je Verzeichnis), iac-ai-workstation.
Hilfsrollen
Dienst aktivieren und starten, auch im Image-Build (dort nur aktivieren):
- ansible.builtin.include_role:
name: iac_service
vars:
iac_service: meineapp-worker
Für die Dienste in services brauchst du das nicht – die startet iac-core selbst.
Datenbank: postgresql in modules, dann:
- name: Database
ansible.builtin.include_role:
name: postgresql
tasks_from: database
vars:
postgresql_db: meineapp
postgresql_user: meineapp
postgresql_password: "{{ iac_settings.meineapp_db_password }}"
when: not iac_build
Dateien im Gast
- Was deine Tasks per
copy/templateablegen, gehört iac und wird bei jedem Lauf abgeglichen. Solche Dateien beginnen mit# managed by iac – do not edit. - Anpassungen der Nutzer gehören in Dateien, die iac nicht anfasst – plane dafür eine
local-Datei ein (iac-netbox:/etc/netbox/configuration.local.py). - Eine Firewall-Datei brauchst du nicht – sie entsteht aus
ports. - Secrets gehören nie ins Repo:
secretsiniac.json, im Template{{ iac_settings.name }}, Tasks damit bekommenno_log: true.
Smoke-Test
iac-core prüft schon instance.json, die services, health und die Ports in der Firewall. In
tests/smoke.sh steht nur, was speziell für deine App ist:
check "API antwortet" 'curl -fsS http://127.0.0.1:9090/api/health | jq -e .ok'
check "läuft als eigener Nutzer" 'ps -o user= -C meineapp | grep -qx meineapp'
check bricht nie ab: Alle Prüfungen laufen, fehlgeschlagene zeigen Befehl und Ausgabe. Derselbe
Test läuft im Boot-Test jedes Images und von Hand im Gast (bash /var/lib/iac/src/core/bin/iac-smoke).
Übernahme
| Datei | Wann | Was |
|---|---|---|
tasks/adopt_check.yml | vor der Rückfrage | prüfen – nichts ändern. Ablehnen mit ansible.builtin.fail und Grund |
tasks/adopt.yml | nach Rückfrage und Backup (adopt.backup), vor tasks/main.yml | vorhandene Daten ins iac-Layout überführen |
Vorbild: iac-traefik (übernimmt die eigene Konfiguration als traefik.local.yaml). Geht es nicht
automatisch, gehört eine Anleitung nach docs/MIGRATION.md – die Website verlinkt sie.
Testen
Lint – Tippfehler, falsche Felder, Ansible-Syntax, Reste alter Apps:
iac-core/tools/lint.sh iac-meineapp
Ohne Proxmox – Docker-Container mit systemd:
iac-core/tools/dev-container.sh iac-meineapp # installieren + Smoke-Test
iac-core/tools/dev-container.sh iac-meineapp # erneut = wie update
IAC_APP_VERSION=1.2.3 iac-core/tools/dev-container.sh iac-meineapp # bestimmte Version
docker exec -e IAC_MODE=update iac-dev-meineapp \
bash /var/lib/iac/src/core/bin/iac-install # update-Pfad der App
Im zweiten Lauf sollte changed=1 stehen (nur instance.json) – dann ist die App idempotent. Der
Container ist kein echtes LXC (privilegiert, kein cloud-init), für die App-Logik reicht er.
Auf Proxmox – ein Branch, bevor er nach main geht:
IAC_APP_REF=mein-branch bash -c "$(curl -fsSL https://iac.heyer.systems/meineapp/ct.sh)"
Im Gast zeigt IAC_VERBOSE=1 bash /var/lib/iac/src/core/bin/iac-install alle Task-Ergebnisse.
CI
Die Workflows sind in allen Apps gleich und bestehen nur aus einem Aufruf. Die Schritte liegen in
iac-core (ci/lint, ci/image):
| Workflow | Wann | Was |
|---|---|---|
lint.yml | jeder Push, jeder Pull Request | tools/lint.sh im Container |
image.yml | Push auf main, montags | Lint → Image bauen → in QEMU booten → Boot-Test inkl. iac-smoke → veröffentlichen |
Nur ein Image, das den Boot-Test besteht, wird veröffentlicht. Hostname und Größe des Images kommen
aus iac.json. Für mehr (zusätzliche bootstrap-vz-Einstellungen) gibt es optional
image/manifest.yml.
Eigene Module
Was mehrere Apps brauchen können (Datenbank, Cache, Monitoring …), gehört als Modul nach iac-core – dann kann jede App es nutzen:
- Rolle
iac-core/ansible/roles/<modul>/mitmodule_state:presentinstalliert und startet,absententfernt Pakete und Dateien wieder – Daten bleiben. - Firewall-Regeln des Moduls in
/etc/nftables.d/40-<modul>.nft. - Eintrag in
iac-core/modules.json:title,description,supports(lxc,vm).
Ein Modul nur für eine App liegt in roles/<modul>/ im App-Repo. Aufbau und Beispiele:
iac-core/ansible/roles/README.md.
Häufige Fehler
| Symptom | Ursache |
|---|---|
| Workflow läuft nie | Datei liegt in .forgejo/ statt in .forgejo/workflows/ |
Lint: module … does not exist | in modules steht ein Modul, das es (noch) nicht gibt |
Lint: … belongs to the old layout | app.func, packages.list oder rootfs/ aus einer alten App kopiert |
Lint: ct.sh is not used any more | alte Einstiegsskripte im Repo – löschen, die Website liefert sie |
| Image-Build bricht im chroot ab | Dienst wird gestartet oder Datenbank angesprochen – when: not iac_build |
Dienst startet bei jedem update neu | Neustart als Task statt als Handler mit notify |
neither the role iac_version nor iac_app_version | App von außerhalb Debians ohne Rolle iac_version |
Eigene Änderungen nach update weg | Änderung in einer managed by iac-Datei statt in einer local-Datei |
item is undefined in einer Schleife | Schleife über etwas, das fehlen kann – erst set_fact, dann loop |
Ansible-Wissen
Verfügbar ist ansible-core aus Debian, also die Module aus ansible.builtin – keine weiteren
Collections: Dokumentation der eingebauten Module.