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
So liest die Konsole die Vorlage beim Import.
- Stufe
ai-applications - Abhängigkeiten
redislitellm - Datenbank · Bucket
meine_appmeine_app - LiteLLM-Schlüssel
meine_app - Secrets
session_secret - Partner-Texte, Logovollständig
Der Weg einer App
In sechs Schritten vom Repo in den Katalog
- 1
Gerüst anlegen
Im Baukasten Schlüssel und Bausteine wählen und das Repo als ZIP laden – oder die Vorlage kopieren.
App-Team - 2
Dienste anbinden
Redis, LiteLLM, Datenbank, S3 und Admin über genai-cli/ anmelden. Zugangsdaten kommen als inputs.
App-Team - 3
Modul schreiben
Ein Terraform-Modul, meist ein helm_release. Keine Provider, kein Backend, alles im Namespace.
App-Team - 4
Lokal prüfen
pruefen.sh prüft das Paket, kind oder k3d das Modul. Die Checkliste sagt, was noch fehlt.
App-Team - 5
Übergeben
Repo in genai-plattform/applications, SemVer-Tag setzen. Das Plattform-Team importiert in den Katalog.
App-Team + Plattform-Team - 6
Im Katalog
Die App ist bestellbar. Abhängigkeiten kommen mit, Secrets werden erzeugt, der AI Hub zeigt die Kachel.
Plattform
Was die Plattform liefert
Die App sagt, was sie braucht. Den Rest stellt die Umgebung.
Abhängigkeiten stehen im dependencies-Block, alles andere in den config.*-Dateien unter genai-cli/. Beim Bestellen kommen Abhängigkeiten automatisch mit, Zugangsdaten erzeugt die Plattform und reicht sie über local.common_vars.inputs an dein Modul.
Redis
Wird mit ausgerollt, Passwort erzeugt die Plattform.
inputs.databases.redis.*Sprachmodelle
Eigener LiteLLM-Schlüssel und Team, OpenAI-kompatibel.
inputs.litellm.<key>.keyDatenbank
Verwaltetes Postgres mit eigenem Zugang.
inputs.databases.<key>.*Ablage
Verwalteter S3-Bucket. Access-Key heißt username.
inputs.s3_buckets.<key>.*Admin
Der Admin der Umgebung – ein Login für alle Apps.
inputs.default_user_*Geheimnisse
Passwörter und Schlüssel, die nie im Repo stehen.
inputs.secrets.<key>.<name>Adresse
Domain mit TLS, Ingress-Klasse und Issuer der Umgebung.
inputs.domains.<key>SMTP der Umgebung, leer wenn sie keinen hat.
inputs.smtp_*Dokumentation
Nachschlagen
Grundlagen
Repo-Aufbau, Schlüssel, terragrunt.hcl.genai und was die Umgebung liefert.
Dienste anbinden
Abhängigkeiten, Redis, LiteLLM, Datenbank, S3, Admin und Secrets.
Katalog
metadata.json, Logo, AI-Hub-Kachel, Versionen und Images.
Prüfen & übergeben
Lokal testen, Checkliste, Übergabe und Stolperstellen.
Baukasten
genai-cli/ aus Bausteinen erzeugen, als ZIP oder Skript.
Vorlage
Ein lauffähiges App-Repo zum Durchklicken und Herunterladen.
Für KI
Harte Regeln, Markdown, llms.txt und ein Auftrag zum Kopieren.
Suchen
Jeder Abschnitt und jede Vorlagen-Datei. Taste / oder ⌘K.
Eine Einreich-API gibt es noch nicht: neue Apps kommen über das Plattform-Team in den Katalog (Übergabe). Ein einfacheres Paketformat ist in Arbeit – diese Seite wird dann fortgeschrieben.
app-entwicklung.md, Einstieg über llms.txt, harte Regeln auf einer Seite.Für KIGrundlagen
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.
genai-cli/, SemVer-TagWas 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#
| Datei | Pflicht | Wofür |
|---|---|---|
terragrunt.hcl.genai | ja | Aufruf des Moduls: Quelle, Abhängigkeiten, Eingaben. Ohne sie ist das Repo nicht importierbar. |
metadata.json | praktisch ja | Name, Beschreibung, Maintainer, Katalogtexte, Versionsquellen. |
logo.svg / logo.png | für Partner ja | Kachel im Katalog und AI Hub. Ohne Logo sehen Partner die App nicht. |
config.domains.hcl.genai | für Web-Apps | Öffentliche Adresse. Ohne sie keine Kachel im AI Hub. |
config.hosts.hcl.genai | empfohlen | Interne Adresse, damit andere Apps dich erreichen. |
config.secrets.hcl.genai | bei Bedarf | Secrets, die die Plattform erzeugen soll (z. B. Admin-Passwort). |
config.litellm.hcl.genai | wenn die App ein LLM nutzt | Eigener LiteLLM-Schlüssel. |
config.databases.hcl.genai | wenn die App Postgres braucht | Verbindungsdaten der verwalteten Datenbank. |
config.s3_buckets.hcl.genai | wenn die App S3 braucht | Verbindungsdaten des Buckets. |
config.versions.hcl.genai | empfohlen | App- 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:
- Den Ordner vorlage/ in ein neues Repo kopieren.
- Überall
meine-appundmeine_appdurch deinen Schlüssel ersetzen (Regeln). - Nicht benötigte
config.*-Dateien löschen und die passenden Zeilen interragrunt.hcl.genaientfernen. main.tfauf deine App anpassen, Logo austauschen,metadata.jsonausfüllen.- Lokal prüfen (Lokal prüfen), SemVer-Tag setzen, an das Plattform-Team übergeben (Übergabe).
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.sh4Der 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 ohneterraform-/-terraform.
Die Plattform schreibt den Schlüssel in zwei Formen. Das ist die häufigste Fehlerquelle:
| Wo | Schreibweise | Beispiel |
|---|---|---|
Katalog, Ordner in der Umgebung, dependencies | mit Bindestrich | open-webui |
Schlüssel in allen config.*-Dateien und in inputs.*.<key> | mit Unterstrich | open_webui |
Abschnitt in config.secrets.hcl.genai und secrets.<key> | mit Unterstrich | secrets.open_webui.admin_password |
5terragrunt.hcl.genai#
Die Datei wird pro Umgebung in zwei Schritten verarbeitet:
- Jinja (
{{ … }}) – beim Erzeugen der Umgebung. Verfügbar sindsource(die Modulquelle mit Tag) und jeder Wert aus den Katalog-Standardwerten (defaultValues). Immer mitdefault(…)schreiben. - Terragrunt/HCL (
${ … },try(…)) – beim Ausrollen. Hier liest du alles, was die Umgebung bereitstellt, auslocal.common_vars.inputs.
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.
| Eingabe | Inhalt |
|---|---|
namespace, release_name_prefix | Kubernetes-Namespace der Umgebung und Präfix für Release-Namen |
domain, environment, organisation | Basis-Domain, Umgebungsname, Organisation |
ingress_class_name, cluster_issuer_name, certificates | Ingress-Klasse, cert-manager-Issuer, Zertifikate |
storage_class | Storage-Klasse für Volumes |
image_pull_secrets, registry_credentials | Namen 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>.key | LiteLLM-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_password | Admin der Umgebung |
smtp_host, smtp_port, smtp_from, smtp_from_name, smtp_username, smtp_password | Mailversand, leer wenn die Umgebung keinen hat |
http_proxy, https_proxy, no_proxy | Proxy, meist leer |
models | Modell-Liste der Umgebung |
Regeln für das Terraform-Modul#
- Keine
provider-Blöcke und keinbackend. Beides erzeugt die Plattform. Eine Dateiproviders.tfwird beim Ausrollen leer überschrieben. - Helm-Provider
~> 2.0. Die erzeugte Provider-Konfiguration nutzt die Syntax von Version 2. k8s_config_pathundk8s_contextnicht deklarieren – die Plattform deklariert sie selbst.- Nichts außerhalb des Namespace
var.namespaceanlegen; 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.
dependencies {
paths = ["../redis", "../../applications/litellm"]
}Was dann passiert:
- Beim Bestellen wählt die Plattform jede Abhängigkeit automatisch mit, auch über mehrere Stufen (deine App → LiteLLM → Redis, Presidio).
- Beim Erzeugen bekommt die Abhängigkeit ihre Secrets (z. B. das Redis-Passwort) und schreibt ihre Verbindungsdaten in die Umgebung.
- Beim Ausrollen wendet Terragrunt die Abhängigkeit vor deiner App an.
- 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. ../nameheiß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 Stufeai-applications(nachapplications). Das ist richtig für Apps, die LiteLLM nutzen. - Postgres ist keine Abhängigkeit, sondern eine verwaltete Datenbank (Datenbank und S3).
| Abhängigkeit | Pfad | Was deine App bekommt |
|---|---|---|
| Redis | ../redis | inputs.databases.redis.{host,port,password} |
| LiteLLM | ../../applications/litellm | inputs.hosts.litellm, inputs.domains.litellm, eigener Schlüssel über config.litellm.hcl.genai |
| Presidio | ../presidio | inputs.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#
- Abhängigkeit eintragen:
"../redis"imdependencies-Block. - Verbindungsdaten lesen. Das Redis-Modul der Umgebung veröffentlicht sie unter
databases.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, "")- Im Modul daraus die URL bauen, die deine App erwartet:
locals {
redis_url = "redis://:${var.redis_password}@${var.redis_host}:${var.redis_port}/0"
}hostist der Service im Namespace (<präfix>redis-master.<namespace>); wer einen voll qualifizierten Namen braucht, hängt.svc.cluster.localan.- 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.
- Abhängigkeit eintragen:
"../../applications/litellm". - Schlüssel anmelden mit
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, "")
}
}
}- Schlüssel und Adresse übergeben:
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 einensk-…-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.
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, "")
}
}
}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.databasesaufsecrets.database.<key>verweist. Dann trägt errequires.databasesim Katalog ein. - Im S3-Abschnitt heißt der Access-Key
username– so erwartet es die Bucket-Bereitstellung. Lies ihn alsinputs.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.
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.
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:
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, "")type | erzeugt | Felder |
|---|---|---|
password | Buchstaben und Ziffern | length (Standard 32) |
hex | Hex-Zeichenkette, 2 Zeichen je Byte | length in Bytes (Standard 32) |
fernet | Fernet-Schlüssel (44 Zeichen) | – |
prefixed | <prefix>-<zufall> | prefix, length (Standard 24) |
static | genau default | default – 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 inconfig.secrets.hcl.genai. - Ein Secret, das der Partner selbst setzen muss (z. B. ein externer API-Schlüssel), meldest du als
staticohnedefaultan und beschreibst es indescription. Das Plattform-Team trägt es dann ein.
6Adressen: Domain und Host#
locals {
domains = {
meine_app = "meine-app-${include.global.locals.domain}"
}
}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 mitingress_class_nameund dem Issuercluster_issuer_name.hosts.<key>muss zum Namen deines Kubernetes-Service passen. Andere Apps erreichen dich darüber.include.global.locals.*darfst du in allenconfig.*-Dateien nutzen; fehlt derinclude "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#
{
"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 }
}
}
}| Feld | Bedeutung |
|---|---|
name, description, maintainers | Grunddaten; description ohne catalog.summary wird zur Kurzbeschreibung |
catalog.label | Anzeigename |
catalog.summary | ein Satz, eine Zeile – Pflicht für Partner |
catalog.details | Klartext-Absätze; keine Zeichen < > * # und kein Backtick – Pflicht für Partner |
catalog.vendor | Hersteller – Pflicht für Partner |
catalog.category | core, observability, ui, tooling oder data – Pflicht, ohne Kategorie lehnt der Katalog den Eintrag ab |
catalog.role | application (Nutzer sehen sie) oder service (Baustein für andere Apps, z. B. Redis) |
catalog.tier | applications oder ai-applications; sonst aus den Abhängigkeitspfaden abgeleitet |
catalog.defaultValues | Jinja-Werte für terragrunt.hcl.genai; Platzhalter ${env}, ${org}, ${envName} werden ersetzt |
versions.<name>.source | woher 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 |
2Logo#
Lege das Logo in genai-cli/ ab. Die Plattform sucht in dieser Reihenfolge: logo.svg, logo.png, logo.jpg, logo.jpeg.
| Regel | Wert |
|---|---|
| Formate | SVG (bevorzugt), PNG, JPEG |
| Größe | höchstens 1 MiB im Repo |
| Rasterbilder | quadratisch, am besten 256 × 256 px; größere werden auf 256 px verkleinert |
| SVG | höchstens 128 KiB, kein <script>, keine on…-Attribute, kein foreignObject, keine externen Verweise, keine DTD |
| Darstellung | auf hellem Grund, ohne Rand, Motiv füllt die Fläche |
| nicht angenommen | logo.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.roleapplicationist undconfig.domains.hcl.genaieine Adresse unter genau dem Schlüssel liefert, mit Unterstrich: Katalogmeine-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.genaiund lässt sich je Umgebung umstellen.
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.
./pruefen.sh2. Modul – das Terraform-Modul gegen einen lokalen Cluster (kind, k3d, Docker Desktop) mit Werten, die sonst die Plattform liefert:
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:80beispiel/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#
- Repo in der GitLab-Gruppe
genai-plattform/applicationsanlegen (lassen). Nur von dort importiert die Konsole, und nur Repos mitgenai-cli/terragrunt.hcl.genaiauf dem Standard-Branch. - SemVer-Tag setzen.
- Dem Plattform-Team Bescheid geben: Repo, Tag, Ansprechpartner.
- 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.
- 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 liest | Woraus |
|---|---|
| Schlüssel | einziger Schlüssel in config.hosts/config.domains, sonst Repo-Name |
| Stufe | catalog.tier, sonst aus den Abhängigkeitspfaden |
| Abhängigkeiten | dependencies-Block, nur Einträge, die im Katalog sind |
| Datenbanken, Buckets | config.databases / config.s3_buckets mit Verweis auf secrets.database.<key> / secrets.s3_buckets.<key> |
| Texte, Kategorie, Rolle, Standardwerte | metadata.json → catalog |
| Logo | logo.svg / logo.png |
| Version | neuester SemVer-Tag |
3Checkliste#
Hake ab, was erledigt ist. Der Stand bleibt in diesem Browser gespeichert.
4Bekannte Stolperstellen#
| Symptom | Ursache | Lösung |
|---|---|---|
| Plan bricht mit „Unsupported attribute“ ab | Zugriff ohne try() auf Daten, die erst später kommen | try(…, "") |
| App findet Redis nicht | hosts.redis statt databases.redis oder Service-Name falsch | inputs.databases.redis.host nutzen |
| LiteLLM-Schlüssel leer | key-Zeile fehlt oder anders geschrieben | genau try(include.global.locals.secrets.litellm.keys.<key>, "") |
| S3-Zugriff verweigert | Access-Key als access_key geschrieben, als username gelesen | im S3-Abschnitt username verwenden |
| Admin-Passwort unbekannt | Secret admin_password erbt das Admin-Passwort | gewollt; sonst anders benennen |
| Statisches Secret leer | type = "static" ohne default | default setzen oder Plattform-Team trägt den Wert ein |
| Import blockiert: „unknown dependency“ | Abhängigkeit ist nicht im Katalog | erst die Abhängigkeit in den Katalog bringen |
| App für Partner unsichtbar | summary, details, vendor oder Logo fehlen, oder details enthält <>*# | Texte und Logo ergänzen |
| Keine Kachel im AI Hub | role ist service oder keine Domain | role: application, config.domains |
| Doppelter Provider beim Init | Modul bringt eigenen provider "helm" mit | Provider-Block entfernen |
| Logo fehlt trotz Datei | logo.webp oder icon.ico | als 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
app-entwicklung.md
Die ganze Anleitung als Markdown – dieselbe Quelle wie diese Seite.
llms.txt
Einstieg für Agenten: Dateien, Kernfakten, wo was steht.
vorlage/
Lauffähiges Repo mit pruefen.sh als Maßstab für „fertig“.
★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.
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.
- Ein App-Paket ist ein Git-Repo: Terraform-Modul im Wurzelordner, Plattform-Dateien in
genai-cli/. - Pflicht ist
genai-cli/terragrunt.hcl.genaiund inmetadata.jsoncatalog.category(core|observability|ui|tooling|data). Für Partner zusätzlichcatalog.summary(eine Zeile),catalog.details(Klartext ohne<>*#und Backtick),catalog.vendorund ein Logologo.svg/logo.png. - Schlüssel:
^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$. Inconfig.*,inputs.*undsecrets.*mit_statt-. .genai-Dateien sind Jinja-Vorlagen. Jinja-Variablen:sourceund die Schlüssel auscatalog.defaultValues. Immer{{ x | default(…) }}.- Zur Laufzeit liest das Modul alles aus
local.common_vars.inputs, wobeicommon_vars = read_terragrunt_config(find_in_parent_folders("terragrunt.hcl")). Jeder Zugriff intry(…, default). - Abhängigkeiten nur im Block
dependencies { paths = [...] }. Letzter Pfadteil = Katalogschlüssel. Verbindungsdaten nie überdependency-Outputs. - Redis: Pfad
../redis; leseninputs.databases.redis.host|port|password. - LiteLLM: Pfad
../../applications/litellm;config.litellm.hcl.genaimitlocals.litellm.<key>.{team_alias,key_alias,create_api_key=true,models=[],metadata,key=try(include.global.locals.secrets.litellm.keys.<key>,"")}; leseninputs.litellm.<key>.key; Basis-URLhttp://${inputs.hosts.litellm}:4000/v1. - Postgres/S3 nie als Abhängigkeit:
config.databases.hcl.genaimitsecrets.database.<key>.{host,port,name,username,password},config.s3_buckets.hcl.genaimitsecrets.s3_buckets.<key>.{bucket,endpoint,region,access_key→username,secret_key}. - Admin:
inputs.default_user_email|default_user_first_name|default_user_password. Eigene Secrets inconfig.secrets.hcl.genaiunterlocals.required_secrets.<key>.<name> = { type = password|hex|fernet|prefixed|static, length, default, prefix, description }; leseninputs.secrets.<key>.<name>.admin_passworderbt das Admin-Passwort. - Modul: keine
provider-/backend-Blöcke, keine Variablenk8s_config_path/k8s_context, Helm-Provider~> 2.0, nur Ressourcen im Namespacevar.namespace, keinegenai.govdigital.de/*-Labels, Release-Name${release_name_prefix}<key>. - Images aus
registry.onstackit.cloud/govdigital/…mit festem Tag;image_pull_secretsausinputs. - Nie Zugangsdaten in
metadata.json,defaultValuesoder Klartext-Dateien. - Vor der Übergabe
pruefen.shaus der Vorlage ausführen; Ergebnis muss fehlerfrei sein.
2Begriffe#
| Begriff | Bedeutung |
|---|---|
| Umgebung | eine 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 |
| Katalog | Liste aller bestellbaren Apps in der Konsole, gepflegt vom Plattform-Team |
| Schlüssel | fester Name einer App im Katalog, z. B. open-webui |
| Sidecar | eine der config.*-Dateien; die Plattform legt sie in der Umgebung ab und führt sie zu inputs zusammen |
secrets.yaml | mit SOPS verschlüsselte Secrets der Umgebung |
| Secret-Abgleich | Schritt, der Zugangsdaten verwalteter Datenbanken und Buckets in secrets.yaml schreibt |
| AI Hub | Startseite einer Umgebung mit Kacheln aller Apps |
| Partner | Organisation, 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.
genai-cli/
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.