HashiCorp Nomad und Vault mit .NET: ASP.NET Core in einem sicheren Workload
Als wir unseren Beitrag HashiCorp Nomad and Vault: Dynamic Secrets veroeffentlichten, lief die Demo ausschliesslich als Python Flask-Anwendung. Seitdem ist das

Jedes Nomad-Tutorial, das Sie online finden, verwendet den docker-Treiber. Das ist verstaendlich — Container sind portabel, Images buendeln alles, und Docker ist allgegenwaertig. Aber Docker ist nicht immer verfuegbar. Edge-Nodes, Bare-Metal-Server mit minimaler Tooling-Ausstattung, Air-Gapped-Umgebungen oder einfach Teams, die lieber einfache JARs statt Images ausliefern, stossen alle an die gleiche Grenze: Docker ist nicht vorhanden.
Nomad hat eine eingebaute Antwort: den java-Treiber.
Dieser Beitrag erweitert unsere Nomad + Vault + MySQL Demo-Serie, indem der docker-Treiber vollstaendig ersetzt wird. Die gleiche Spring-Boot-Anwendung — mit Vault Transit-Verschluesselung, dynamischen Datenbank-Credentials und Consul Service Discovery — laeuft als ueberwachter JVM-Prozess und wird zur Scheduling-Zeit direkt von GitHub Releases heruntergeladen.
Der java-Treiber ist ein vollwertiger Nomad Task-Treiber. Wenn Nomad einen Task damit plant, laeuft folgender Lebenszyklus ab:
local/-Verzeichnis des Tasks herunter.template-Bloecke.java -jar <jar_path> [jvm_options] als Kindprozess auf.restart-Stanza bei Fehlern neu.Der Nomad-Worker benoetigt ein JDK/JRE in seinem PATH. Nomad verwaltet die JVM-Installation nicht selbst — das uebernimmt Ihre Base-Image- oder Konfigurationsmanagement-Tooling (Ansible, Packer usw.).
In dieser Serie laedt die containerbasierte Variante Container-Images aus einer Registry. Die Java-Variante laedt stattdessen ein Fat-JAR herunter, das in GitHub Releases veroeffentlicht wurde:
1artifact {
2 source = "https://github.com/infralovers/nomad-vault-mysql/releases/download/java-artifact-latest/nomad-vault-mysql-java.jar"
3 destination = "local/"
4}
Der artifact-Stanza von Nomad unterstuetzt HTTP(S), S3, GCS und mehrere weitere Backends. GitHub Releases ist ein praktischer Standard fuer Open-Source-Projekte — keine privaten Registry-Zugangsdaten, kein Image-Layer-Cache und eine stabile JAR-URL ueber den java-artifact-latest-Tag.
Fuer Produktions-Deployments funktioniert das gleiche Pattern mit jedem Artifact-Store: Nexus, Artifactory, S3 oder einem einfachen HTTPS-Dateiserver.
Ein feiner Unterschied zwischen dem Java- und dem Docker-Treiber ist die Port-Zuweisung. Docker-Jobs pinnen den internen Port des Containers in der Regel mit to = 8080. Beim Java-Treiber gibt es keinen Container-Netzwerk-Namespace — der Prozess bindet direkt an einen Port auf dem Host.
Nomad weist einen zufaelligen freien Port zu und stellt ihn ueber die Umgebungsvariable NOMAD_PORT_web bereit:
1network {
2 port "web" {} # kein static = ..., kein to = ...
3}
Die Spring-Boot-Anwendung liest dies beim Start:
1env {
2 APP_CONFIG_PATH = "${NOMAD_TASK_DIR}/config/config.ini"
3 SERVER_PORT = "${NOMAD_PORT_web}"
4}
Spring Boot beruecksichtigt SERVER_PORT von Haus aus. Der Consul Health-Check und die Service-Registrierung verwenden automatisch den gleichen zugewiesenen Port.
Die Java-Anwendung ist eine Spring Boot 3.3.5 + Java 21 MVC-Anwendung, die die Python-Implementierung funktional spiegelt:
customers-Tabelle in MySQLbirth_date, address und salaryconfig.ini (gleiches Format wie Python und .NET)/health-Endpunkt fuer Nomad Service-Checksapp/java/src/main/java/io/infralovers/nomadvaultmysql/
├── NomadVaultMysqlApplication.java # Spring Boot Einstiegspunkt
├── config/
│ └── AppProperties.java # INI → Java Config-Binding
├── controller/
│ └── AppController.java # MVC-Routen
├── model/
│ └── Customer.java # Datenmodell
└── service/
├── VaultService.java # Roher HTTP Vault-Client
└── CustomerRepository.java # JDBC + Vault-Integration
Die Java-App nutzt Spring Vault fuer eine saubere, idiomatische Vault-Integration. VaultService.java initialisiert Spring Vaults VaultTemplate mit Token-Authentifizierung und wrapped es fuer einfache Verwendung in der gesamten Anwendung:
1public void initialize() {
2 if (!config.isEnabled()) { return; }
3
4 token = config.isInjectToken()
5 ? System.getenv().getOrDefault("VAULT_TOKEN", "").strip()
6 : config.getToken().strip();
7
8 if (config.getAddress().isBlank() || token.isBlank()) {
9 log.warn("Vault-Adresse oder Token fehlen.");
10 return;
11 }
12
13 try {
14 VaultEndpoint endpoint = VaultEndpoint.from(URI.create(config.getAddress()));
15 vaultTemplate = new VaultTemplate(endpoint, new TokenAuthentication(token));
16 vaultTransit = vaultTemplate.opsForTransit(config.getKeyPath());
17 log.info("Spring Vault Client initialisiert fuer Mount-Pfad {}", config.getKeyPath());
18 enabled = true;
19 } catch (Exception e) {
20 log.warn("Spring Vault-Initialisierung fehlgeschlagen ({}); Fallback zu Raw HTTP.", e.getMessage());
21 // Fallback zu Raw HTTP, wenn Spring Vault fehlschlaegt
22 }
23}
Das Token wird von Nomad ueber die Umgebungsvariable VAULT_TOKEN injiziert, wenn InjectToken = True gesetzt ist — der gleiche Mechanismus wie bei der Python-Variante.
1public String encrypt(String value) {
2 if (!enabled || value == null || value.isBlank()) return value;
3
4 if (vaultTransit != null) {
5 try {
6 return vaultTransit.encrypt(config.getKeyName(), value);
7 } catch (Exception e) {
8 log.warn("Spring Vault Transit-Verschluesselung fehlgeschlagen ({}); Fallback.", e.getMessage());
9 }
10 }
11
12 // Fallback zu Raw HTTP, wenn Spring Vault nicht verfuegbar ist
13 // ... Raw HTTP-Implementierung ...
14}
15
16public String decrypt(String value) {
17 if (!enabled || value == null || !value.startsWith("vault:v")) return value;
18
19 if (vaultTransit != null) {
20 try {
21 return vaultTransit.decrypt(config.getKeyName(), value);
22 } catch (Exception e) {
23 log.warn("Spring Vault Transit-Entschluesselung fehlgeschlagen ({}); Fallback.", e.getMessage());
24 }
25 }
26
27 // Fallback zu Raw HTTP, wenn Spring Vault nicht verfuegbar ist
28 // ... Raw HTTP-Implementierung ...
29}
Die Spring Vault-API ist bemerkenswert sauber: vaultTransit.encrypt() und vaultTransit.decrypt() handhaben Base64-Kodierung, HTTP-Aufrufe und Response-Parsing. Falls Spring Vault auf einen Fehler trifft, faellt die Anwendung elegant auf Raw HTTP zurueck — dies gewaehrleistet Robustheit auch wenn Vault-Client-Bibliotheks-Versionen Probleme verursachen.
Spring Vault verarbeitet auch Datenbank-Credentials nahtlos:
1public Optional<DatabaseCredentials> readDatabaseCredentials(String path) {
2 if (!enabled || path.isBlank()) return Optional.empty();
3
4 try {
5 if (vaultDbOps != null) {
6 // Spring Vault Database-Credentials-API
7 var creds = vaultDbOps.getCredentials(roleName);
8 return Optional.of(new DatabaseCredentials(creds.getUsername(), creds.getPassword()));
9 }
10 } catch (Exception e) {
11 log.warn("Spring Vault DB-Credentials fehlgeschlagen ({}); Fallback.", e.getMessage());
12 }
13
14 // Fallback zu Raw HTTP, wenn Spring Vault nicht verfuegbar ist
15 HttpRequest req = buildGet("/v1/" + path);
16 HttpResponse<String> res = http.send(req, HttpResponse.BodyHandlers.ofString());
17 // ... JSON-Response parsen ...
18}
Der Pfad (dynamic-app/db/creds/app) kommt aus config.ini — der gleiche konfigurationsgesteuerte Ansatz wie bei den anderen Implementierungen.
Das Verzeichnis nomad/java/ enthaelt die gleiche Progression von Job-Dateien wie die Python-Serie:
app_hardcoded.hcl)Zugangsdaten direkt in das Template geschrieben. Identisch mit dem Ausgangspunkt im urspruenglichen Beitrag, nur mit dem java-Treiber.
1task "java-app" {
2 driver = "java"
3
4 config {
5 jar_path = "local/nomad-vault-mysql-java.jar"
6 jvm_options = ["-Xmx256m", "-Xms128m"]
7 }
8
9 artifact {
10 source = "https://github.com/infralovers/nomad-vault-mysql/releases/download/java-artifact-latest/nomad-vault-mysql-java.jar"
11 destination = "local/"
12 }
13
14 template {
15 destination = "local/config/config.ini"
16 data = <<EOF
17[DATABASE]
18Address = 127.0.0.1
19Port = 3306
20Database = my_app
21User = root
22Password = super-duper-password
23
24[VAULT]
25Enabled = False
26EOF
27 }
28}
app_dynamic.hcl)Fuegt den vault-Stanza und Consul-Template-Ausdruecke hinzu, um kurzlebige Credentials aus der Vault Database Secrets Engine zu beziehen:
1group "java-app" {
2 vault {
3 policies = ["nomad-dynamic-app"]
4 change_mode = "signal"
5 change_signal = "SIGINT"
6 }
7
8 task "java-app" {
9 driver = "java"
10 template {
11 destination = "local/config/config.ini"
12 data = <<EOF
13[DATABASE]
14{{ range service "mysql-server" }}
15Address = {{ .Address }}
16Port = {{ .Port }}
17{{ end }}
18{{ with secret "dynamic-app/kv/database" }}
19Database = {{ .Data.data.database }}
20{{ end }}
21{{ with secret "dynamic-app/db/creds/app" }}
22User = {{ .Data.username }}
23Password = {{ .Data.password }}
24{{ end }}
25
26[VAULT]
27Enabled = False
28EOF
29 }
30 }
31}
Wenn Vault die Credentials rotiert (Standard-TTL: 1 Stunde), empfaengt Nomad ein SIGINT und die Anwendung liest die Konfigurationsdatei neu — Zero-Downtime-Credential-Rotation ohne jegliche Anwendungslogik.
app_transit.hcl)Aktiviert die Vault Transit-Engine fuer feldbasierte Verschluesselung. Die einzige Aenderung gegenueber app_dynamic.hcl ist das Hinzufuegen des [VAULT]-Abschnitts mit Enabled = True:
1template {
2 destination = "local/config/config.ini"
3 data = <<EOF
4# ... DATABASE-Abschnitt wie oben ...
5
6[VAULT]
7Enabled = True
8InjectToken = True
9Namespace =
10Address = {{ env "VAULT_ADDR" }}
11KeyPath = dynamic-app/transit
12KeyName = app
13EOF
14}
app_transit_connect.hcl)Die vollstaendigste Variante fuegt einen Consul Connect Sidecar hinzu, damit MySQL-Traffic ueber das Service Mesh (mTLS) laeuft — passend zur Topologie aus unserem Consul Connect-Beitrag:
1network {
2 mode = "bridge"
3 port "web" {}
4}
5
6service {
7 connect {
8 sidecar_service {
9 proxy {
10 upstreams {
11 destination_name = "mysql-server"
12 local_bind_port = 3306
13 }
14 }
15 }
16 }
17}
Die Anwendung verbindet sich mit 127.0.0.1:3306 — dem lokalen Envoy-Sidecar — und Consul uebernimmt transparent die mTLS-Verbindung zum MySQL-Server.
java-Treiber | docker-Treiber | |
|---|---|---|
| Container-Runtime erforderlich | ✗ | ✓ |
| Image-Pull-Overhead | ✗ | ✓ |
| Artifact-Quelle | Beliebige URL, S3, GCS | Container-Registry |
| JVM-Versionsverwaltung | Host-OS oder Packer | Dockerfile |
| Prozessisolierung | OS-Prozess (cgroups) | Container-Namespace |
| Multi-Language-Workloads | Nur JVM | Beliebige Sprache |
| Am besten geeignet fuer | Bare Metal, Edge, JVM-Flotten | Gemischte Workloads, Portabilitaet |
Der java-Treiber ist kein Ersatz fuer Docker — er ist eine erstklassige Option fuer Umgebungen, in denen Docker unpraktisch ist, oder fuer Teams, die einfach JARs statt Images ausliefern moechten.
Der CI-Workflow baut das Fat-JAR und veroeffentlicht es bei jedem Push auf main in GitHub Releases:
1- name: JAR bauen
2 run: mvn -q package -DskipTests
3 working-directory: app/java
4
5- name: In GitHub Releases veroeffentlichen
6 uses: softprops/action-gh-release@v2
7 with:
8 tag_name: java-artifact-latest
9 files: app/java/target/nomad-vault-mysql-java-*.jar
10 name: nomad-vault-mysql-java.jar
Der java-artifact-latest-Tag wird bei jedem Build aktualisiert, sodass der Nomad-Job immer die neueste Version herunterlaedt. Fuer gepinnte Deployments verwenden Sie stattdessen einen versionierten Tag.
Der Nomad java-Treiber ermoeglicht ein wirklich anderes Deployment-Modell:
SERVER_PORTDer vollstaendige Quellcode inklusive aller vier Job-Varianten ist unter github.com/infralovers/nomad-vault-mysql verfuegbar.
Sie interessieren sich für unsere Trainings oder haben einfach eine Frage, die beantwortet werden muss? Sie können uns jederzeit kontaktieren! Wir werden unser Bestes tun, um alle Ihre Fragen zu beantworten.
Hier kontaktieren