Die Konfiguration unseres Java-Backend über Umgebungsvariablen zur Laufzeit ist zum Beispiel mit MicroProfile-Config im JEE/Quarkus Kontext oder ConfigurationProperties in Spring Boot ziemlich einfach. Wenn unsere Anwendung einmal gebaut ist, können wir sie in unterschiedlichen Umgebungen ausrollen und verwenden. Das ist insbesondere praktisch, wenn wir die Anwendung als Containerimage (z.B. mit Docker) bereitstellen. Hier ist es üblich den Container bei der Ausführung mit den notwendigen Konfigurationen über Umgebungsvariablen zu versorgen. So können wir unser Image auch zuerst in einer Testumgebung betreiben und nach einer erfolgreichen Prüfung in die Produktivumgebung ausrollen ohne dabei das Image anpassen zu müssen. Diese Vorgehensweise kann allerdings etwas ins Stocken geraten, wenn wir auch unser JavaScript-Frontend derart konfigurieren wollen. Das Problem tritt in unseren Projekten zum Beispiel auf, wenn die Anwendung über einen SSO-Server wie Keycloak authentifiziert wird, die URL und andere Konfigurationen für die Anmeldung sich aber je nach Einsatzumgebung unterscheiden.
In einer Angular-Anwendung geschieht die Konfiguration (ähnlich wie auch in anderen JavaScript-Anwendungen) üblicherweise zur Build-Zeit über Environments (siehe: https://angular.io/guide/build#configuring-application-environments). Hier kann man grundsätzlich auch mehrere Umgebungen unterscheiden, die Konfigurationswerte müssen allerdings bereits zum Build-Zeitpunkt festgelegt werden, unabhängig von der späteren tatsächlichen Einsatzumgebung.
Diese Beschränkung ist für den Betrieb der Anwendung als Containerimage natürlich ärgerlich. Gibt es nicht einen Weg wie wir die einfache Konfigurierbarkeit des Java-Backend nutzen können um die Umgebungsvariablen auch dem Angular-Frontend zur Verfügung zu stellen? Klar! Das Frontend fragt sowieso die darzustellenden Daten beim Backend an, da können wir auch eine Schnittstelle hinzufügen, die die Werte der Umgebungsvariablen bereitstellt.
Konfiguration des Backend
In unserem einfachen Beispielprojekt (GitHub) stellen wir im Quarkus-Backend am Endpunkt /api/envConfig
die Property de.gedoplan.quinoaDemo.envConfig.runtimeEnvironment
bereit. Für die Konfiguration verwenden wir die @ConfigProperties
Annotation von MicroProfile-Config.
@ConfigProperties(prefix = "de.gedoplan.quinoaDemo.envConfig")
public class EnvConfig {
private String runtimeEnvironment;
[... Getter ...]
}
@Path("/envConfig")
public class EnvConfigResource {
@Inject
@ConfigProperties
EnvConfig envConfig;
@GET
@Produces(MediaType.APPLICATION_JSON)
public EnvConfig getEnvConfig() {
return envConfig;
}
}
Den Konfigurationswert können wir in der application.properties
Datei mit default-Werten belegen und/oder zur Laufzeit zum Beispiel über Umgebungsvariablen setzen:
%dev.de.gedoplan.quinoaDemo.envConfig.runtimeEnvironment=default-dev
%test.de.gedoplan.quinoaDemo.envConfig.runtimeEnvironment=default-test
%prod.de.gedoplan.quinoaDemo.envConfig.runtimeEnvironment=default-prod
DE_GEDOPLAN_QUINOADEMO_ENVCONFIG_RUNTIMEENVIRONMENT=<env-name> \
java -jar target/quarkus-app/quarkus-run.jar
docker run -p 8080:8080 \
-e DE_GEDOPLAN_QUINOADEMO_ENVCONFIG_RUNTIMEENVIRONMENT=<env-name> \
quinoa-demo
Laden der Konfiguration im Frontend
Das Frontend im Beispielprojekt ist eine Angular 17 Anwendung mit standalone components. Dasselbe Prinzip kann allerdings auch in einer Anwendung mit @NgModule
verwendet werden.
Zunächst legen wir hier ein Interface und einen Service an um die Konfiguration aus dem Backend zu laden und bereitzustellen.
export interface EnvConfig {
runtimeEnvironment: string;
}
@Injectable()
export class EnvConfigService {
private config!: Promise<EnvConfig>;
constructor(private http: HttpClient) {}
loadConfig(): void {
this.config = firstValueFrom(this.http.get<EnvConfig>('/api/envConfig'));
}
getConfig(): Promise<EnvConfig> {
if (!this.config) {
this.loadConfig();
}
return this.config;
}
}
In der ApplicationConfig
, die in main.ts
an bootstrapApplication()
übergeben wird, können wir dafür sorgen, dass die Konfiguration beim Initialisieren der Anwendung geladen wird. In diesem Fall wird die ApplicationConfig
in app.config.ts
erstellt.
export function initializeEnvConfigService(envConfigService: EnvConfigService) {
return () => envConfigService.loadConfig();
}
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
importProvidersFrom(HttpClientModule),
EnvConfigService,
{
provide: APP_INITIALIZER,
useFactory: initializeEnvConfigService,
multi: true,
deps: [EnvConfigService],
}
]
};
Über den EnvConfigService
können wir nun auf die Konfiguration zugreifen:
@Component({[...]})
export class DetailComponent implements OnInit {
envConfig = {
runtimeEnvironment: ''
} as EnvConfig;
constructor(private envConfigService: EnvConfigService) { }
ngOnInit(): void {
this.envConfigService.getConfig().then(data => this.envConfig = data)
}
}
Anwendungsfall: Keycloak-Authentifizierung
Wie in dem einfachen Beispiel oben können wir auch eine Keycloak-Authentifizierung damit konfigurieren. Eine beispielhafte Umsetzung findet sich auf dem keycloak Branch des Repositories. Für die Authentifizierung verwenden wir keycloak-angular
als Dependency. Diese muss mit der Keycloak-URL, dem Realm und der Client-Id konfiguriert werden. Vor dem Initialisieren des KeycloakService
können wir in der app.config.ts
sicherstellen, dass die Konfiguration geladen ist und die entsprechenden Werte verwenden:
function initializeEnvConfigAndKeycloak(keycloak: KeycloakService, envConfigService: EnvConfigService) {
return () => new Promise<boolean>((resolve) => {
envConfigService.getConfig().then(envConfig => {
resolve(keycloak.init({
config: {
url: envConfig.keycloakFrontendUrl,
realm: envConfig.keycloakRealm,
clientId: envConfig.keycloakClient,
},
loadUserProfileAtStartUp: true,
initOptions: {
onLoad: 'login-required'
}
}));
});
});
}
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
importProvidersFrom(HttpClientModule),
KeycloakAngularModule,
importProvidersFrom(KeycloakAngularModule),
EnvConfigService,
{
provide: APP_INITIALIZER,
useFactory: initializeEnvConfigAndKeycloak,
multi: true,
deps: [KeycloakService, EnvConfigService]
}
]
};
Im Quarkus-Backend ist die Keycloak-Authentifizierung konfiguriert, der Endpunkt zum Laden der Konfiguration muss allerdings ungesichert erreichbar sein um die Angular-Anwendung zu initialisieren. Da hier nur Informationen übertragen werden, die sowieso öffentlich im JavaScript-Client konfiguriert werden, stellt das auch kein Sicherheitsproblem dar. Geheime Schlüssel oder ähnliches sollten natürlich über diesen Weg nicht übertragen werden.
Die Anwendung kann jetzt ohne weitere Konfigurationsanpassung im Angular-Frontend ausgeführt werden und ein erstellter Container kann bei der Ausführung zum Beispiel in einem Kubernetes-Cluster einfach über das Backend konfiguriert werden.
Das ganze Projekt inklusive Ausführungsbeispiele gibt es auf GitHub.