Eigene App erstellen
Schritt für Schritt vom leeren Repo zur App mit LXC, VM-Image und Update – am Beispiel iac-example
Das Prinzip
Eine iac-App beschreibt nur, was sie braucht: Pakete, Dateien, Ports und ein paar Funktionen („Hooks“). Den Ablauf hat allein iac-core: Container anlegen, Image bauen, Firewall, Update, Übernahme bestehender Systeme, Tests. Eine App enthält deshalb keinen eigenen Installationsablauf und keinen kopierten Code aus anderen Apps.
Alle Wege landen beim selben, wiederholbaren Schritt bin/iac-install (dem „Sollzustand“):
Proxmox-Node Gast (LXC, VM oder Image-Build)
──────────── ───────────────────────────────────────────────────────
ct.sh ──── pct exec ──────────▶ bin/iac-install
vm.sh ──── fertiges Image ────▶ (wurde beim Image-Build im chroot ausgeführt)
update → Debian-Upgrade → app_update → bin/iac-install
adopt.sh → app_adopt_check → Backup → app_adopt_migrate
→ bin/iac-install
Boot-Test → bin/iac-smoke
bin/iac-install:
Baseline (SSH, Benutzer, Firewall-Grundregeln, update)
→ packages.list installieren
→ app_install Benutzer, Verzeichnisse, Downloads
→ rootfs/ einspielen Konfiguration, systemd-Units (+ Firewall aus iac.json)
→ app_configure Konfiguration prüfen, neu laden/starten
→ APP_SERVICES aktivieren und starten
→ app_healthy läuft die App?
→ /etc/iac/instance.json
Weil bin/iac-install bei jedem Lauf denselben Zustand herstellt, muss alles in deiner App
wiederholbar sein: Ein zweiter Lauf darf nichts kaputt machen und nichts doppelt anlegen.
Vorbereitung
Die Repos nebeneinander auschecken:
mkdir -p ~/repositories/iac && cd ~/repositories/iac
git clone https://git.heyer.systems/iac/iac-core.git
git clone https://git.heyer.systems/iac/iac-example.git
Lokal gebraucht werden shellcheck und jq (für Lint) und Docker (zum Ausprobieren ohne
Proxmox).
iac-example ist eine absichtlich kleine App: busybox httpd liefert eine Seite auf Port 8080 aus. Jeder Hook kommt darin genau einmal vor, jede Datei ist kommentiert. Lies sie in der Reihenfolge aus ihrem README, bevor du anfängst. Größere Vorbilder: iac-nginx (Debian-Paket) und iac-traefik (Binary von GitHub, mit Update und Adopt).
1. Gerüst anlegen
iac-core/tools/new-app.sh meineapp # legt iac-meineapp/ an
new-app.sh überschreibt nie etwas. Ein angefangenes Repo ergänzt es um die fehlenden
Dateien (new-app.sh meineapp iac-meineapp). Danach gibt es:
| Datei | Anpassen? |
|---|---|
iac.json | ja – Beschreibung, Ports, Ressourcen |
packages.list | ja – Debian-Pakete |
app.func | ja – die Hooks |
rootfs/ | ja – Konfigurationsdateien, systemd-Units |
tests/smoke.sh | ja – Prüfungen für deine App |
image/manifest.yml | selten – Hostname, Image-Größe |
ct.sh, vm.sh, adopt.sh | nein – nur IAC_APP steht darin |
.forgejo/workflows/*.yml | nein – in allen Apps identisch |
2. iac.json – Steckbrief
{
"name": "meineapp",
"title": "Meine App",
"description": "Ein Satz für die Website.",
"website": "https://meineapp.example/",
"upstream": { "type": "github-release", "repo": "hersteller/meineapp" },
"supports": ["lxc", "vm"],
"defaults": {
"lxc": { "cpu": 1, "ram": 512, "disk": 4 },
"vm": { "cpu": 1, "ram": 1024, "disk": 8 }
},
"ports": [
{ "port": 80, "proto": "tcp", "description": "Web", "access": "public" },
{ "port": 9090, "proto": "tcp", "description": "Admin", "access": "private" }
],
"modules": [
{ "name": "node-exporter", "title": "Prometheus node exporter", "default": false }
],
"access": "Web: http://{ip}/"
}
| Feld | Bedeutung |
|---|---|
name | Kleinbuchstaben, Ziffern, -. Das Repo heißt iac-<name>, die URLs /<name>/ct.sh |
upstream.type | debian (Paket aus Debian), github-release oder other |
supports | lxc braucht ct.sh, vm braucht vm.sh und ein Image, docker eine docker-compose.yml |
defaults | RAM in MiB, Disk in GiB – je Plattform außer docker |
ports | daraus entsteht die Firewall – public für alle, private nur aus privaten Netzen |
modules | als Text = immer installiert, als Objekt = wählbar. Nur vorhandene Module (iac-core/modules/) |
hidden | true = erscheint nicht auf der Website |
3. packages.list – Debian-Pakete
Ein Paket pro Zeile, # für Kommentare. Es ist die einzige Paketliste: LXC, VM-Image und
Adopt nutzen dieselbe. Die Grundpakete (curl, jq, nftables, …) bringt iac-core mit.
4. rootfs/ – Dateien im Gast
Alles unter rootfs/ landet 1:1 im Gast (rootfs/etc/meineapp/config.yml →
/etc/meineapp/config.yml) und gehört iac: Bei jedem update wird es abgeglichen, was aus
dem Repo verschwindet, wird auch im Gast entfernt. Solche Dateien beginnen mit
# managed by iac – do not edit.
- Eigene Anpassungen der Nutzer gehören nie in diese Dateien, sondern in Dateien, die iac
nicht anfasst – z. B.
config.local.yml,/etc/nftables.d/90-local.nftodersystemctl edit. Plane solche Stellen in deiner App ein (iac-example:/etc/iac-example/index.html). rootfs.lxc/,rootfs.vm/: nur für eine Plattform.rootfs.image/: nur beim Image-Build, einmalig, danach nicht verwaltet.rootfs.perms: Rechte, die vom Standard (0644 root:root, 0755 für Ausführbares) abweichen:/etc/meineapp/secret.yml 0640 root:meineapp.- Eine Firewall-Datei brauchst du nicht – sie entsteht aus
ports.
5. app.func – die Hooks
| Hook | Wann | Wofür |
|---|---|---|
APP_SERVICES=(…) | Installation, Smoke-Test | systemd-Units, die aktiviert, gestartet und geprüft werden |
app_installed_version | Pflicht | installierte Version, leer wenn nicht installiert |
app_latest_version | Pflicht für VM | neueste Version – benennt das Image, läuft auf dem CI-Runner |
app_install | vor rootfs/ | Benutzer, Verzeichnisse, Downloads (Pakete sind schon da) |
app_configure | nach rootfs/ | Konfiguration prüfen, bei Änderung neu laden/starten |
app_healthy | Ende der Installation, Smoke-Test | funktioniert die App? Ein paar Sekunden Geduld einbauen |
app_update | bei update, vor der Installation | neue Upstream-Version holen (Debian-Pakete macht iac-core) |
app_adopt_check | adopt.sh, vor der Rückfrage | bestehende Installation prüfen – nichts ändern, sonst iac_adopt_refuse "Grund" |
app_adopt_migrate | adopt.sh, nach dem Backup | vorhandene Daten ins iac-Layout überführen |
APP_ADOPT_BACKUP=(…) | adopt.sh | Pfade, die vorher gesichert werden |
Alles außer den beiden Versionen ist optional – was du nicht brauchst, löschst du.
Nützliche Funktionen aus iac-core: msg_info, msg_ok, msg_warn, die,
iac_run "Text" befehl… (Ausgabe nur bei Fehler), iac_download, iac_verify_sha256,
iac_changed '/etc/meineapp/*' (hat die Installation diese Dateien gerade geändert?),
iac_in_chroot (läuft gerade der Image-Build?), iac_service_restart, iac_version_gt.
Zwei Regeln, die oft übersehen werden:
- Image-Build: Im chroot läuft kein Dienst.
iac_service_restarttut dort nichts,app_healthywird übersprungen. Eigenesystemctl restartimmer mitiac_in_chroot ||schützen. - Nur bei Änderung neu starten:
iac_changedstatt bei jedem Lauf neu zu starten – sonst unterbricht jedesupdateden Dienst.
6. tests/smoke.sh – Prüfungen
iac-core prüft schon: instance.json, APP_SERVICES aktiv, app_healthy, alle Ports aus
iac.json 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 "Beschreibung" 'Befehl' bricht nie ab: Alle Prüfungen laufen, fehlgeschlagene zeigen
ihren Befehl und ihre Ausgabe. Derselbe Test läuft im Boot-Test jedes Images und von Hand in
jedem Gast: bash /var/lib/iac/src/core/bin/iac-smoke.
7. Testen
Lint – findet Tippfehler, falsche Felder und kopierte Reste:
iac-core/tools/lint.sh iac-meineapp
Ohne Proxmox – in einem Docker-Container mit systemd:
iac-core/tools/dev-container.sh iac-meineapp # installiert und führt den Smoke-Test aus
docker exec -it iac-dev-meineapp bash # hineinschauen, Log: /var/log/iac/iac.log
iac-core/tools/dev-container.sh iac-meineapp # nach Änderungen erneut = wie "update"
Der Container ist kein echtes LXC (privilegiert, kein cloud-init), für die App-Logik reicht er.
Auf Proxmox – einen Branch als LXC, bevor er nach main geht:
IAC_APP_REF=mein-branch bash -c "$(curl -fsSL https://git.heyer.systems/iac/iac-meineapp/raw/branch/mein-branch/ct.sh)"
Im Gast gibt IAC_VERBOSE=1 bash /var/lib/iac/src/core/bin/iac-install alle Ausgaben direkt aus.
CI – passiert automatisch:
| Workflow | Wann | Was |
|---|---|---|
lint.yml | jeder Push, jeder Pull Request | tools/lint.sh im Container |
image.yml | Push auf main, montags | Image bauen → in QEMU booten → Boot-Test inkl. iac-smoke → veröffentlichen |
Nur ein Image, das den Boot-Test besteht, wird veröffentlicht. VM-Images entstehen nur aus main.
8. Veröffentlichen
- Repo
iac-meineappin der Organisation iac anlegen (öffentlich) und pushen. - Der erste Push auf main baut das VM-Image (gut 10 Minuten).
- Die Website liest stündlich alle öffentlichen
iac-*-Repos mitiac.jsonein – danach gibt es die App-Seite mit Generator und die kurzen URLs/meineapp/ct.shund/meineapp/vm.sh.
Häufige Fehler
| Symptom | Ursache |
|---|---|
| Workflow läuft nie | Datei liegt in .forgejo/ statt in .forgejo/workflows/ |
Module … not found (Lint: module … does not exist) | in modules steht ein Modul, das es (noch) nicht gibt |
| Fremde Firewall-Regeln, falscher Name im Log | Dateien aus einer anderen App kopiert – lieber new-app.sh |
| Image-Build bricht im chroot ab | Dienst wird gestartet, obwohl kein systemd läuft – iac_in_chroot |
Dienst startet bei jedem update neu | Neustart ohne iac_changed |
app_installed_version returns nothing | die Version wird falsch ermittelt oder die Installation in app_install fehlt |
Eigene Änderungen nach update weg | Änderung in einer managed by iac-Datei statt in einer lokalen Datei |
lint.sh erkennt die ersten drei Fehler selbst.
Wenn die Hooks nicht reichen
- Datenbank, Cache … gehören als wiederverwendbares Modul nach
iac-core/modules/(Aufbau:iac-core/modules/README.md) – nicht in jede App einzeln. - Völlig eigener Ablauf: Liegt eine
install.shim App-Repo, ruft iac-core diese statt der Hooks auf. Das ist die Ausnahme – dann musst du Baseline, Overlay undinstance.jsonselbst über die Funktionen ausiac-core/lib/guest.funcanstoßen.