Aspire-AppHost¶
Coworkee.Aspire macht aus dem AppHost wenige Zeilen. Du nennst die App; Coworkee findet ihre Dienste, ergänzt die Infrastruktur, die jeder braucht, und die Einstellungen, die sie verbinden.
var builder = DistributedApplication.CreateBuilder(args);
builder.AddCoworkeeApp("myapp", options =>
{
options.DisplayName = "MyApp";
options.LogoUrl = "/coworkee-icon.svg";
options.UseKeycloak(keycloak => keycloak.Users.Add(new KeycloakUser("info@coworkee.de", "Administrator", "MyApp") { Password = "Dev-Passw0rd!" }));
}).AddProjects();
builder.Build().Run();
LogoUrl ist das Logo der Anmeldeseiten, solange das Theme keines hat: Ein Pfad wird auf der Web-App genommen ({web}/coworkee-icon.svg), eine absolute Adresse so, wie sie ist.
options.SignIn setzt Layout, Medium und Seite des Mediums der Anmeldeseiten (Coworkee:Auth:Design des Auth-Servers); ein Pfad zum Medium wird wie LogoUrl auf der Web-App aufgelöst:
options.SignIn.Layout = SignInLayouts.Split;
options.SignIn.MediaUrl = "/media/welcome.mp4";
options.SignIn.MediaSide = SignInMediaSide.Right;
Sharemee ergänzt danach einen Worker, dessen Name keiner Konvention folgt:
var app = builder.AddCoworkeeApp("shareme", ...).AddProjects();
app.AddWorker<Projects.Shareme_Processing_Host>("processing");
Projekte nach Konvention¶
AddProjects() schaut sich die Projects.*-Typen an, die Aspire für die referenzierten Projekte des AppHosts erzeugt, und fügt sie nach Namen in dieser Reihenfolge hinzu:
| Typname | Hinzugefügt mit | Ressource |
|---|---|---|
*_Migrations |
AddMigrations |
{name}-migrations |
*_Auth |
AddAuthServer |
{name}-auth |
*_Api |
AddApi |
{name}-api |
*_Worker*, *Worker |
AddWorker |
{name}-worker |
*_Web |
AddWeb |
{name}-web |
*_AirWorks |
AddAirWorks |
{name}-airworks |
*_ElsaStudio |
AddElsaStudio (in einem Deployment nur, solange das Tool elsastudio an ist) |
{name}-elsastudio |
Das Suffix ist der Teil nach dem ersten Unterstrich, kleingeschrieben mit Bindestrichen: Aus MyApp_Jobs_Worker wird myapp-jobs-worker. Andere Projekte bleiben unberührt.
builder.AddCoworkeeApp("myapp", options =>
{
options.Configure("api", api => api.WithReplicas(2)); // nach Suffix, auch für explizit hinzugefügte Dienste
options.Skip("web", "MyApp_Admin_Api"); // nach Suffix oder Typname
}).AddProjects();
Explizite Aufrufe funktionieren weiter und haben Vorrang: Ein Projekt, das vor AddProjects() hinzugefügt wurde, kommt nicht doppelt hinzu. Soll ein gefundenes Projekt eine andere Rolle oder ein anderes Suffix bekommen, überspringst du es und fügst es danach selbst hinzu.
Rollen¶
| Methode | Ressource | Verdrahtung |
|---|---|---|
AddMigrations<T>() |
{name}-migrations |
Datenbank, wartet auf den Datenbankserver, die referenzierten Module wie jeder Dienst (Blob-Speicher für geseedete Dateien, ...), Dashboard-Befehle |
AddAuthServer<T>() |
{name}-auth |
externe Endpunkte, kein Job-Server, öffentliche Auth-URL, Entwicklungszertifikate in Development, Keycloak-Anbieter |
AddApi<T>() |
{name}-api |
Token-Authority und Audience {name}_api, API-Scope am Auth-Server, Setup-Token (lokal: dasselbe für alle Dienste, in Konsole und Dashboard sichtbar, solange das System nicht eingerichtet ist, siehe Setup-Assistent) |
AddWorker<T>(suffix) |
{name}-{suffix} |
nur Infrastruktur, keine öffentlichen Endpunkte |
AddWeb<T>() |
{name}-web |
BFF-Einstellungen, OIDC-Client am Auth-Server mit Redirect-URIs, Referenz auf die API, Links zu Swagger und zum Job-Dashboard |
AddAirWorks<T>() |
{name}-airworks |
wie AddWeb: BFF-Einstellungen, der nächste OIDC-Client am Auth-Server, ein eigener Name des Sitzungs-Cookies; für den AirWorks-Client |
AddElsaStudio<T>() |
{name}-elsastudio |
der nächste OIDC-Client am Auth-Server, ein eigenes Sitzungscookie, die Elsa-API der ersten API, Coworkee:Workflows:Elsa:StudioUrl der APIs; für den Workflow-Designer |
Ruf sie in dieser Reihenfolge auf (AddProjects() tut das). AddWeb vor AddAuthServer wirft eine Exception, die genau das sagt.
Infrastruktur aus den Paketen¶
Bevor Coworkee einen Dienst verdrahtet, liest es dessen obj/project.assets.json und schaut, welche Pakete und Projekte er nutzt, direkt oder über andere Projekte:
| Paket im Dienst | Er bekommt |
|---|---|
Coworkee.Infrastructure |
Connection-String der App-Datenbank und Coworkee:Database:Provider, wartet, bis die Migrationen fertig sind |
Coworkee.Realtime |
Redis für die SignalR-Backplane, Anwesenheit, Cache-Invalidierung und Sitzungsänderungen zwischen Instanzen (Skalieren mit Redis); options.WithoutRedis() lässt es weg |
Coworkee.Mailing |
Mailpit als Standard-SMTP-Server |
Coworkee.Storage |
den gemeinsamen Blob-Ordner .data/blobs (nur lokal; veröffentlichte Container behalten ihren Standard) |
Coworkee.Search.Elasticsearch |
Elasticsearch |
Coworkee.Notifications |
Links in Zusammenfassungsmails zeigen auf die Web-App |
Grafana (mit Prometheus, Loki, Tempo und Collector) und WebDataStudio kommen mit options.UseGrafana() und options.UseWebDataStudio() dazu, siehe Grafana und WebDataStudio. Kibana kommt mit options.UseKibana(), siehe Kibana.
Container entstehen nur, wenn ein Dienst sie braucht. Postgres, SQL Server, Redis, Elasticsearch und Keycloak halten ihre Daten in Volumes; --Coworkee:EphemeralInfrastructure=true startet sie leer, das nutzen die Tests.
Kibana¶
options.UseKibana() stellt Kibana neben den Elasticsearch-Container (nicht bei UseSearch: ein eigenes Elasticsearch bringt sein eigenes Kibana mit). Kibanas Single Sign-on (OpenID Connect, SAML) braucht eine kostenpflichtige Elastic-Lizenz, deshalb setzt der AppHost oauth2-proxy davor:
- Die Ressource
kibanaist der Proxy und die Adresse, die du öffnest. Er meldet dich am Auth-Server der App an (Client<app>-kibana); ist Keycloak an, schickt der Auth-Server dich wie bei der App zu Keycloak weiter. - Der Auth-Server lässt nur Administratoren der System-Organisation durch (
AuthClientOptions.SystemAdministratorsOnly). Alle anderen bekommenaccess_denied. kibana-serverist Kibana selbst. Es lässt jede Anfrage als Elasticsearch-Benutzerelasticherein (sein Anonymous-Provider, Teil der freien Lizenz), deshalb darf nur der Proxy es erreichen: Das Dashboard zeigt keinen Link darauf, ein Deployment veröffentlicht keinen Port davon (ein lokaler Lauf behält seinen Port auf der Loopback-Adresse).kibana-setupsetzt das Passwort des eingebauten Benutzerskibana_system, sobald Elasticsearch antwortet.- Kibana nimmt die Image-Version des Elasticsearch-Containers. Seine gespeicherten Objekte liegen in Elasticsearch und damit in dessen Volume.
Ohne Auth-Server behält Kibana sein eigenes Login: Benutzer elastic mit dem Passwort von Elasticsearch (Parameter elasticsearch-password). coworkee new --elasticsearch --kibana (Template-Option kibana, standardmäßig aus) fügt den Aufruf hinzu.
Eigene Module¶
app.Modules enthält diese Verdrahtung nach Paket- oder Projektname. App-Module melden ihren Bedarf genauso an; jeder Dienst, der das Projekt direkt oder transitiv referenziert, bekommt ihn:
var app = builder.AddCoworkeeApp("myapp");
var stripe = builder.AddConnectionString("stripe");
app.Modules.Add("MyApp.Billing", (app, service) => service.WithReference(stripe));
app.AddProjects();
Registriere Module, bevor du die Dienste hinzufügst. app.Modules[CoworkeeModules.Search] = ... ersetzt eine eingebaute Verdrahtung, app.Modules.Remove(...) entfernt sie.
Eigene Infrastruktur¶
Jedes Teil lässt sich konfigurieren, ersetzen oder abschalten:
builder.AddCoworkeeApp("myapp", options =>
{
options.ConfigurePostgres(postgres => postgres.WithPgAdmin())
.ConfigureRedis(redis => redis.WithRedisInsight())
.ConfigureMail(mail => mail.WithLifetime(ContainerLifetime.Persistent))
.ConfigureSearch(search => search.WithEnvironment("ES_JAVA_OPTS", "-Xmx1g"))
.ConfigureKeycloak(keycloak => keycloak.WithLifetime(ContainerLifetime.Persistent));
// Dienste mit Coworkee.Search.Elasticsearch bekommen kein Elasticsearch
options.Without(CoworkeeModules.Search);
}).AddProjects();
| Option | Statt |
|---|---|
UseDatabase(resource) |
des Datenbank-Containers; jede Ressource mit Connection-String, z. B. builder.AddConnectionString("myapp") |
UseSqlServer(configure) |
von Postgres: ein SQL-Server-Container (app.SqlServer), oder mit UseDatabase ein eigener SQL Server |
UseSqlite(path) |
von Postgres: eine SQLite-Datei, ohne Angabe .data/<name>.db neben src; kein Container |
UseRedis(resource) |
des Redis-Containers |
UseSearch(resource) |
des Elasticsearch-Containers |
UseMail(endpoint) |
von Mailpit; der SMTP-Endpunkt einer anderen Ressource |
UseKeycloak(resource, ...) |
des Keycloak-Containers; dein Keycloak bekommt den Realm |
UseSqlServer und UseSqlite setzen bei den Diensten auch Coworkee:Database:Provider; die Migrationen der App müssen für diesen Provider erstellt sein (siehe Datenbank-Provider).
Die Dienste bekommen eigene Ressourcen unter den Namen, die sie erwarten (ConnectionStrings:myapp, redis, elasticsearch). Coworkee wartet nicht auf sie; ergänze bei Bedarf WaitFor über options.Configure.
// Produktion: der Connection-String kommt aus der Konfiguration (ConnectionStrings:myapp) oder wird beim Deployment abgefragt
var database = builder.AddConnectionString("myapp");
builder.AddCoworkeeApp("myapp", options => options.UseDatabase(database)).AddProjects();
Dashboard¶
Läuft der AppHost lokal in Development, hat die Migrations-Ressource zwei Befehle:
- Re-run migrations startet das Migrationsprojekt erneut: neue Migrationen und das Seeding der Demodaten.
- Reset database löscht nach einer Rückfrage die App-Datenbank und führt die Migrationen erneut aus. Den Befehl gibt es nur für den eingebauten Postgres-Container, nie für SQL Server, SQLite oder eine eigene Datenbank und nie außerhalb von Development.
Die Web-Ressource verlinkt Swagger (/swagger) und, wenn eine API Coworkee.BackgroundJobs nutzt, das Hangfire-Dashboard (Jobs, /admin/jobs). Mailpit zeigt seinen Posteingang als Mailpit UI.
Dienstadressen¶
Jeder Dienst der App bekommt die Adressen der anderen unter Coworkee:Services:<Ressource>: das Aspire-Dashboard (dashboard), das Hangfire-Dashboard (jobs), jedes Projekt (myapp-api, myapp-auth, myapp-web, mit HealthPath /health) und jeden Container mit http-Endpunkt (mail, pgadmin, keycloak, ...), auch Ressourcen, die nach AddCoworkeeApp hinzukommen. Injiziert werden sie als IOptionsMonitor<CoworkeeServicesOptions>; die Admin-Seite Dienste zeigt sie mit ihrem aktuellen Zustand (siehe Einstellungen). Ein Deployment trägt dort die öffentlichen Adressen seiner Tools ein; weitere kommen in appsettings.
Ein AspireUI im AppHost bringt die Apps und Stacks, die du selbst damit hostest, auf dieselbe Seite. builder.AddAspireUI().WithRetrieveApi() genügt: Der AppHost gibt die Adresse (und den Schlüssel aus WithRetrieveApi(...): Ein Parameter bleibt eine Referenz, nie ein aufgelöster String; ein Klartext-Schlüssel wird als Text weitergegeben) als Coworkee:AspireUI:Url und ApiKey an jeden Dienst weiter. Das erste AspireUI mit Retrieve-API wird verwendet; eines ohne wird ignoriert, und eine veröffentlichte App bekommt nichts (Coworkee:AspireUI dann in appsettings setzen). Dienste finden sie über IServiceDirectory (siehe Einstellungen).
Typisierte Einstellungen¶
Einstellungen werden über den Konfigurationsbaum gesetzt, nicht über Zeichenketten:
api.WithSetting(s => s.Coworkee.Storage.Provider, "S3")
.WithSetting(s => s.Coworkee.Settings.Defaults["Mail.Smtp.Host"], "smtp.example.com")
.WithSettings(s => s.Coworkee.Bff.ForwardedPrefixes, "/admin/jobs", "/hubs");
Aus s => s.Coworkee.Bff.Scopes[0] wird die Umgebungsvariable Coworkee__Bff__Scopes__0. Werte können Zeichenketten, Zahlen, Wahrheitswerte, Endpunkt-Referenzen, Referenzausdrücke oder Parameter sein. CoworkeeSettings spiegelt die echten Options-Klassen aus Coworkee.Contracts.Configuration; eine umbenannte Eigenschaft bricht also den Build des AppHosts, nicht die laufende App.
Keycloak¶
UseKeycloak fügt einen Keycloak-Container mit einem Realm hinzu, der wie die App heißt:
- ein vertraulicher Client
{name}-auth, dessen Secret ein generierter Parameter ist ({name}-keycloak-client-secret), - die angegebenen Benutzer, jeder mit seinem
Password; Benutzer ohne teilen sich ein generiertes Passwort ({name}-keycloak-user-password, im Dashboard sichtbar und in den User-Secrets des AppHosts gespeichert).
Der Realm wird bei jedem Start auf den aktuellen Stand gebracht, sobald Keycloak bereit ist und bevor der Auth-Server startet: Ein fehlender Realm wird angelegt, ein vorhandener (im Daten-Volume) bekommt über die Admin-API die aktuellen Client-Secrets, Redirect-Adressen, Benutzer und Passwörter. Ein geänderter Secret-Parameter oder ein geändertes Passwort gilt also ab dem nächsten Start, und Benutzer oder Clients, die du in der Keycloak-Konsole angelegt hast, bleiben erhalten. Das Ressourcen-Log von Keycloak meldet Realm myapp is up to date: clients ..., users ....
Der Auth-Server bekommt die Anbieter-Einstellungen und bietet Sign in with Keycloak an. LoginMode schaltet zwischen beidem, nur extern und nur intern um und ist der Standard der Einstellung Anmeldemethoden (Auth.LoginMode), die Administratoren zur Laufzeit ändern (siehe Anmelderegeln); Port legt den Host-Port fest, wenn du eine feste URL willst.
Die Api-Hosts liefern die Einstellungen, also bekommen auch sie Coworkee:Auth:External:Mode und den Anzeigenamen des Anbieters, damit die Einstellung dort definiert ist; das Client-Secret und die übrigen Anbieter-Einstellungen bleiben beim Auth-Server.
Browser schicken die Cookies von localhost an jeden Port, also erreichen die Cookies aller lokalen Apps auch Keycloak. Über HTTPS spricht Keycloak HTTP/2 und nimmt dabei standardmäßig nur 8 KB Request-Header an; darüber antwortet es mit 431 Request Header Fields Too Large. Der Container bekommt deshalb QUARKUS_HTTP_LIMITS_MAX_HEADER_LIST_SIZE=65536, die 64 KB, die er über HTTP/1.1 annimmt. Zeigt der Browser trotzdem 431, lösch die localhost-Cookies anderer Apps; Auth-Server und Web-App (Kestrel) nehmen 32 KB an.
Anmeldeseite im Design der App¶
Die Anmeldeseite von Keycloak sieht aus wie die App: die Farben des Standard-Themes (hell und dunkel), sein Logo (oder LogoUrl), der DisplayName der App über dem Formular sowie Layout und Medien aus SignIn (Karte, geteilt oder Slide). Coworkee schreibt dafür ein Login-Theme coworkee-{name}, ein Kind des Keycloak-Themes keycloak.v2 mit einem Stylesheet, einem kleinen Skript und der Titel-Meldung, ohne eigene Templates, damit es auch mit neueren Keycloak-Versionen funktioniert.
- Der AppHost mountet
src/MyApp.AppHost/.coworkee/keycloak-themesals/opt/keycloak/themesin den Keycloak-Container (die.gitignoredes Templates lässt.coworkee/aus) und schreibt das Theme beim ersten Start dorthin (ein nicht beschreibbarer Ordner wird als Warnung geloggt und hält den Realm nicht auf), mit den Ocean-Farben und den Optionen ausAddCoworkeeApp; Pfade wie/logo.svgoder/media/welcome.mp4werden zu Adressen der Web-App. - Der Realm-Abgleich setzt bei jedem Start
loginTheme,displayNameunddisplayNameHtmldes Realms, für einen neuen Realm und für einen aus dem Daten-Volume. - Die Api-Hosts bekommen
Coworkee:Keycloak:ThemeDirectory,ThemeName,AppName,LogoUrlundBaseUrl(die Web-App) und schreiben das Theme neu: beim Start, eine Sekunde nachdem eine Theme-Änderung gespeichert ist (Theme speichern, anderes Theme zum Standard machen) oder ein Administrator das Design der Anmeldeseite speichert, und mit Nach Keycloak übernehmen (siehe Theming). start-devcacht keine Themes, eine Änderung erscheint also beim nächsten Seitenaufruf.
Abschalten mit options.UseKeycloak(k => k.LoginTheme = false): kein Mount, kein Theme, der Realm behält sein Theme (fehlt der Ordner, nimmt Keycloak sein eigenes). Ein eigener Keycloak (UseKeycloak(resource, ...)) bekommt den Anzeigenamen des Realms, aber kein Theme; mounte den Ordner selbst, wenn du es willst.
Weitere Clients und die Benutzer¶
Andere Ressourcen des AppHosts können sich über denselben Realm anmelden und seine Benutzer wiederverwenden, zum Beispiel ein Dashboard mit OpenID Connect:
var app = builder.AddCoworkeeApp("myapp", options => options.UseKeycloak(k => k.Users.AddRange(users))).AddProjects();
var grafana = builder.AddContainer("grafana", "grafana/grafana").WithHttpEndpoint(targetPort: 3000);
var client = app.AddKeycloakClient("myapp-grafana", [ReferenceExpression.Create($"{grafana.GetEndpoint("http")}/login/generic_oauth")]);
grafana.WithEnvironment("GF_AUTH_GENERIC_OAUTH_CLIENT_ID", client.ClientId)
.WithEnvironment("GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET", client.Secret)
.WithEnvironment("GF_AUTH_GENERIC_OAUTH_AUTH_URL", ReferenceExpression.Create($"{app.KeycloakAuthority}/protocol/openid-connect/auth"));
Member von CoworkeeApp |
|
|---|---|
AddKeycloakClient(clientId, redirectUris?, secret?) |
ein weiterer vertraulicher Client des Realms; ohne Secret ein generiertes ({clientId}-secret), ohne Redirect-Adressen jede lokale Adresse |
KeycloakClients |
die Clients des Realms, der des Auth-Servers zuerst |
KeycloakAuthority |
Issuer-Adresse des Realms, {keycloak}/realms/{realm} |
KeycloakUsers, KeycloakPassword(user) |
die Benutzer und das Passwort jedes Benutzers als Reference Expression, etwa für den lokalen Login einer anderen Ressource |
Ruf sie nach UseKeycloak und AddAuthServer (oder AddProjects) auf.
AspireUI¶
AspireUI meldet sich mit einem Aufruf am Auth-Server der App an; die Administratoren der Systemorganisation werden AspireUI-Admins:
using Nextended.Aspire.Hosting.AspireUI;
var app = builder.AddCoworkeeApp("myapp").AddProjects();
builder.AddAspireUI().WithCoworkeeSignOn(app);
Der Aufruf registriert den Client {app}-{resource} am Auth-Server mit der Redirect-Adresse /api/auth/sso/callback und setzt den Claim role als Gruppen. Ist Keycloak an, schickt der Auth-Server weiter zu Keycloak wie bei der App. AspireUI übernimmt Einstellungen nur, solange sie leer sind; deshalb erzwingt der Aufruf die Einstellungen des App-Hosts (ASPIREUI_SET_FORCE), sonst behielte ein Daten-Volume aus einem früheren Client die alte Anmeldung.
Tools melden sich am Auth-Server an¶
Grafana, WebDataStudio, AspireUI, Open WebUI und der Proxy von Kibana melden sich am Auth-Server der App an, mit oder ohne Keycloak. Jedes bekommt einen vertraulichen Client {app}-{tool}, der nicht im Launcher steht und nur die Administratoren der Systemorganisation hereinlässt (SystemAdministratorsOnly); sie sind Admins des Tools. Die lokale Administrator-Anmeldung jedes Tools bleibt. Solange der App-Host läuft, nennt sich der Auth-Server nach der Adresse des Browsers (Coworkee:Auth:Issuer) und gibt Containern mit Coworkee:Auth:DynamicBackchannel Token, Benutzerinfo und Schlüssel unter der Adresse, unter der sie gefragt haben: Die Container erreichen ihn über das Container-Netz, der Browser über den Host. Ein Deployment nutzt für beides die öffentliche Adresse.
Open WebUI¶
Open WebUI ist ein Chat im Browser über die Modelle eines Ollama-Containers:
Der Administrator der App meldet sich lokal an (WEBUI_ADMIN_EMAIL, über das Formular registriert sich niemand), und mit dem Auth-Server melden sich die Administratoren der Systemorganisation darüber an, als Admins von Open WebUI (Client {app}-openwebui, PKCE). Die Einstellungen gehören dem App-Host (ENABLE_PERSISTENT_CONFIG=false): Sonst behielte Open WebUI die Adressen seines ersten Starts in seiner Datenbank. Daten im Volume {app}-openwebui-data. coworkee new --ai-provider ollama --open-web-ui (Template-Option openWebUI) fügt den Aufruf ein; coworkee deploy --without openwebui lässt es im Deployment weg.
AspireUI in einer generierten App¶
coworkee new --aspire-ui (Template-Option aspireUI, standardmäßig aus) fügt dieselben Zeilen in src/MyApp.AppHost/AppHost.cs ein, dazu den Paketverweis Nextended.Aspire.Hosting.AspireUI im AppHost und einen Abschnitt in LOGIN.md: Der Administrator meldet sich lokal mit E-Mail und Passwort der App an oder am Auth-Server der App (WithCoworkee(app)). Das Studio zeigt die Option neben Grafana und WebDataStudio; die Vorlage Everything schaltet sie ein.
builder.AddAspireUI()
.WithAdminUser(administrator.Email, administrator.Password)
.WithSeedFromDirectory(builder.AppHostDirectory)
.WithCoworkeeSignOn(app)
.WithRetrieveApi();
Veröffentlichen¶
coworkee deploy compose|azure|k8s veröffentlicht den AppHost für Docker Compose, Azure Container Apps oder Kubernetes: öffentliche Adressen, Token-Zertifikate, der erste Administrator und der Mailserver als Parameter, keine Demo-Benutzer, und die Tools des AppHosts (Keycloak, Grafana, WebDataStudio, LibreTranslate, Ollama, AspireUI) kommen mit, außer du schaltest sie mit DeployTool oder coworkee deploy --without ab. PublishToDockerCompose(configure) konfiguriert weiterhin die Compose-Umgebung (compose => compose.WithDashboard(false)); eine Umgebung, die der AppHost vor AddCoworkeeApp selbst hinzufügt, ersetzt die von Coworkee:Deploy:Target. Alles Weitere, auch was geprüft ist, steht unter Deployment.