heyer.systems

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

FeldBedeutung
nameKleinbuchstaben, Ziffern, -. Das Repo heißt iac-<name>, die Einzeiler /<name>/ct.sh
title, descriptionName und ein Satz für die Website
websiteProjektseite der Software
upstreamwoher 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
supportslxc, vm (mit VM-Image), docker (mit docker-compose.yml im Repo)
defaultsRessourcen je Plattform: cpu, ram (MiB), disk (GiB). Der Boot-Test nutzt den VM-RAM (höchstens 2,5 GiB)
image.sizeGröße der Root-Partition im VM-Image, z. B. "8GiB" (Standard 3GiB); die Disk wächst beim Anlegen
portsdaraus entsteht die Firewall: port, proto (tcp/udp), description, access (public = alle, private = nur private Netze)
servicessystemd-Units der App: iac-core aktiviert und startet sie, der Smoke-Test prüft sie
healthurl, die nach jeder Installation antworten muss; status = erlaubte HTTP-Codes (Standard [200])
secretspro Instanz einmal erzeugt, nie im Image: Name oder {"name": …, "length": 32} → iac_settings.<name>
settingsEinstellungen beim Anlegen (var_<name>): name, title, description, secret → iac_settings.<name>
modulesnur, was die App braucht: Text = Pflichtmodul, {"name": …, "default": true} = vorausgewählt. Alle anderen Module sind ohnehin wählbar (Module)
adopt.backupPfade, die adopt.sh vor der Übernahme sichert
accessHinweis nach dem Anlegen, {ip} wird ersetzt
iconLogo auf der Website: Slug bei selfh.st/icons (Standard: name)
hiddentrue = 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:

VariableInhalt
iac_buildtrue im Image-Build (chroot: kein Dienst läuft, keine Secrets, keine Datenbank)
iac_modeinstall, update oder adopt
iac_platformlxc oder vm
iac_settingsEinstellungen und Secrets der Instanz, z. B. iac_settings.meineapp_db_password
iac_modules_activeaktive Module
iac_jsondie ganze iac.json
iac_app_version_pinVersion, die der Image-Build vorgibt (sonst leer)
iac_upstream_latestneueste 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
Lageiac_version_target
nicht installiertVersion 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
sonstdie 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/template ablegen, 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: secrets in iac.json, im Template {{ iac_settings.name }}, Tasks damit bekommen no_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

DateiWannWas
tasks/adopt_check.ymlvor der Rückfrageprüfen – nichts ändern. Ablehnen mit ansible.builtin.fail und Grund
tasks/adopt.ymlnach Rückfrage und Backup (adopt.backup), vor tasks/main.ymlvorhandene 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):

WorkflowWannWas
lint.ymljeder Push, jeder Pull Requesttools/lint.sh im Container
image.ymlPush auf main, montagsLint → 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:

  1. Rolle iac-core/ansible/roles/<modul>/ mit module_state: present installiert und startet, absent entfernt Pakete und Dateien wieder – Daten bleiben.
  2. Firewall-Regeln des Moduls in /etc/nftables.d/40-<modul>.nft.
  3. 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

SymptomUrsache
Workflow läuft nieDatei liegt in .forgejo/ statt in .forgejo/workflows/
Lint: module … does not existin modules steht ein Modul, das es (noch) nicht gibt
Lint: … belongs to the old layoutapp.func, packages.list oder rootfs/ aus einer alten App kopiert
Lint: ct.sh is not used any morealte Einstiegsskripte im Repo – löschen, die Website liefert sie
Image-Build bricht im chroot abDienst wird gestartet oder Datenbank angesprochen – when: not iac_build
Dienst startet bei jedem update neuNeustart als Task statt als Handler mit notify
neither the role iac_version nor iac_app_versionApp 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 SchleifeSchleife ü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.