GEDOPLAN
QuarkusDevOpsJakarta EE (Java EE)Webprogrammierung

Build Once, Deploy Everywhere: Umgebungsvariablen im Angular-Frontend

QuarkusDevOpsJakarta EE (Java EE)Webprogrammierung
build once deploy everywhere

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.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Bitte füllen Sie dieses Feld aus.
Bitte füllen Sie dieses Feld aus.
Bitte gib eine gültige E-Mail-Adresse ein.
Sie müssen den Bedingungen zustimmen, um fortzufahren.

Autor

Diesen Artikel teilen

LinkedIn
Xing

Gibt es noch Fragen?

Fragen beantworten wir sehr gerne! Schreibe uns einfach per Kontaktformular.

Kurse

weitere Blogbeiträge

IT-Training - GEDOPLAN
Webprogrammierung

Angular, testen mit Karma/Jasmine

Anwendungen zu testen ist ein leidiges Thema und wird gerne aus Zeitgründen vernachlässigt. Jeder der schon mal ein größeres Refactoring…

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!

Work Life Balance. Jobs bei Gedoplan

We are looking for you!

Lust bei GEDOPLAN mitzuarbeiten? Wir suchen immer Verstärkung – egal ob Entwickler, Dozent, Trainerberater oder für unser IT-Marketing! Schau doch einfach mal auf unsere Jobseiten! Wir freuen uns auf Dich!