Dockerloses Java auf HashiCorp Nomad: Spring Boot mit dem Java-Treiber


Bicycle

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.

Wie der Nomad Java-Treiber funktioniert

Der java-Treiber ist ein vollwertiger Nomad Task-Treiber. Wenn Nomad einen Task damit plant, laeuft folgender Lebenszyklus ab:

  1. Artifact-Fetch — Nomad laedt das angegebene JAR (oder ZIP) in das local/-Verzeichnis des Tasks herunter.
  2. Allocation-Setup — Nomad erstellt einen Netzwerk-Namespace und rendert alle template-Bloecke.
  3. Prozessstart — Nomad ruft java -jar <jar_path> [jvm_options] als Kindprozess auf.
  4. Ueberwachung — Nomad ueberwacht den Prozess, wendet Ressourcenlimits an und startet ihn gemaess dem restart-Stanza bei Fehlern neu.
  5. Log-Erfassung — stdout/stderr werden ueber Nomads Log-Treiber gestreamt und sind in der Nomad-UI und CLI genauso sichtbar wie Container-Workloads.

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.).

Das Artifact-Pattern: GitHub Releases statt Registry

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.

Dynamische Port-Zuweisung

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 Spring-Boot-Anwendung

Die Java-Anwendung ist eine Spring Boot 3.3.5 + Java 21 MVC-Anwendung, die die Python-Implementierung funktional spiegelt:

  • CRUD-Operationen auf einer customers-Tabelle in MySQL
  • Vault Transit-Verschluesselung via Spring Vault fuer birth_date, address und salary
  • Dynamische Datenbank-Credentials aus der Vault Database Secrets Engine
  • Konfiguration ueber config.ini (gleiches Format wie Python und .NET)
  • /health-Endpunkt fuer Nomad Service-Checks

Anwendungsstruktur

app/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

Der Vault-Client — Spring Vault

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.

Transit-Verschluesselung mit Spring Vault

 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.

Dynamische Datenbank-Credentials lesen mit Spring Vault

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.

Progressive Job-Varianten

Das Verzeichnis nomad/java/ enthaelt die gleiche Progression von Job-Dateien wie die Python-Serie:

1. Hartcodiert (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}

2. Dynamische Datenbank-Credentials (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.

3. Transit-Verschluesselung (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}

4. Transit + Consul Connect (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 vs Docker-Treiber: Wann welcher?

java-Treiberdocker-Treiber
Container-Runtime erforderlich
Image-Pull-Overhead
Artifact-QuelleBeliebige URL, S3, GCSContainer-Registry
JVM-VersionsverwaltungHost-OS oder PackerDockerfile
ProzessisolierungOS-Prozess (cgroups)Container-Namespace
Multi-Language-WorkloadsNur JVMBeliebige Sprache
Am besten geeignet fuerBare Metal, Edge, JVM-FlottenGemischte 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.

JAR bauen und veroeffentlichen

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.

Zusammenfassung

Der Nomad java-Treiber ermoeglicht ein wirklich anderes Deployment-Modell:

  • Kein Docker-Daemon auf dem Worker-Node
  • Artifact-basierte Auslieferung — das JAR wird zur Scheduling-Zeit von einem beliebigen HTTP-Endpunkt abgerufen
  • Dynamische Port-Zuweisung — Nomad weist einen freien Port zu; Spring Boot liest SERVER_PORT
  • Vollstaendige Vault-Integration — das gleiche Transit-Verschluesselungs- und Dynamic-Credentials-Pattern funktioniert identisch zu den Docker-basierten Varianten
  • Consul Connect — Service-Mesh-mTLS ist auch ohne Container verfuegbar

Der vollstaendige Quellcode inklusive aller vier Job-Varianten ist unter github.com/infralovers/nomad-vault-mysql verfuegbar.

Zurück Unsere Trainings entdecken

Wir sind für Sie da

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