1. Einführung

Das Ziel dieses Tutorials ist es, mithilfe von @ConfigMapping auf Konfigurationen aus verschiedensten Config-Sources zuzugreifen. Besonders ersichtlich wird im Tutorial, welche Config-Source den Vorrang hat, wenn Konfigurationen aus mehreren Sources gelesen werden.

1.1. Voraussetzungen

Folgendes wird benötigt:

  • Entwicklungsumgebung (IntelliJ IDEA)

  • Git-Repository (GitHub)

  • git-secret

  • gpg (GNU Privacy Guard)

  • Docker (optional)

  • Angular (optional)

2. Erstellung des Projekts

2.1. Quarkus-Projekt erstellen

2.1.1. IntelliJ IDEA

create project 1
create project 2

2.1.2. Maven

mvn io.quarkus.platform:quarkus-maven-plugin:3.16.3:create \
    -DprojectGroupId=at.htl.configdemo \
    -DprojectArtifactId=config-demo
cd config-demo
idea .

2.1.3. Quarkus CLI

quarkus create app at.htl.configdemo:config-demo
cd config-demo
idea .

2.2. Dependencies hinzufügen

pom.xml
<dependencies>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy</artifactId> (1)
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-jackson</artifactId> (2)
    </dependency>
</dependencies>
1 Ermöglicht die Erstellung von RESTful Web Services
2 Ermöglicht Serialisierung und Deserialisierung von JSON zu Java-Objekten

2.3. Projektstruktur

Bevor die Implementierung beginnt, sollten alle Verzeichnisse und Klassen, die im weiteren Verlauf zum Einsatz kommen, erstellt werden. Die Projektstruktur soll schlussendlich wie folgt aussehen:

fT313S8m30NGUtS1hr2f6x21ILqmIkt8Tf2wFMtQ0oSau1Ob K VxZtGOk9Cta5IcVXs6cbMS4im8miyy8c00VV8nuVsVjKg8ze20ySojFiBkLPBTDuYNsdRCKpp6 O7ilQ8dn6IM3l0K3rqtfLg6Apo2RDZT0Si9NFQ0yMqa0MJTpt9sBRy0W00
Figure 1. Projektstruktur

3. Konfiguration hinzufügen

In diesem Schritt fügen wir in die application.properties Konfigurationen mit dem prefix custom hinzu.
config sources application properties
Figure 2. application.properties
application.properties mit Konfigurationswerten befüllen
7SpH3SCm20N0  xIie9NGHXNI3QW1za KGQu6zMCNhQrFYJXo xVgIUe87JOdA0L5qIBE7ERS5luBn6fQ7lOK9zlS0C0
application.properties
custom.city=Linz

# list of cities (array)
custom.cities[0]=Vienna (1)
custom.cities[1]=London (1)
custom.cities[2]=Madrid (1)

# nested configuration
custom.other.city=Paris
custom.other.cities[0]=Berlin (1)
custom.other.cities[1]=Chicago (1)
custom.other.cities[2]=Boston (1)
1 Um eine Liste von Konfigurationen zu erstellen, wird der Index in eckigen Klammern angegeben.

4. Zugriff auf die Konfigurationen

Die Konfigurationen wurden im vorherigen Schritt gesetzt, nun möchte man auf diese im Code zugreifen können. Hierbei gibt es zwei Möglichkeiten, welche in den folgenden 2 Abschnitten erklärt werden.

4.1. ConfigProperty

Diese Option ist nur zur Demonstration gedacht und wird nicht in das Tutorial eingebaut.

Eine Möglichkeit, um auf die Konfigurationswerte zuzugreifen, bietet die Annotation @ConfigProperty.

ConfigProperty
    ...

    @ConfigProperty(name = "custom.city") (1)
    String city;

    @ConfigProperty(name = "custom.cities") (1)
    List<String> cities;

    @ConfigProperty(name = "custom.other.city") (1)
    String otherCity;

    @ConfigProperty(name = "custom.other.cities") (1)
    List<String> otherCities;

    ...
1 Die Annotation @ConfigProperty ermöglicht es, auf die Konfigurationen zuzugreifen und den Wert in die entsprechende Variable zu speichern. Der name-Parameter gibt an, auf welche Konfiguration zugegriffen werden soll.

4.1.1. Vorteile

  • Direkter Zugriff einzelner Werte, ohne zusätzliche Klassen oder Interfaces

4.1.2. Nachteile

  • Bei vielen Konfigurationen wird der Code unübersichtlich

  • Zusammenhängende Konfigurationswerte (wie z. B. greeting.message und greeting.language) kann man nicht zusammenfassen

4.2. ConfigMapping

Diese Option ist in das Tutorial einzubauen.

Eine andere Möglichkeit, um auf die Konfigurationswerte zuzugreifen, bietet die Annotation @ConfigMapping.

ConfigMapping
entity/CustomConfiguration.java
package at.htl.configdemo.entity;

import io.smallrye.config.ConfigMapping;

import java.util.List;

@ConfigMapping(prefix = "custom") (1)
public interface CustomConfiguration {

    String city(); (2)
    List<String> cities(); (2)
    Other other(); (2)

    interface Other { (3)
        String city(); (2)
        List<String> cities(); (2)
    }
}
1 Die Annotation @ConfigMapping wird verwendet, um Konfigurationseigenschaften, welche mit einem bestimmten prefix beginnen, zu extrahieren.
2 Die Methoden in einem @ConfigMapping-annotierten Interface definieren die Konfigurationseigenschaften und deren Datentypen. Der Name der Methode sollte dem Namen der Konfigurationseigenschaft (nach dem prefix) entsprechen.
3 Nested Configurations können durch das Definieren eines weiteren Interfaces innerhalb des @ConfigMapping-annotierten Interface erstellt werden.

4.2.1. Vorteile

  • Verwandte Konfigurationswerte können in einem Interface zusammengefasst werden

  • Besonders bei großen Projekten sorgt @ConfigMapping für saubere und wartbare Konfigurationsmodelle

4.3. Fazit

Der Einsatz der oben vorgestellten Optionen sollte auf Basis der Anforderungen des Projekts entschieden werden.

Die Verwendung von @ConfigProperty eignet sich besonders, wenn nur wenige Konfigurationswerte benötigt werden, die unabhängig voneinander sind. Dieser Ansatz ist einfach und ermöglicht einen direkten Zugriff auf einzelne Konfigurationswerte.

@ConfigMapping hingegen bietet eine strukturierte Lösung für den Umgang mit Konfigurationen. Es ermöglicht die Definition von Interfaces, die verwandte Konfigurationswerte logisch gruppieren und somit eine klare Trennung schaffen. Besonders in großen Projekten sorgt dieser Ansatz für bessere Wartbarkeit und Lesbarkeit des Codes.

Im Tutorial wird @ConfigMapping verwendet, da es den Anforderungen einer komplexen sowie verschachtelten Konfiguration am besten gerecht wird.

5. Default-Values definieren

Beim bisherigen Stand des Projekts werden die Konfigurationen aus den application.properties gelesen. Sollte jedoch ein Wert nicht gesetzt sein, der jedoch im Interface deklariert ist, fliegt eine NoSuchElementException.

no such element exception
Figure 3. NoSuchElementException

5.1. ConfigMapping

Um eine NoSuchElementException zu vermeiden, muss man Default-Values im Interface definieren.

ConfigMapping mit Default-Values
entity/CustomConfiguration.java
package at.htl.configdemo.entity;

import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;

import java.util.List;
import java.util.Optional;

@ConfigMapping(prefix = "custom")
public interface CustomConfiguration {

    @WithDefault("Linz") (1)
    String city();

    Optional<List<String>> cities(); (2)

    Other other();

    interface Other {

        @WithDefault("Paris") (1)
        String city();

        Optional<List<String>> cities(); (2)
    }
}
1 @WithDefault injected einen Standardwert, falls keine Konfiguration in jeglichen Config-Sources gesetzt wurde.
2 Bei Listen funktioniert die @WithDefault-Annotation nicht. Hierbei kommt Optional zum Einsatz, welches ein empty-Optional injected, falls die Konfiguration nicht gesetzt ist.

5.2. ConfigProperty

Diese Option ist nur zur Demonstration gedacht und wird nicht in das Tutorial eingebunden.
ConfigProperty mit Default-Values
@ConfigProperty(name = "greeting.suffix", defaultValue="!") (1)
String suffix;

@ConfigProperty(name = "greeting.name")
Optional<String> name; (2)
1 Die defaultValue wird injected, falls die Konfiguration nicht gesetzt ist.
2 Ein empty-Optional wird injected, falls die Konfiguration nicht gesetzt ist.

6. Endpoints erstellen

Um die Konfigurationswerte auszugeben, erstellen wir REST-Endpoints. Diese geben die Konfigurationen als JSON-Objekte zurück. Um jedoch auf die Konfigurationen zugreifen zu können, benötigen wir eine Service-Klasse.

Service-Klasse
control/CityConfig.java
package at.htl.configdemo.control;

import at.htl.configdemo.entity.CustomConfiguration;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import java.util.List;
import java.util.Optional;

@ApplicationScoped (1)
public class CityConfig {

    @Inject
    CustomConfiguration configuration; (2)

    public String getCity(){
        return configuration.city(); (3)
    }

    public Optional<List<String>> getCities(){
        return configuration.cities(); (3)
    }

    public CustomConfiguration.Other getOther(){
        return configuration.other(); (3)
    }
}
1 @ApplicationScoped sorgt dafür, dass alle Klassen, die diese Klasse per @Inject nutzen, auf dieselbe Instanz zugreifen.
2 @Inject sorgt dafür, dass eine Instanz von CustomConfiguration zur Verfügung gestellt wird.
3 Diese GETTER liefern die Konfigurationswerte zurück und sorgen schlussendlich dafür, dass man im Programm Zugriff auf die Konfigurationswerte hat.

Bevor die Endpoints implementiert werden, wird eine RestConfig-Klasse erstellt, welche die Basis-URL für alle Endpoints des Servers festlegt.

RestConfig
control/RestConfig.java
package at.htl.configdemo.control;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

@ApplicationPath("api") (1)
public class RestConfig extends Application { (2)
}
1 Die @ApplicationPath-Annotation legt die Basis-URL für alle Endpoints fest. In diesem Fall bedeutet es, dass alle Endpoints unter dem Pfad /api erreichbar sind.
2 Die Klasse RestConfig erweitert Application, was sie zur REST-Konfigurationsklasse macht. Durch die Vererbung von Application kann diese Klasse verwendet werden, um globales Verhalten oder zusätzliche Konfigurationen für Endpoints zu definieren.

Nun wird die Resource-Klasse implementiert, welche die Konfigurationen als JSON-Objekte per Endpoint zurückgibt.

Resource-Klasse
boundary/CityConfigResource.java
package at.htl.configdemo.boundary;

import at.htl.configdemo.control.CityConfig;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

import java.util.List;
import java.util.Optional;

@Path("config")
@Produces(MediaType.APPLICATION_JSON)
public class CityConfigResource {

    @Inject
    CityConfig cityConfig; (1)

    @GET
    @Path("city")
    public String getCity(){
        return cityConfig.getCity(); (2)
    }

    @GET
    @Path("cities")
    public Optional<List<String>> getCities(){
        return cityConfig.getCities(); (2)
    }

    @GET
    @Path("other/city")
    public String getOtherCity(){
        return cityConfig.getOther().city(); (2)
    }

    @GET
    @Path("other/cities")
    public Optional<List<String>> getOtherCities(){
        return cityConfig.getOther().cities(); (2)
    }
}
1 @Inject sorgt dafür, dass eine Instanz der CityConfig-Klasse zur Verfügung gestellt wird und somit der Zugriff auf die Konfigurationen ermöglicht wird.
2 Die einzelnen Endpoints liefern die Konfigurationswerte zurück.

7. Testen des Projekts

Der Zugriff auf die Konfigurationen und die Endpoints zur Rückgabe der Werte sind implementiert. Um diese zu testen, benötigt man ein requests.http-File.

requests.http
http-requests/requests.http
@baseUrl = http://localhost:8080/api/config

### GET city
GET {{baseUrl}}/city
Accept: application/json

### GET cities
GET {{baseUrl}}/cities
Accept: application/json

### Get other city
GET {{baseUrl}}/other/city
Accept: application/json

### Get other cities
GET {{baseUrl}}/other/cities
Accept: application/json
Nun muss nur noch der Quarkus-Server gestartet und die Requests im requests.http-File ausgeführt werden.
Server starten (Dev-Mode)
./mvnw quarkus:dev clean

7.1. Requests ausführen

Nachdem der Server gestartet wurde, können die Requests im requests.http-File ausgeführt werden. Die Responses sollten hierbei wie folgt aussehen:

GET city
response get city
GET cities
response get cities
GET other city
response get other city
GET other cities
response get other cities

8. Konfigurationen im YAML-Format

Bei komplexeren bzw. hierarchischen Strukturen empfiehlt es sich, die Konfigurationen im YAML-Format zu definieren. Da dies die Lesbarkeit der Konfigurationen erhöht.

8.1. application.yaml erstellen

application.properties wird nun von application.yaml abgelöst.

Die Konfigurationen bzw. die Konfigurationswerte bleiben gleich, nur das Format ändert sich.
src/main/resources/application.yaml
custom:
  city: Linz
  cities:
  - Vienna
  - London
  - Madrid
  other:
    city: Paris
    cities:
    - Berlin
    - Chicago
    - Boston

Damit die Konfigurationen aus dem application.yaml-File gelesen werden können, muss die pom.xml um folgende Dependency erweitert werden:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-config-yaml</artifactId>
</dependency>
Nun sollten beim Testen des Projekts die gleichen Responses wie zuvor zurückgeliefert werden. Siehe hier.

9. Config-Sources

Per Default liest Quarkus Konfigurationseigenschaften aus mehreren Quellen in absteigender Reihenfolge:

In diesem Tutorial wird nicht auf die META-INF/microprofile-config.properties eingegangen. Für weitere Informationen bezüglich MicroProfile-Config siehe hier.
config sources
In der Abbildung sind die verschiedenen Config-Sources dargestellt, aus denen die Konfigurationen gelesen werden können. Je weiter links sich die Config-Source in der Grafik befindet, desto höher ist die Priorität, sprich Konfigurationswerte von weiter links gelegenen Config-Sources überschreiben die Werte von weiter rechts gelegenen Config-Sources.

Als nächsten Schritt werden die Konfigurationen aus verschiedensten Config-Sources gelesen und im Projekt verwendet. Dies soll als Demonstration der Priorität der einzelnen Config-Sources dienen.

9.1. config/application.properties

config sources config application properties

9.1.1. Erstellen

Zuerst muss ein Verzeichnis config im Projekt-Root erstellt werden. In dieses Verzeichnis kommt schlussendlich entweder ein application.properties- oder application.yaml-File.

SoWkIImgISlCIItcqa EpqlBJDTDIStDroykIjRNqzFJHn65ilKJ2mfoCfCJIpBpy GAYl8BKejACe46EAJcfO2D0W00

In das application.properties- oder application.yaml-File kommt folgende Konfiguration:

application.properties
custom.city=Salzburg
application.yaml
custom:
  city: Salzburg

9.1.2. Testen

Beim Testen des betroffenen Endpoints sollte nun folgende Response zurückgeliefert werden:

Get city
response config application properties get city

9.2. .env-Files

config sources env file

9.2.1. Erstellen

Das .env-File wird im Projekt-Root erstellt.

SoWkIImgISlCIItcqa EpqlBJDTDIStDroykIjRNqzFJHnNLIyqhut98pKi1wm00

In das .env-File kommt folgende Konfiguration:

.env
CUSTOM_CITY=Moscow

9.2.2. Testen

Beim Testen des betroffenen Endpoints sollte nun folgende Response zurückgeliefert werden:

Get city
response env file get city
Das .env-File ist in der Regel bei richtigen Projekten nicht in das Repository zu pushen, da es oft sensible Daten wie Passwörter oder API-Keys enthält. Somit sollte es immer im .gitignore enthalten sein. Wie man das .env-File trotzdem mit weiteren, berechtigten Entwicklern teilen kann, ist hier dokumentiert.

9.3. Umgebungsvariablen

Umgebungsvariablen können der Anwendung beim Start durch das Schlüsselwort export übergeben werden.

config sources environment variables

9.3.1. Erstellen

Hierbei benötigt man ein uber-jar, welches alle Dependencies der Anwendung, den Code der Anwendung und eine ausführbare Main-Klasse (Entry-Point) enthält. Um dieses zu erstellen, muss der folgende Abschnitt Schritt für Schritt durchgegangen werden.

Zuerst application.properties um folgende Zeile erweitern:

quarkus.package.jar.type=uber-jar

Oder application.yaml um folgende Zeile erweitern:

quarkus:
  package:
    jar:
      type: uber-jar
Je nachdem, ob mit application.properties oder application.yaml gearbeitet wird, muss die entsprechende Datei erweitert werden.
Package erstellen
./mvnw clean package

Nun sollte sich im target-Verzeichnis ein *-runner.jar-File befinden.

SoWkIImgISlCIItcqa EpqlBJDTDIStDrmSAfwUMw6lf5fNcvQKMwMKb5d4vfEQbW9K20000

Nun kann das jar-File mit folgendem Befehl gestartet und mit einer Umgebungsvariable versehen werden:

export CUSTOM_CITY=Munich ; java -jar target/*-runner.jar

9.3.2. Testen

Beim Testen des betroffenen Endpoints sollte nun folgende Response zurückgeliefert werden:

Get city
response env variables get city

9.4. System Properties

System Properties können der Anwendung beim Start durch das Flag -D übergeben werden.

config sources system properties

9.4.1. Erstellen

Auch bei den System Properties benötigt man ein uber-jar (falls man den Server nicht im Dev-Mode starten möchte), genau wie bei den Umgebungsvariablen. Siehe hier.
Im Dev-Mode
./mvnw quarkus:dev -Dcustom.city=Amsterdam
Als uber-jar
java -Dcustom.city=Amsterdam -jar target/config-demo-1.0-SNAPSHOT-runner.jar

9.4.2. Testen

Beim Testen des betroffenen Endpoints sollte nun folgende Response zurückgeliefert werden:

GET city
response system properties get city

10. Profiles

Profiles ermöglichen es, verschiedene Konfigurationen für verschiedene Umgebungen zu setzen.

10.1. Default Profiles

  • dev

    • Ist im Entwicklungsmodus aktiv (quarkus:dev)

  • test

    • Ist beim Ausführen von Tests aktiv

  • prod

    • Ist aktiv, wenn man nicht im Dev- oder Test-Modus ist

Außerdem kann man noch Custom Profiles erstellen. Genaueres dazu ist hier zu finden.

10.2. Wie setzt man Profiles?

10.2.1. *.properties-File

In *.properties-Files werden Profiles wie folgt gesetzt: %{profile-name}.config.name=value

Profile setzen
%{profile}.custom.city=Vienna
Anwendung in bestimmter Umgebung starten
./mvnw quarkus:<profile> clean
Profile ist hierbei der Platzhalter für die jeweilige Umgebung (dev, test, prod, …​).

Je nachdem in welcher Umgebung der Server gestartet wird, wird die entsprechende Konfiguration aus dem *.properties-File verwendet.

10.2.2. .env-File

In .env-Files werden Profiles wie folgt gesetzt: _{PROFILE}_CONFIG_KEY=value

Profile setzen
_{PROFILE}_CUSTOM_CITY=Vienna

10.3. Profile-aware Files

Um nicht alle Konfigurationen aus verschiedensten Umgebungen in ein *.properties-File zu stopfen, können Konfigurationen für eine bestimmte Umgebung in eine eigene Datei ausgelagert werden (application-{profile}.properties). Für weitere Informationen siehe hier.

11. Git-Secrets

11.1. Was ist git-secret?

git-secret ist ein Tool, das entwickelt wurde, um sensible Daten wie Passwörter, API-Schlüssel oder Konfigurationsdateien sicher in Git-Repositories zu speichern. Es nutzt GPG-Verschlüsselung (GNU Privacy Guard), um diese Dateien bzw. Daten vor unbefugtem Zugriff zu schützen. Nur autorisierte Benutzer, die über den entsprechenden GPG-Schlüssel verfügen, können die verschlüsselten Inhalte entschlüsseln.

gpg asymmetric encryption
Figure 4. Asymmetrische Verschlüsselung mit GPG

11.2. Welches Problem wird gelöst?

git-secret löst das Problem, dass sensible Daten wie Passwörter, Token oder andere geheime Informationen unbeabsichtigt in Git-Repositories landen können, wo sie für jeden sichtbar sind, der Zugriff auf das Repository hat. Dies ist besonders in öffentlichen Repositories gefährlich.

Durch die Verschlüsselung der sensiblen Dateien stellt git-secret sicher, dass nur autorisierte Personen auf diese Informationen zugreifen können. Es bietet:

  • Sicherheit: Schutz sensibler Daten durch Verschlüsselung.

  • Versionierung: Möglichkeit, Änderungen an sensiblen Daten nachzuverfolgen.

11.3. Wie wendet man git-secret an?

Das Tutorial muss sich in einem Git-Repository befinden, um git-secret verwenden zu können.

11.3.1. GPG-Schlüsselpaar erstellen

Zuerst muss ein GPG-Schlüsselpaar (privater und öffentlicher Schlüssel) erstellt werden.

gpg --full-generate-key
Ablauf bei der Erstellung eines GPG-Schlüsselpaares
creating gpg key pair
Die Optionen, welche mit default versehen sind, werden automatisch ausgewählt, wenn man ENTER drückt.

11.3.2. git-secret initialisieren

Nun wird git-secret im Repository initialisiert. Dadurch wird ein .gitsecret-Verzeichnis erstellt, in dem die GPG-Schlüssel autorisierter Benutzer und andere Metadaten gespeichert werden.

git secret init

11.3.3. Benutzer hinzufügen

Nun fügt man sich selbst mit dem GPG-Schlüssel als autorisierten Benutzer hinzu.

git secret tell you@domain.com (1)
1 you@domain.com wird mit der eigenen E-Mail-Adresse, die beim Erstellen des GPG-Schlüsselpaars verwendet wurde, ersetzt.

11.3.4. Datei hinzufügen

Nun wird das .env-File, welches im Tutorial erstellt wurde, hinzugefügt, um es im weiteren Verlauf verschlüsseln zu können.

git secret add config-demo/.env

11.3.5. Weitere Benutzer hinzufügen

Um nun anderen Benutzern Zugriff auf die verschlüsselte Datei zu gewähren, müssen folgende Schritte durchgeführt werden:

Zuerst muss der öffentliche GPG-Schlüssel des Benutzers hinzugefügt werden.

gpg --import key.txt

Schlussendlich wird noch der Benutzer hinzugefügt.

git secret tell user@domain.com

11.3.6. Datei verschlüsseln

Nun wird das .env-File verschlüsselt, sodass nur autorisierte Benutzer darauf zugreifen und die Datei lesen können.

git secret hide

Nachdem der Befehl ausgeführt wurde, wird das .env-File verschlüsselt und das .env.secret-File erstellt, welches im Repository sichtbar sein darf, da es, dass verschlüsselte .env-File darstellt.

Dies war nun die Vorgehensweise für das Hinzufügen und Verschlüsseln einer Datei, um diese Datei schlussendlich sicher im Repository zu speichern.

11.4. Wie entschlüsselt man Dateien mit git-secret?

Nun wird demonstriert wie ein weiterer Benutzer, welcher ebenfalls im gleichen Git-Repository arbeitet, auf die verschlüsselten Dateien zugreifen kann. Sprich es wird dargestellt, wie man von der gegenüberliegenden Seite zum entschlüsselten .env-File kommt.

11.4.1. Datei entschlüsseln

Autorisierte Benutzer können auf die verschlüsselten Dateien zugreifen, indem sie diese mit folgendem Befehl entschlüsseln.

git secret reveal

Nachdem der Befehl ausgeführt wurde, wird das .env.secret-File entschlüsselt und das .env-File kann nun gelesen und bearbeitet werden.

11.5. Erklärung der Befehle

Nun folgt eine detaillierte Erklärung der wichtigsten Befehle in Zusammenhang mit git-secret.

11.5.1. git secret tell

git secret tell fügt den öffentlichen GPG-Schlüssel des angegebenen Benutzers zur Konfiguration von git-secret hinzu. Dadurch wird dieser Benutzer für die Entschlüsselung von verschlüsselten Dateien autorisiert.

Es wird sicherstellt, dass der angegebene Benutzer (mit seinem öffentlichen GPG-Schlüssel) in der Lage ist, die verschlüsselten Dateien zu entschlüsseln, die mit git secret hide verschlüsselt wurden.

Beispiel

Angenommen, ein Benutzer hat einen öffentlichen GPG-Schlüssel mit einer hinterlegten E-Mail: user@example.com. Mit git secret tell user@example.com wird dieser öffentliche Schlüssel in der .gitsecret-Konfigurationsdatei gespeichert.

Wenn dieser Benutzer nun git secret reveal ausführt, wird der private Schlüssel dieses Benutzers benötigt, um die Datei zu entschlüsseln.

11.5.2. git secret hide

Mit git secret hide wird die Datei mit GPG verschlüsselt. Alle öffentlichen GPG-Schlüssel, die in der git secret tell-Konfiguration gespeichert sind, werden verwendet, um die Datei zu verschlüsseln. Dadurch wird die Datei für die entsprechenden Benutzer zugänglich, aber für andere Benutzer bleibt sie verschlüsselt.

Der Inhalt der Datei wird mit dem öffentlichen GPG-Schlüssel der autorisierten Benutzer verschlüsselt. Die verschlüsselte Datei wird dann im Git-Repository gespeichert.

11.5.3. git secret reveal

Bei git secret reveal wird GPG verwendet, um die verschlüsselten Dateien zu entschlüsseln. Um die Datei zu entschlüsseln, benötigt der Benutzer den privaten GPG-Schlüssel, der dem öffentlichen Schlüssel zugeordnet ist, mit dem die Datei verschlüsselt wurde. Das bedeutet, der private Schlüssel des Benutzers, der für die Entschlüsselung durch git secret tell autorisiert wurde, muss auf dem eigenen Rechner vorhanden sein.

12. Docker-Secrets

Der folgende Abschnitt (Docker-Secrets) ist nicht Teil des Tutorials, sondern dient nur als Zusatzinformation.

Mit Docker-Secrets kann man sensible Daten wie Passwörter oder API-Schlüssel sicher in Docker verwalten.

Vorteile:

  • Sicherheit (verschlüsselte Speicherung und Übertragung)

  • Zentralisierte Verwaltung

  • Trennung von Code und sensiblen Daten

12.1. Verwaltung von Secrets in Docker

Docker Secrets ermöglichen die Speicherung, Verteilung und Nutzung sensibler Daten in einem Docker-Swarm-Cluster. Die Daten werden verschlüsselt gespeichert und nur an Container weitergegeben, die explizit Zugriff benötigen.

12.1.1. Docker Swarm

Docker Secrets sind nur im Swarm-Modus verfügbar.

Docker Swarm ist ein Container-Orchestrierungsdienst von Docker, der es ermöglicht, mehrere Docker-Hosts (Rechner auf denen Docker läuft) zu einem Cluster zusammenzuschließen.

Alternativen wären Kubernetes oder Docker Compose. Der Unterschied zwischen Docker Swarm und Docker Compose ist, dass Docker Compose nur Container auf einem Docker-Host verwalten kann, während Docker Swarm Container von mehreren Hosts zu einem Cluster verbindet und verwaltet.

3 Komponenten des Docker Swarm Clusters:

  • Manager Nodes

    • Verwalten das Cluster und delegieren Aufgaben an Worker Nodes

  • Worker Nodes

    • Führen die Container aus

  • State Store (vergleichbar mit etcd in Kubernetes)

    • Speichert den Zustand des Clusters

      • Status der Nodes

      • Secrets und Konfigurationen

docker swarm nodes
Figure 5. Docker Swarm Nodes

Im Docker Swarm Mode sorgt TLS (Transport Layer Security) für eine sichere Kommunikation zwischen den Manager und Worker-Nodes sowie zwischen den Manager-Nodes untereinander. TLS verschlüsselt die Datenübertragung und stellt sicher, dass die Identität der Knoten überprüft wird, bevor sie miteinander kommunizieren können.

12.2. Docker Secrets erstellen

Zuerst muss ein Swarm-Cluster initialisiert werden, um Docker Secrets zu verwenden.

Docker Swarm initialisieren
docker swarm init

Nun kann ein Docker Secret erstellt werden. Als Beispiel wird ein .env-File als Docker Secret verwendet.

Docker Secret erstellen
docker secret create my_secret ./.env

Nun wird ein Docker Service erstellt, der Zugriff zum Docker Secret erhält.

Docker Services stellen beispielsweise eine Web-App oder einen Datenbank-Server in einem Docker Swarm Cluster dar. Ein Service legt unter anderem fest, wie viele Replicas erstellt werden sollen und welche Secrets verwendet werden.
Docker Service mit Zugriff auf Docker Secret erstellen
docker service create \
  --name my_service \
  --secret my_secret \
  eclipse-temurin:21-jre (1)
1 eclipse-temurin:21-jre ist ein Beispiel für ein Docker(-Base)-Image, das als Service erstellt wird. Es kann durch jedes andere Docker-Image ersetzt werden.

Um den Zugriff auf das Docker Secret zu entfernen, muss der Service aktualisiert werden.

Zugriff auf Docker Secret entfernen
docker service update --secret-rm my_secret my_service

Das Docker Secret kann mit folgendem Befehl auch gelöscht werden.

Docker Secret löschen
docker secret rm my_secret

13. Internationalisierung

Der folgende Abschnitt (Internationalisierung) ist nicht Teil des Tutorials, sondern dient nur als Zusatzinformation.

i18n steht für "Internationalization" und stellt die Anpassung von Software dar, um sie in verschiedenen Sprachen und Regionen verfügbar zu machen. Das Ziel ist es, nicht nur den Text, sondern auch Datum- und Zahlenformate in mehreren Sprachen bereitzustellen.

Internationalisierung steigert die Benutzerfreundlichkeit für globale Zielgruppen.

Die folgende Erklärung bezieht sich speziell auf die Internationalisierung in Angular.

13.1. Übersetzungsdateien

Übersetzungsdateien sind zentrale Bestandteile der Internationalisierung (i18n), die verwendet werden, um Textinhalte einer Anwendung in verschiedene Sprachen zu übersetzen. Sie enthalten Key-Value-Paare, bei denen der Key der Platzhalter für den Text ist und die Value die übersetzte Version des Textes in der jeweiligen Sprache.

Bei Webanwendungen werden Übersetzungsdateien häufig im JSON-Format gespeichert, aber auch andere Dateiformate wie YAML können verwendet werden. Diese Dateien sind typischerweise nach der Sprache strukturiert (z. B. en.json für Englisch, de.json für Deutsch).

de.json
{
  "welcome": "Willkommen",
  "user": "Hallo, {{name}}!", (1)
  "header": {
    "home": "Startseite",
    "about": "Über uns",
    "contact": "Kontakt"
  }
}
1 Platzhalter können in den Übersetzungsdateien verwendet werden, um dynamische Inhalte zu unterstützen. In diesem Fall ersetzt der Platzhalter {{name}} den Benutzernamen.
en.json
{
  "welcome": "Welcome",
  "user": "Hello, {{name}}!",
  "header": {
    "home": "Home",
    "about": "About Us",
    "contact": "Contact"
  }
}

Die Übersetzungsdateien werden im src/assets/i18n-Verzeichnis gespeichert.

XSqn2e0m34RXtRkOuEROguGsIaj9I vuVq5dSNuVl8GGZwDsnS9YvzN5r cZQGw6PvOvcOZuem8QuBhjnaMfOTW L7jXKYlF GO0
Figure 6. Projektstruktur (Angular)

Im angular.json-File muss nun das assets-Directory definiert werden, um auf die Übersetzungsdateien zugreifen zu können.

angular.json
{
  ...
  "projects": {
    "i18n-demo": {
      ...
      "architect": {
        "build": {
          ...
          "options": {
            ...
            "assets": [
              "src/assets", (1)
              {
                "glob": "**/*",
                "input": "public"
              }
            ],
            ...
          },
          ...
        }
      }
    }
  },
  ...
}
1 In diesem Fall wird definiert, dass der src/assets-Folder beim Bauen des Projekts in das Build-Verzeichnis kopiert wird.

13.2. Implementierung

13.2.1. Bibliotheken installieren

Zuerst muss die @ngx-translate/core- und die @ngx-translate/http-loader-Bibliothek installiert werden, welche für folgendes benötigt werden:

  • @ngx-translate/core

    • Kernkomponente für die Übersetzungslogik

    • Funktionen:

      • Übersetzung verwalten

      • Sprache wechseln

      • Parameterisierte Übersetzungen

  • @ngx-translate/http-loader

    • bietet Möglichkeit, Übersetzungsdateien aus einem externen Verzeichnis wie src/assets/i18n/ zu laden

    • Ergänzt die @ngx-translate/core-Bibliothek

Bibliotheken installieren
npm install @ngx-translate/core @ngx-translate/http-loader

13.2.2. AppModule konfigurieren

app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {TranslateHttpLoader} from '@ngx-translate/http-loader';
import {TranslateLoader, TranslateModule} from '@ngx-translate/core';

export function HttpLoaderFactory(http: HttpClient) {
  return new TranslateHttpLoader(http); (1)
}

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    HttpClientModule,
    TranslateModule.forRoot({ (2)
      loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [HttpClient]
      }
    })
  ],
  providers: [HttpClient],
  bootstrap: [AppComponent]
})
export class AppModule { }
1 Die Factory-Funktion HttpLoaderFactory wird verwendet, um eine Instanz des TranslateHttpLoader zu erstellen, die schlussendlich verwendet wird, um Übersetzungsdateien zu laden. Hierbei ist wichtig, dass sich die Übersetzungsdateien im src/assets/i18n/-Verzeichnis befinden. Ansonsten muss der Pfad in der Factory-Funktion angepasst werden.
2 Dieser Code konfiguriert das @ngx-translate-Modul, indem ein Loader definiert wird, welcher die Übersetzungsdateien lädt. Der TranslateLoader wird durch die Factory-Funktion HttpLoaderFactory bereitgestellt, die den TranslateHttpLoader verwendet, um die Dateien über HTTP zu laden. Der TranslateHttpLoader verwendet schlussendlich den HttpClient, um die HTTP-Anfragen auszuführen bzw. um die Übersetzungsdateien zu laden.

13.2.3. Konfigurationswerte in Komponenten verwenden

app.component.ts
import { Component } from '@angular/core';
import {TranslateService} from '@ngx-translate/core';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  standalone: false,
  styleUrl: './app.component.css'
})
export class AppComponent {
  username = 'John Doe';

  constructor(private translate: TranslateService) { (1)
    translate.setDefaultLang('de');
  }

  switchLanguage(lang: string) {
    this.translate.use(lang); (2)
  }
}
1 Der Konstruktor injiziert den TranslateService, der verwendet wird, um die Sprache der Anwendung zu ändern und um auf die Werte der Konfigurationen zuzugreifen. Zusätzlich wird die Default-Sprache auf Deutsch gesetzt.
2 Die Methode switchLanguage wird verwendet, um die Sprache der Anwendung zu ändern. Hierbei wird die use-Methode des TranslateService verwendet, um die Sprache auf die gewünschte Sprache zu setzen.
app.component.html
<div>
  <h1>{{ 'welcome' | translate }}</h1> (1)

  <!-- Displaying dynamic user greeting with a placeholder -->
  <p>{{ 'user' | translate: { name: username } }}</p> (2)

  <div>
    <nav>
      <ul>
        <li><a href="#">{{ 'header.home' | translate }}</a></li>
        <li><a href="#">{{ 'header.about' | translate }}</a></li>
        <li><a href="#">{{ 'header.contact' | translate }}</a></li>
      </ul>
    </nav>
  </div>

  <button (click)="switchLanguage('de')">Deutsch</button>
  <button (click)="switchLanguage('en')">English</button>
</div>
1 Die translate-Pipe wird verwendet, um die Übersetzungen in der View anzuzeigen. Hierbei wird der Key der Übersetzung als Argument übergeben.
2 Platzhalter können in den Übersetzungsdateien verwendet werden, um dynamische Inhalte zu unterstützen. In diesem Fall wird der Platzhalter {{name}} durch den Benutzernamen ersetzt.

13.3. Resultat

Nachdem die Implementierung abgeschlossen ist, sollte die Anwendung in der Lage sein, die Übersetzungen in verschiedenen Sprachen anzuzeigen. Durch Klicken auf die Buttons "Deutsch" und "English" wird die Sprache der Anwendung entsprechend geändert.