GOVDIGITAL

GenAI Apps

App-Pakete für die GenAI-Plattform

So baust du heute ein App-Paket, das die Plattform selbst ausrollen und verdrahten kann: ein Git-Repository mit einem Terraform-Modul und einem Ordner genai-cli/. Darin steht, was die App braucht (Redis, LiteLLM, Datenbank, Admin-Zugang, Logo) – den Rest erledigt der Deployment-Workflow.

Heutiges FormatStand 13.10.2026 · Modul-Repo mit genai-cli/ · ohne Einreich-API

meine-app · v1.0.0Importierbar

So liest die Konsole die Vorlage beim Import.

  • Stufeai-applications
  • Abhängigkeitenredislitellm
  • Datenbank · Bucketmeine_appmeine_app
  • LiteLLM-Schlüsselmeine_app
  • Secretssession_secret
  • Partner-Texte, Logovollständig
Gegen den Import-Code der Konsole geprüft, Stand 13.10.2026.
Für KI-Assistenten: dieselbe Anleitung als app-entwicklung.md, Einstieg über llms.txt, harte Regeln auf einer Seite.Für KI

Grundlagen

Aufbau eines App-Pakets

Was im Repository liegt, wie der Schlüssel geschrieben wird und wie die Plattform dein Modul aufruft.

Auf dieser Seite

1Auf einen Blick#

Eine App ist heute ein Modul-Repository in der GitLab-Gruppe genai-plattform/applications. Die Plattform liest daraus den Ordner genai-cli/, erzeugt pro Umgebung die Terragrunt-Dateien und rollt aus.

1 · DuModul-Repo mit genai-cli/, SemVer-Tag
→
2 · Plattform-Teamimportiert das Repo in den App-Katalog
→
3 · BestellungApp wird für eine Umgebung gewählt, Abhängigkeiten kommen mit
→
4 · WorkflowSecrets erzeugen, Dateien rendern, Terragrunt ausführen

Was die Plattform für dich übernimmt:

  • Abhängigkeiten – brauchst du Redis, wird Redis mit ausgerollt und vor deiner App angewendet.
  • Zugangsdaten – Redis-Passwort, LiteLLM-Schlüssel, Datenbank, S3 und eigene Secrets werden erzeugt, verschlüsselt (SOPS) abgelegt und deiner App als Eingabe übergeben.
  • Domain, TLS, Ingress-Klasse, Pull-Secrets – kommen aus der Umgebung.
  • Admin-Zugang – die Umgebung hat einen Admin (E-Mail + Passwort), den deine App übernehmen kann.
  • Katalog-Kachel – Name, Beschreibung und Logo erscheinen im Katalog und im AI Hub.
  • Pflicht-Labels – genai.govdigital.de/* setzt die Plattform selbst.

2Aufbau des Repositorys#

meine-app/ ├── main.tf # Terraform-Modul: meist ein helm_release ├── variables.tf # Eingaben, die die Plattform liefert ├── outputs.tf ├── terraform.tf # required_providers, keine provider-Blöcke ├── chart/ # eigenes Helm-Chart (optional, oder Chart aus Repo) └── genai-cli/ # ← das liest die Plattform ├── terragrunt.hcl.genai # Pflicht: wie das Modul in der Umgebung aufgerufen wird ├── metadata.json # Name, Beschreibung, Katalogtexte ├── logo.svg # oder logo.png ├── config.domains.hcl.genai # öffentliche Adresse ├── config.hosts.hcl.genai # Adresse im Cluster ├── config.secrets.hcl.genai # eigene Secrets, Admin-Login ├── config.litellm.hcl.genai # LiteLLM-Schlüssel ├── config.databases.hcl.genai # verwaltetes Postgres ├── config.s3_buckets.hcl.genai # verwalteter S3-Bucket └── config.versions.hcl.genai # App- und Chart-Version
DateiPflichtWofür
terragrunt.hcl.genaijaAufruf des Moduls: Quelle, Abhängigkeiten, Eingaben. Ohne sie ist das Repo nicht importierbar.
metadata.jsonpraktisch jaName, Beschreibung, Maintainer, Katalogtexte, Versionsquellen.
logo.svg / logo.pngfür Partner jaKachel im Katalog und AI Hub. Ohne Logo sehen Partner die App nicht.
config.domains.hcl.genaifür Web-AppsÖffentliche Adresse. Ohne sie keine Kachel im AI Hub.
config.hosts.hcl.genaiempfohlenInterne Adresse, damit andere Apps dich erreichen.
config.secrets.hcl.genaibei BedarfSecrets, die die Plattform erzeugen soll (z. B. Admin-Passwort).
config.litellm.hcl.genaiwenn die App ein LLM nutztEigener LiteLLM-Schlüssel.
config.databases.hcl.genaiwenn die App Postgres brauchtVerbindungsdaten der verwalteten Datenbank.
config.s3_buckets.hcl.genaiwenn die App S3 brauchtVerbindungsdaten des Buckets.
config.versions.hcl.genaiempfohlenApp- und Chart-Version an einer Stelle.

Jede .genai-Datei darf auch ohne Endung heißen (terragrunt.hcl); die Plattform sucht zuerst die Variante mit .genai.

3Schnellstart mit der Vorlage#

Schnellster Weg: im Baukasten Schlüssel und Bausteine wählen und das fertige Repo als ZIP laden. Von Hand geht es so:

  1. Den Ordner vorlage/ in ein neues Repo kopieren.
  2. Überall meine-app und meine_app durch deinen Schlüssel ersetzen (Regeln).
  3. Nicht benötigte config.*-Dateien löschen und die passenden Zeilen in terragrunt.hcl.genai entfernen.
  4. main.tf auf deine App anpassen, Logo austauschen, metadata.json ausfüllen.
  5. Lokal prüfen (Lokal prüfen), SemVer-Tag setzen, an das Plattform-Team übergeben (Übergabe).
Vorlage übernehmen
cp -R docs/app-entwicklung/vorlage ../meine-wissensdatenbank
cd ../meine-wissensdatenbank
grep -rl 'meine[-_]app' . | xargs sed -i '' -e 's/meine-app/wissensdatenbank/g' -e 's/meine_app/wissensdatenbank/g'
./pruefen.sh

4Der Schlüssel der App#

Der Schlüssel ist der feste Name der App, zum Beispiel open-webui. Er lässt sich nach dem Import nicht mehr ändern.

  • Nur Kleinbuchstaben, Ziffern und Bindestrich, höchstens 40 Zeichen, nicht mit - beginnen oder enden.
  • Reserviert: apps, import, new.
  • Der Import nimmt den einzigen Schlüssel aus config.hosts/config.domains, sonst den Repo-Namen ohne terraform-/-terraform.

Die Plattform schreibt den Schlüssel in zwei Formen. Das ist die häufigste Fehlerquelle:

WoSchreibweiseBeispiel
Katalog, Ordner in der Umgebung, dependenciesmit Bindestrichopen-webui
Schlüssel in allen config.*-Dateien und in inputs.*.<key>mit Unterstrichopen_webui
Abschnitt in config.secrets.hcl.genai und secrets.<key>mit Unterstrichsecrets.open_webui.admin_password

5terragrunt.hcl.genai#

Die Datei wird pro Umgebung in zwei Schritten verarbeitet:

  1. Jinja ({{ … }}) – beim Erzeugen der Umgebung. Verfügbar sind source (die Modulquelle mit Tag) und jeder Wert aus den Katalog-Standardwerten (defaultValues). Immer mit default(…) schreiben.
  2. Terragrunt/HCL (${ … }, try(…)) – beim Ausrollen. Hier liest du alles, was die Umgebung bereitstellt, aus local.common_vars.inputs.
genai-cli/terragrunt.hcl.genai
include "root" {
  path = find_in_parent_folders("root.hcl")
}

# Provider kommen von der Plattform. kubernetes.hcl ignoriert die Pflicht-Labels.
include "kubernetes" {
  path = "${get_terragrunt_dir()}/../providers/kubernetes.hcl"
}

include "helm" {
  path = "${get_terragrunt_dir()}/../providers/helm.hcl"
}

terraform {
  source = "{{ source | default('../../../../../terraform-meine-app') }}"
}

# Was vor dieser App angewendet sein muss. Daraus wird im Katalog "dependsOn".
dependencies {
  paths = ["../redis", "../../applications/litellm"]
}

locals {
  common_vars = read_terragrunt_config(find_in_parent_folders("terragrunt.hcl"))
}

inputs = merge(
  local.common_vars.inputs,
  {
    release_name = "meine-app"
    domain       = try(local.common_vars.inputs.domains.meine_app, "")
    replicas     = {{ replicas | default(1) }}

    # Redis – vom Redis-Modul der Umgebung
    redis_host     = try(local.common_vars.inputs.databases.redis.host, "")
    redis_port     = try(local.common_vars.inputs.databases.redis.port, "6379")
    redis_password = try(local.common_vars.inputs.databases.redis.password, "")

    # LiteLLM – OpenAI-kompatibel, eigener Schlüssel aus config.litellm.hcl.genai
    openai_base_url = "http://${try(local.common_vars.inputs.hosts.litellm, "localhost")}:4000/v1"
    openai_api_key  = try(local.common_vars.inputs.litellm.meine_app.key, "")

    # Admin – der Admin der Umgebung
    admin_email    = try(local.common_vars.inputs.default_user_email, "")
    admin_password = try(local.common_vars.inputs.default_user_password, "")
  }
)

Immer try(…, default)

Zugangsdaten für Datenbank und S3 stehen erst nach dem ersten Secret-Abgleich in der Umgebung. Ein nackter Zugriff wie inputs.databases.meine_app.password bricht dann schon den Plan ab. try() hält ihn lauffähig.

Was die Umgebung bereitstellt#

Alles unter local.common_vars.inputs. Weil inputs = merge(local.common_vars.inputs, {...}) alles weiterreicht, kann dein Modul diese Namen auch direkt als Terraform-Variable deklarieren. Was es nicht deklariert, ignoriert Terraform.

EingabeInhalt
namespace, release_name_prefixKubernetes-Namespace der Umgebung und Präfix für Release-Namen
domain, environment, organisationBasis-Domain, Umgebungsname, Organisation
ingress_class_name, cluster_issuer_name, certificatesIngress-Klasse, cert-manager-Issuer, Zertifikate
storage_classStorage-Klasse für Volumes
image_pull_secrets, registry_credentialsNamen der Pull-Secrets, Registry-Zugänge
domains.<key>, hosts.<key>öffentliche und interne Adressen aller Apps der Umgebung
databases.<key>, s3_buckets.<key>Verbindungsdaten (auch databases.redis)
litellm.<key>.keyLiteLLM-Schlüssel je App
secrets.<key>.<name>eigene Secrets aus config.secrets.hcl.genai
versions.<key>, helm_chart_versions.<key>Versionen aus config.versions.hcl.genai
default_user_email, default_user_first_name, default_user_passwordAdmin der Umgebung
smtp_host, smtp_port, smtp_from, smtp_from_name, smtp_username, smtp_passwordMailversand, leer wenn die Umgebung keinen hat
http_proxy, https_proxy, no_proxyProxy, meist leer
modelsModell-Liste der Umgebung

Regeln für das Terraform-Modul#

  • Keine provider-Blöcke und kein backend. Beides erzeugt die Plattform. Eine Datei providers.tf wird beim Ausrollen leer überschrieben.
  • Helm-Provider ~> 2.0. Die erzeugte Provider-Konfiguration nutzt die Syntax von Version 2.
  • k8s_config_path und k8s_context nicht deklarieren – die Plattform deklariert sie selbst.
  • Nichts außerhalb des Namespace var.namespace anlegen; Cluster-Ressourcen gehören nicht in ein App-Modul.
  • Keine eigenen genai.govdigital.de/*-Labels und nie Selektoren oder Pod-Template-Labels dafür ändern.
  • Release- und Workload-Namen enthalten den Schlüssel (${release_name_prefix}meine-app). Daran ordnet das Monitoring Pods der App zu.

Dienste anbinden

Redis, LiteLLM, Datenbank und Secrets

Du sagst, was deine App braucht. Die Plattform rollt es aus, erzeugt die Zugangsdaten und reicht sie an dein Modul weiter.

Auf dieser Seite

1Abhängigkeiten (dependsOn)#

Du erklärst eine Abhängigkeit an einer Stelle: im dependencies-Block von terragrunt.hcl.genai. Daraus leitet der Import das Katalogfeld dependsOn ab.

Abhängigkeit auf Redis und LiteLLM
dependencies {
  paths = ["../redis", "../../applications/litellm"]
}

Was dann passiert:

  1. Beim Bestellen wählt die Plattform jede Abhängigkeit automatisch mit, auch über mehrere Stufen (deine App → LiteLLM → Redis, Presidio).
  2. Beim Erzeugen bekommt die Abhängigkeit ihre Secrets (z. B. das Redis-Passwort) und schreibt ihre Verbindungsdaten in die Umgebung.
  3. Beim Ausrollen wendet Terragrunt die Abhängigkeit vor deiner App an.
  4. Deine App liest die Verbindungsdaten über local.common_vars.inputs – nicht über Terragrunt-Outputs.

Regeln:

  • Der letzte Pfadteil ist der Katalogschlüssel der Abhängigkeit (redis, litellm). Ist er nicht im Katalog, blockiert der Import. Eine eigene Abhängigkeit (z. B. eine zweite App von dir) muss deshalb zuerst importiert werden.
  • ../name heißt „gleiche Stufe“, ../../applications/name „andere Stufe“. Liegt die Abhängigkeit woanders, korrigiert die Plattform den Pfad selbst.
  • Verweist deine Datei auf ../../…, landet die App in der Stufe ai-applications (nach applications). Das ist richtig für Apps, die LiteLLM nutzen.
  • Postgres ist keine Abhängigkeit, sondern eine verwaltete Datenbank (Datenbank und S3).
AbhängigkeitPfadWas deine App bekommt
Redis../redisinputs.databases.redis.{host,port,password}
LiteLLM../../applications/litellminputs.hosts.litellm, inputs.domains.litellm, eigener Schlüssel über config.litellm.hcl.genai
Presidio../presidioinputs.hosts.presidio_analyzer, inputs.hosts.presidio_anonymizer
eine andere App../<key>was diese App in config.hosts/config.domains veröffentlicht

Nachinstallieren in eine laufende Umgebung

Bestellung und Assistent ziehen Abhängigkeiten automatisch mit. Fügt ein Operator eine App einzeln einer bestehenden Umgebung hinzu, passiert das heute nicht – die Abhängigkeit muss dort schon laufen.

2Redis anbinden#

  1. Abhängigkeit eintragen: "../redis" im dependencies-Block.
  2. Verbindungsdaten lesen. Das Redis-Modul der Umgebung veröffentlicht sie unter databases.redis:
terragrunt.hcl.genai – Redis
redis_host     = try(local.common_vars.inputs.databases.redis.host, "")
redis_port     = try(local.common_vars.inputs.databases.redis.port, "6379")
redis_password = try(local.common_vars.inputs.databases.redis.password, "")
  1. Im Modul daraus die URL bauen, die deine App erwartet:
main.tf – Redis-URL für die App
locals {
  redis_url = "redis://:${var.redis_password}@${var.redis_host}:${var.redis_port}/0"
}
  • host ist der Service im Namespace (<präfix>redis-master.<namespace>); wer einen voll qualifizierten Namen braucht, hängt .svc.cluster.local an.
  • Das Passwort erzeugt die Plattform, sobald Redis in der Umgebung ist. Du musst nichts dafür deklarieren.
  • Teile dir Redis mit anderen Apps: nimm eine eigene Datenbanknummer oder ein Schlüsselpräfix.

Ältere Schreibweise

Ältere Module lesen inputs.hosts.redis und inputs.redis_password. Beides funktioniert noch; für neue Apps gilt inputs.databases.redis.*.

3LiteLLM anbinden#

Deine App spricht mit LiteLLM wie mit der OpenAI-API. Sie bekommt einen eigenen Schlüssel mit eigenem Team, damit Nutzung und Kosten getrennt sichtbar sind.

  1. Abhängigkeit eintragen: "../../applications/litellm".
  2. Schlüssel anmelden mit config.litellm.hcl.genai:
genai-cli/config.litellm.hcl.genai
include "global" {
  path   = find_in_parent_folders("global.hcl")
  expose = true
}

locals {
  litellm = {
    meine_app = {
      team_alias     = "meine-app-team"
      key_alias      = "meine-app-api-key"
      create_api_key = true
      models         = []   # leer = alle Modelle der Umgebung
      metadata       = { service = "meine-app" }
      key            = try(include.global.locals.secrets.litellm.keys.meine_app, "")
    }
  }
}
  1. Schlüssel und Adresse übergeben:
terragrunt.hcl.genai – LiteLLM
openai_base_url = "http://${try(local.common_vars.inputs.hosts.litellm, "localhost")}:4000/v1"
openai_api_key  = try(local.common_vars.inputs.litellm.meine_app.key, "")
  • Die Zeile key = try(include.global.locals.secrets.litellm.keys.<key>, "") ist Pflicht und muss genau so heißen: an ihr erkennt die Plattform, dass sie einen sk-…-Schlüssel erzeugen und in LiteLLM anlegen soll.
  • Nimm die interne Adresse (hosts.litellm, Port 4000). Die öffentliche (https://${inputs.domains.litellm}) geht auch, läuft aber über den Ingress.
  • Welche Modelle es gibt, steht in inputs.models. Modellnamen besser als Katalog-Standardwert ({{ model_chat | default('…') }}) statt fest im Code.

4Datenbank und S3#

Postgres und S3 sind verwaltet: die Plattform legt Datenbank bzw. Bucket in der Infrastruktur-Stufe an und schreibt die Zugangsdaten in die Secrets der Umgebung. Deine App bekommt sie über zwei Dateien.

genai-cli/config.databases.hcl.genai
locals {
  databases = {
    meine_app = {
      host     = try(include.global.locals.secrets.database.meine_app.host, "localhost")
      port     = try(include.global.locals.secrets.database.meine_app.port, "5432")
      name     = try(include.global.locals.secrets.database.meine_app.name, "${include.global.locals.environment_name}-meine-app")
      username = try(include.global.locals.secrets.database.meine_app.username, "${include.global.locals.environment_name}_meine_app")
      password = try(include.global.locals.secrets.database.meine_app.password, "")
    }
  }
}
genai-cli/config.s3_buckets.hcl.genai
locals {
  s3_buckets = {
    meine_app = {
      bucket           = try(include.global.locals.secrets.s3_buckets.meine_app.bucket, "meine-app")
      endpoint         = try(include.global.locals.secrets.s3_buckets.meine_app.endpoint, "")
      region           = try(include.global.locals.secrets.s3_buckets.meine_app.region, "eu01")
      force_path_style = false
      username         = try(include.global.locals.secrets.s3_buckets.meine_app.access_key, "")
      secret_key       = try(include.global.locals.secrets.s3_buckets.meine_app.secret_key, "")
    }
  }
}

In terragrunt.hcl.genai liest du dann inputs.databases.meine_app.{host,port,name,username,password} und inputs.s3_buckets.meine_app.{bucket,endpoint,region,username,secret_key}.

  • Der Import erkennt die Datenbank nur, wenn config.databases auf secrets.database.<key> verweist. Dann trägt er requires.databases im Katalog ein.
  • Im S3-Abschnitt heißt der Access-Key username – so erwartet es die Bucket-Bereitstellung. Lies ihn als inputs.s3_buckets.<key>.username.
  • Eine App, die Postgres selbst mitbringt (eigener StatefulSet), braucht beides nicht – dann ist das Passwort ein eigenes Secret (Eigene Secrets).

5Admin-Zugang und eigene Secrets#

Der Admin der Umgebung#

Jede Umgebung hat einen Admin: E-Mail, Vorname, Passwort. Der Partner gibt die E-Mail bei der Bestellung an, das Passwort erzeugt die Plattform. Übernimm diesen Admin, statt einen eigenen zu erfinden – dann meldet sich der Partner überall gleich an.

terragrunt.hcl.genai – Admin übernehmen
admin_email      = try(local.common_vars.inputs.default_user_email, "")
admin_first_name = try(local.common_vars.inputs.default_user_first_name, "Administrator")
admin_password   = try(local.common_vars.inputs.default_user_password, "")

Dein Modul legt damit beim ersten Start den Admin an (Bootstrap-Variablen, Init-Job oder Helm-Hook – je nach App).

Eigene Secrets erzeugen lassen#

Braucht deine App weitere Secrets (Signaturschlüssel, Admin-Benutzername, interne Passwörter), meldest du sie in config.secrets.hcl.genai an. Die Plattform erzeugt fehlende Werte beim Anlegen der Umgebung und überschreibt nie einen vorhandenen.

genai-cli/config.secrets.hcl.genai
locals {
  required_secrets = {
    meine_app = {
      admin_username = {
        type        = "static"
        default     = "admin"
        description = "Login-Name des Admins"
      }
      admin_password = {
        type        = "password"
        length      = 32
        description = "Passwort des Admins"
      }
      session_secret = {
        type        = "hex"
        length      = 32
        description = "Signiert Sitzungen"
      }
    }
  }
}

Lesen in terragrunt.hcl.genai:

terragrunt.hcl.genai – eigene Secrets
admin_username = try(local.common_vars.inputs.secrets.meine_app.admin_username, "admin")
admin_password = try(local.common_vars.inputs.secrets.meine_app.admin_password, "")
session_secret = try(local.common_vars.inputs.secrets.meine_app.session_secret, "")
typeerzeugtFelder
passwordBuchstaben und Ziffernlength (Standard 32)
hexHex-Zeichenkette, 2 Zeichen je Bytelength in Bytes (Standard 32)
fernetFernet-Schlüssel (44 Zeichen)–
prefixed<prefix>-<zufall>prefix, length (Standard 24)
staticgenau defaultdefault – ohne default bleibt der Wert leer

admin_password erbt das Admin-Passwort

Heißt ein Secret admin_password, bekommt es das Passwort des Admins der Umgebung statt eines Zufallswerts. Das ist gewollt (ein Login für alle Apps) und vorläufig: es entfällt, sobald Konten zentral über den AI Hub vergeben werden. Willst du ein unabhängiges Passwort, nenne es anders.

  • Zugangsdaten nie in metadata.json, Katalog-Standardwerte oder Klartext-Dateien schreiben. Secrets gehören in config.secrets.hcl.genai.
  • Ein Secret, das der Partner selbst setzen muss (z. B. ein externer API-Schlüssel), meldest du als static ohne default an und beschreibst es in description. Das Plattform-Team trägt es dann ein.

6Adressen: Domain und Host#

genai-cli/config.domains.hcl.genai
locals {
  domains = {
    meine_app = "meine-app-${include.global.locals.domain}"
  }
}
genai-cli/config.hosts.hcl.genai
locals {
  hosts = {
    meine_app = "${include.global.locals.release_name_prefix}meine-app.${include.global.locals.k8s_namespace}"
  }
}
  • domains.<key> ist die öffentliche Adresse. Dein Ingress nutzt sie zusammen mit ingress_class_name und dem Issuer cluster_issuer_name.
  • hosts.<key> muss zum Namen deines Kubernetes-Service passen. Andere Apps erreichen dich darüber.
  • include.global.locals.* darfst du in allen config.*-Dateien nutzen; fehlt der include "global"-Block, ergänzt ihn die Plattform.

Katalog

Im Katalog und im AI Hub erscheinen

Name, Texte, Logo und Versionen: was Partner sehen und was die Konsole beim Import ausliest.

Auf dieser Seite

1metadata.json und Katalogtexte#

genai-cli/metadata.json
{
  "name": "Meine App",
  "description": "Ein Satz, was die App tut.",
  "maintainers": ["team-meine-app"],
  "catalog": {
    "label": "Meine App",
    "summary": "Ein Satz für die Katalog-Kachel, ohne Zeilenumbruch.",
    "details": "Ein bis drei Absätze für die Detailseite.\n\nNur Klartext, kein Markdown.",
    "vendor": "Hersteller (Open Source)",
    "category": "ui",
    "role": "application",
    "defaultValues": {
      "replicas": 1,
      "model_chat": "stackit-openai-gpt-oss-120b"
    }
  },
  "versions": {
    "meine_app": {
      "description": "Image der App",
      "source": { "type": "registry", "host": "registry.onstackit.cloud", "repo": "govdigital/meine-app", "tagPrefix": "v", "limit": 50 }
    }
  }
}
FeldBedeutung
name, description, maintainersGrunddaten; description ohne catalog.summary wird zur Kurzbeschreibung
catalog.labelAnzeigename
catalog.summaryein Satz, eine Zeile – Pflicht für Partner
catalog.detailsKlartext-Absätze; keine Zeichen < > * # und kein Backtick – Pflicht für Partner
catalog.vendorHersteller – Pflicht für Partner
catalog.categorycore, observability, ui, tooling oder data – Pflicht, ohne Kategorie lehnt der Katalog den Eintrag ab
catalog.roleapplication (Nutzer sehen sie) oder service (Baustein für andere Apps, z. B. Redis)
catalog.tierapplications oder ai-applications; sonst aus den Abhängigkeitspfaden abgeleitet
catalog.defaultValuesJinja-Werte für terragrunt.hcl.genai; Platzhalter ${env}, ${org}, ${envName} werden ersetzt
versions.<name>.sourcewoher die Konsole verfügbare Versionen liest: registry, github-release oder gitlab-release mit host, repo, tagPrefix, stripPrefix, limit
helmChartVersions.<name>dasselbe für die Chart-Version

Lege das Logo in genai-cli/ ab. Die Plattform sucht in dieser Reihenfolge: logo.svg, logo.png, logo.jpg, logo.jpeg.

RegelWert
FormateSVG (bevorzugt), PNG, JPEG
Größehöchstens 1 MiB im Repo
Rasterbilderquadratisch, am besten 256 × 256 px; größere werden auf 256 px verkleinert
SVGhöchstens 128 KiB, kein <script>, keine on…-Attribute, kein foreignObject, keine externen Verweise, keine DTD
Darstellungauf hellem Grund, ohne Rand, Motiv füllt die Fläche
nicht angenommenlogo.webp, icon.ico

Ohne Logo bleibt die App für Partner unsichtbar.

3Im AI Hub erscheinen#

Eine App bekommt automatisch eine Kachel im AI Hub der Umgebung, wenn

  • catalog.role application ist und
  • config.domains.hcl.genai eine Adresse unter genau dem Schlüssel liefert, mit Unterstrich: Katalog meine-app → domains.meine_app.

Nach jedem Ausrollen erneuert die Plattform den Hub selbst. Logo und Texte kommen aus dem Katalog.

4Versionen und Tags#

  • Modul-Version: setze SemVer-Tags (v1.0.0, v1.1.0). Beim Import legt die Konsole den neuesten Tag als Version fest; das Plattform-Team kann stattdessen „immer den neuesten Tag“ wählen. Ohne Tag läuft der unversionierte Stand – nur zum Testen.
  • Neue Tags erreichen neue Bestellungen, sobald die Version im Katalog umgestellt ist (oder „neuester Tag“ gilt). Laufende Umgebungen aktualisieren sich nicht selbst.
  • App-Version: steht in config.versions.hcl.genai und lässt sich je Umgebung umstellen.
genai-cli/config.versions.hcl.genai
locals {
  versions = {
    meine_app = "{{ meine_app_version | default('v1.0.0') }}"
  }
  helm_chart_versions = {
    meine_app = "1.0.0"
  }
}

Jede Änderung, die eine Umgebung betrifft, ist ein neuer Tag. Einen veröffentlichten Tag nie verschieben.

5Images und Registry#

  • Images gehören in die STACKIT-Registry unter registry.onstackit.cloud/govdigital/…. Eine Cluster-Richtlinie meldet Images aus anderen Registries; heute nur als Prüfbefund, später als Sperre.
  • Pull-Secrets nicht selbst anlegen: image_pull_secrets = try(local.common_vars.inputs.image_pull_secrets, []) übergeben und im Chart nutzen.
  • Immer einen festen Tag verwenden, nie latest.
  • Container ohne Root-Rechte, mit Ressourcen-Limits und Health-Checks (readinessProbe, livenessProbe).

Prüfen & übergeben

Prüfen und übergeben

Lokal testen, Checkliste abhaken, an das Plattform-Team geben – und was tun, wenn etwas hakt.

Auf dieser Seite

1Lokal prüfen#

Eine Testumgebung zum Selbstbedienen gibt es noch nicht. Lokal prüfst du drei Dinge:

1. Paket-Check – die Vorlage bringt pruefen.sh mit. Es prüft Pflichtdateien, Schlüssel-Schreibweisen, metadata.json, Logo und Abhängigkeitspfade.

Paket prüfen
./pruefen.sh

2. Modul – das Terraform-Modul gegen einen lokalen Cluster (kind, k3d, Docker Desktop) mit Werten, die sonst die Plattform liefert:

Modul lokal ausrollen
kind create cluster --name app-test
kubectl run redis --image=redis:7-alpine -- redis-server --requirepass lokal-test
kubectl expose pod redis --port 6379
export KUBE_CONFIG_PATH=~/.kube/config
terraform init
terraform apply -var-file=beispiel/lokal.tfvars
kubectl port-forward svc/meine-app 8080:80

beispiel/lokal.tfvars in der Vorlage setzt Namespace, Image, Redis, LiteLLM-Ersatz und Admin mit Testwerten. Als LiteLLM-Ersatz taugt jeder OpenAI-kompatible Dienst (z. B. Ollama unter http://host.docker.internal:11434/v1). Ein provider-Block ist dafür nicht nötig: der Helm-Provider liest KUBE_CONFIG_PATH.

3. Chart – helm lint chart/ und helm template chart/ mit deinen Werten.

Was lokal nicht prüfbar ist

Die Jinja-Werte, die Secret-Erzeugung und das Zusammenspiel der Umgebung prüft heute nur das Plattform-Team beim Import und in einer Dev-Umgebung.

2Übergabe an das Plattform-Team#

  1. Repo in der GitLab-Gruppe genai-plattform/applications anlegen (lassen). Nur von dort importiert die Konsole, und nur Repos mit genai-cli/terragrunt.hcl.genai auf dem Standard-Branch.
  2. SemVer-Tag setzen.
  3. Dem Plattform-Team Bescheid geben: Repo, Tag, Ansprechpartner.
  4. Das Plattform-Team importiert unter App-Katalog → Importieren. Der Import zeigt einen Entwurf (Schlüssel, Stufe, Abhängigkeiten, Datenbanken, Buckets, Secrets, Logo, Texte) und blockiert, wenn eine Abhängigkeit nicht im Katalog ist oder die Vorlage am Tag fehlt.
  5. Danach: Test in einer Dev-Umgebung, Version festlegen, für Partner freischalten. Neue Einträge sind angeboten, aber für Partner erst sichtbar, wenn das Plattform-Team es einschaltet.
Was der Import liestWoraus
Schlüsseleinziger Schlüssel in config.hosts/config.domains, sonst Repo-Name
Stufecatalog.tier, sonst aus den Abhängigkeitspfaden
Abhängigkeitendependencies-Block, nur Einträge, die im Katalog sind
Datenbanken, Bucketsconfig.databases / config.s3_buckets mit Verweis auf secrets.database.<key> / secrets.s3_buckets.<key>
Texte, Kategorie, Rolle, Standardwertemetadata.json → catalog
Logologo.svg / logo.png
Versionneuester SemVer-Tag

3Checkliste#

Hake ab, was erledigt ist. Der Stand bleibt in diesem Browser gespeichert.

4Bekannte Stolperstellen#

SymptomUrsacheLösung
Plan bricht mit „Unsupported attribute“ abZugriff ohne try() auf Daten, die erst später kommentry(…, "")
App findet Redis nichthosts.redis statt databases.redis oder Service-Name falschinputs.databases.redis.host nutzen
LiteLLM-Schlüssel leerkey-Zeile fehlt oder anders geschriebengenau try(include.global.locals.secrets.litellm.keys.<key>, "")
S3-Zugriff verweigertAccess-Key als access_key geschrieben, als username gelesenim S3-Abschnitt username verwenden
Admin-Passwort unbekanntSecret admin_password erbt das Admin-Passwortgewollt; sonst anders benennen
Statisches Secret leertype = "static" ohne defaultdefault setzen oder Plattform-Team trägt den Wert ein
Import blockiert: „unknown dependency“Abhängigkeit ist nicht im Katalogerst die Abhängigkeit in den Katalog bringen
App für Partner unsichtbarsummary, details, vendor oder Logo fehlen, oder details enthält <>*#Texte und Logo ergänzen
Keine Kachel im AI Hubrole ist service oder keine Domainrole: application, config.domains
Doppelter Provider beim InitModul bringt eigenen provider "helm" mitProvider-Block entfernen
Logo fehlt trotz Dateilogo.webp oder icon.icoals SVG oder PNG ablegen

Für KI

Für KI-Assistenten

Dieselbe Anleitung als harte Regeln, dazu Markdown, llms.txt und ein fertiger Auftrag zum Kopieren.

Auf dieser Seite

★Auftrag zum Kopieren#

Gib diesen Text deinem Assistenten, zusammen mit app-entwicklung.md und dem Ordner vorlage/. Der Baukasten füllt ihn auf Wunsch mit deinen Bausteinen aus.

Auftrag an den KI-Assistenten
Du baust ein App-Paket für die GenAI-Plattform von govdigital im heutigen
Modulformat (Terraform-Modul + Ordner genai-cli/).

Lies zuerst app-entwicklung.md, vor allem „Regeln für KI-Assistenten“,
und nimm vorlage/ als Ausgangspunkt. Erfinde keine Felder, die dort nicht stehen.

App:          <Name>, Schlüssel <schluessel> (a-z, 0-9, -)
Image:        registry.onstackit.cloud/govdigital/<schluessel>:<tag>
Braucht:      Redis | LiteLLM | Postgres | S3 | Admin der Umgebung | Mail
Eigene Secrets: <name> (<password|hex|fernet|prefixed|static>), …
Web-Oberfläche: ja/nein, Container-Port <port>

Liefere:
- Terraform-Modul: main.tf, variables.tf, terraform.tf, outputs.tf, Helm-Chart
- genai-cli/ mit terragrunt.hcl.genai und nur den config.*-Dateien, die die App braucht
- genai-cli/metadata.json mit catalog.summary, details, vendor, category, role
- genai-cli/logo.svg

Prüfe am Ende mit ./pruefen.sh. Es muss „0 Fehler“ melden.

1Regeln für KI-Assistenten#

Kompakt für Agenten, die ein App-Paket erzeugen oder prüfen. Jede Regel ist oben begründet.

  1. Ein App-Paket ist ein Git-Repo: Terraform-Modul im Wurzelordner, Plattform-Dateien in genai-cli/.
  2. Pflicht ist genai-cli/terragrunt.hcl.genai und in metadata.json catalog.category (core|observability|ui|tooling|data). Für Partner zusätzlich catalog.summary (eine Zeile), catalog.details (Klartext ohne <>*# und Backtick), catalog.vendor und ein Logo logo.svg/logo.png.
  3. Schlüssel: ^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$. In config.*, inputs.* und secrets.* mit _ statt -.
  4. .genai-Dateien sind Jinja-Vorlagen. Jinja-Variablen: source und die Schlüssel aus catalog.defaultValues. Immer {{ x | default(…) }}.
  5. Zur Laufzeit liest das Modul alles aus local.common_vars.inputs, wobei common_vars = read_terragrunt_config(find_in_parent_folders("terragrunt.hcl")). Jeder Zugriff in try(…, default).
  6. Abhängigkeiten nur im Block dependencies { paths = [...] }. Letzter Pfadteil = Katalogschlüssel. Verbindungsdaten nie über dependency-Outputs.
  7. Redis: Pfad ../redis; lesen inputs.databases.redis.host|port|password.
  8. LiteLLM: Pfad ../../applications/litellm; config.litellm.hcl.genai mit locals.litellm.<key>.{team_alias,key_alias,create_api_key=true,models=[],metadata,key=try(include.global.locals.secrets.litellm.keys.<key>,"")}; lesen inputs.litellm.<key>.key; Basis-URL http://${inputs.hosts.litellm}:4000/v1.
  9. Postgres/S3 nie als Abhängigkeit: config.databases.hcl.genai mit secrets.database.<key>.{host,port,name,username,password}, config.s3_buckets.hcl.genai mit secrets.s3_buckets.<key>.{bucket,endpoint,region,access_key→username,secret_key}.
  10. Admin: inputs.default_user_email|default_user_first_name|default_user_password. Eigene Secrets in config.secrets.hcl.genai unter locals.required_secrets.<key>.<name> = { type = password|hex|fernet|prefixed|static, length, default, prefix, description }; lesen inputs.secrets.<key>.<name>. admin_password erbt das Admin-Passwort.
  11. Modul: keine provider-/backend-Blöcke, keine Variablen k8s_config_path/k8s_context, Helm-Provider ~> 2.0, nur Ressourcen im Namespace var.namespace, keine genai.govdigital.de/*-Labels, Release-Name ${release_name_prefix}<key>.
  12. Images aus registry.onstackit.cloud/govdigital/… mit festem Tag; image_pull_secrets aus inputs.
  13. Nie Zugangsdaten in metadata.json, defaultValues oder Klartext-Dateien.
  14. Vor der Übergabe pruefen.sh aus der Vorlage ausführen; Ergebnis muss fehlerfrei sein.

2Begriffe#

BegriffBedeutung
Umgebungeine Installation der Plattform für einen Kunden, mit eigenem Namespace und eigenen Secrets
Stufe (Tier)Ordner einer Umgebung, die nacheinander ausgerollt werden: infrastructure, applications, ai-applications
KatalogListe aller bestellbaren Apps in der Konsole, gepflegt vom Plattform-Team
Schlüsselfester Name einer App im Katalog, z. B. open-webui
Sidecareine der config.*-Dateien; die Plattform legt sie in der Umgebung ab und führt sie zu inputs zusammen
secrets.yamlmit SOPS verschlüsselte Secrets der Umgebung
Secret-AbgleichSchritt, der Zugangsdaten verwalteter Datenbanken und Buckets in secrets.yaml schreibt
AI HubStartseite einer Umgebung mit Kacheln aller Apps
PartnerOrganisation, die Umgebungen bestellt (z. B. AKDB, Dataport)

Baukasten

App-Paket zusammenstellen

Schlüssel, Texte und Bausteine wählen – daneben entstehen die Dateien für genai-cli/, genau wie in der Vorlage. Kopieren, einzeln laden oder das ganze Repo als ZIP.

Deine App

Name

Bausteine

Eigene Secrets

Image und Versionen

genai-cli/

ZIP = Vorlage mit deinem Schlüssel + diese Dateien

Vorlage

Ein lauffähiges App-Repo

Terraform-Modul, Helm-Chart, alle genai-cli/-Dateien und das Prüfskript. Geprüft gegen den Import der Konsole, terraform validate und helm lint. Datei anklicken, kopieren, laden.