Die Geschichte eines Hacks.
Problemstellung
Da wir vorwiegend mit Trunk-Based-Development arbeiten sind für uns Feature Toggles unerlässlich. Damit können einzelne Features (z.B. im Web Cockpit) ein- und ausgeschaltet werden. Um diese Infrastruktur dazu noch flexibler zu gestalten haben wir kürzlich entschieden RemoteConfiguration von Firebase einzubinden.
Zu welchem Firebase Projekt sich das Web Cockpit verbinden soll, ist von der Instanz abhängig. So sollte die Development Instanz auf ein anderes Projekt verbinden als die produktiven Instanzen der Betreiber. Diese Konfiguration soll, wie bei uns üblich, in unserem GitOps Repository hinterlegt werden können. Die eingestellten Werte werden dann an die Applikation als Umgebungsvariablen übergeben und schon verbindet sich die Instanz mit dem korrekten Projekt, dachten wir…
Das Problem: Unser Web Cockpit ist eine vue Applikation, welche während des Build Prozesses in statische Dateien umgewandelt und über den Webserver nginx in einem Docker Container ausgeliefert wird. Der in vue übliche Weg mit process.env.MY_ENV_VAR
auf eine Umgebungsvariable zuzugreifen klappt so natürlich nicht.
Lösung
Um dies zu lösen gibt es verschiedene mehr oder minder komplexe Ansätze. Wir möchten euch unsere Lösung dieses Problems kurz vorstellen, da wir denken dass sie verhältnismässig simple ist.
Beim Start des Docker Containers schreiben wir die Umgebungsvariablen in eine Datei.
Dazu passen wir den ENTRYPOINT im Dockerfile zum Starten von nginx entsprechend an:
ENTRYPOINT ["/write-env-to.sh /usr/share/nginx/html/environment && nginx -g 'daemon off;'"]
Wie man sieht, wird die Datei in den html Ordner von nginx geschrieben. Somit ist sie später unter /environment per HTTP erreichbar.
Das Skript zum Schreiben der Environment Variablen gestaltet sich relativ simpel:
#!/usr/bin/env bash
set -o errexit
set -o pipefail
set -o nounset
dir=$(cd -P -- "$(dirname -- "$0")" && pwd -P)
main() {
local file_path="${1:-}"
if [ -z "${file_path}" ]; then
(>&2 echo "USAGE: ${0} <file path>")
exit 1
fi
env | grep '^VUE_APP_' > "${file_path}"
}
main "$@"%
In unserer vue App fragen wir dann die /environment Datei per axios ab, parsen sie und stellen sie als Environment zur Verfügung. Als Fallback wird der Wert welcher im Environment steht zurückgegeben. Dies kann beispielsweise für lokale Tests hilfreich sein.
import axios from 'axios'
import { parse } from 'envfile'
class Environment {
loadedEnvironment: { [key: string]: string } = {}
async load(): Promise<void> {
const response = await axios.get('/environment')
this.loadedEnvironment = parse(response.data)
}
get(key: string): string | undefined {
return this.loadedEnvironment[key] ?? process.env[key]
}
}
const environment = new Environment()
Somit haben wir einen einfachen und zugegebenermassen etwas “gefrickelten” Weg um ohne separaten Build je nach Umgebung verschiedene Konfigurationen zu haben.
VORSICHT: Diese Lösung sollte nicht für Secrets verwendet werden, da wir die Umgebungsvariablen unter /environment öffentlich zugänglich machen.
Für unseren Use Case geht das, da Firebase API Keys nicht wie Secrets behandelt werden müssen (siehe firebase.google.com/docs/projects/api-keys). Für schützenswerte Daten braucht es weitergehende Lösungen wie beispielsweise das Laden der Konfiguration für ein Environment vom Backend.