Workload-Orchestrierung mit HashiCorp Nomad.

03.11.2021Raffael Schneider
Cloud HashiCorp nomad Orchestration Kubernetes Distributed Systems Serverless Cloud native

Eines der wichtigsten Tech-Entwicklungen der letzten zehn Jahre war die Containerisierung. Damit wurde es möglich ganze Applikationsumgebungen in einem Container laufen zu lassen.

Kubernetes ist die weit verbreiteste und mit Abstand bekannteste Lösung für die Orchestrierung von Containern. Gibt es denn überhaupt eine Alternative zu Kubernetes? Ja, HashiCorps Nomad ist vielleicht die einzige Alternative zu Kubernetes. Was zeichnet Nomad aus, oder besser gesagt, was macht Nomad anders? Genau zwei Dinge:

  • Ganz nach der Art der UNIX-Philosophie: Do one thing and do it well.

  • Flexibilität bezüglich containerisierten und nicht-containerisierten Applikationen

Steigen Sie mit uns ein und lassen Sie uns zusammen schauen was Nomad kann und warum vielleicht genau Sie Nomad Kubernetes bevorzugen könnten!

Mit einem Hammer sieht alles wie ein Nagel aus

Figure: Quelle: https://atodorov.me/2021/02/27/why-you-should-take-a-look-at-nomad-before-jumping-on-kubernetes/ (aufgerufen am 03.11.2021)

Kubernetes als die gängigste Lösung für Container-Orchestrierung hat auch weniger bekannte Limitierungen. Kubernetes 5’000 Nodes mit 300’000 Containern vs. Nomad mit erfolgreichen Realversuchen von 10’000 Nodes und bis zu 2 Millionen Containern. Siehe hierfür die ‘The Two Million Container Challenge’ (https://www.hashicorp.com/c2m ). Dabei wurden 2’000’000 Docker-Container auf 6’100 Hosts in 10 unterschiedlichen AWS-Regions in einem Zeitrahmen von 22 Minuten erfolgreich ausgerollt.

Es gibt bereits zahlreiche Nomad-Nutzer wovon bekannte Unternehmen wie Cloudflare, CircleCI, SAP, eBay oder die altbekannte Internet Archive zählen.

Workload-Orchestrierung mit Nomad

Nomad punktet mit ganz eigenen Vorteilen, die Kubernetes nicht zwingend in dieser Form zusichert. Darunter kann man folgende Punkte festhalten:

  • Deployment von Containern, Legacy-Applikationen und weiteren Workload-Typen

  • Einfach und zuverlässig

  • Geräte-Plugin und GPU-Unterstützung

  • Federation von Multi-Regions und Multi-Cloud

  • Bewährte Skalierbarkeit

  • HashiCorp-Ökosystem

Die Nomad-Architektur

Genau wie bei Kubernetes oder ähnlichen weitreichenden Software-Lösungen, kommt mit Nomad ein eigenes Verständnis der Orchestrierungsarchitektur, sowie mit dem damit verbundenen Glossar mit.

Vielleicht allen anderen Erläuterungen vorausgeschickt, Nomad ist eine einzelne Binary. Diese Binary kann wahlweise auf einem Host als Prozess, jeweils als Client oder Server (die Unterscheidung wird gleich erläutert) oder als Command-Line Interface durch den Endnutzer ausgeführt werden.

  • Server(s): Nomad-Server ist ein Verbund von mindestens 3 Server-Einheiten worauf ein Nomad-Agent im Server-Modus als Prozess läuft. Dabei ist dieser immer im Verbund der Leader und hat die Deutungshoheit über den Cluster. Diese Ordnung entspricht dem Consesus Protocol und basiert auf dem Raft-Konsensusalgorithmus.

  • Client(s): Nomad-Clients ist ein Verbund von einer Vielzahl von einzelnen Client-Einheiten worauf ein Nomad-Agent im Client-Modus als Prozess läuft. Der Client dient als Zielsystem, wenn Workload durch den Server-Verbund orchestriert werden muss. Auf einem Client wird die Workload, im Nomad-Glossar Job genannt.

  • Job: Ein Job ist eine Spezifikation, welche durch den Endnutzer deklariert wurde und als Workload auf eine Client-Einheit orchestriert und ausgerollt wird.

Job-Spezifikation

Der Job ist in erster Linie eine Spezifikation, die durch den Nutzer einmalig deklariert wird. Innerhalb eines Jobs gibt es eine Group, welche wiederum eine oder mehrere Tasks beinhalten kann. Die Hierarchie kann wie folgt zusammengefasst werden:

1
2
3
job
  \_ group
        \_ task

Job, Group und Task werden im Nomad-Jargon wie folgt definiert:

  • Job: Die Workload als deklarative Spezifikation.

  • Group: Auch Task Group genannt, ist eine Ansammlung von Tasks, welche zusammen ausgeführt werden müssen und Teil eines Jobs sind.

  • Task: Ist die kleinste Arbeitseinheit in Nomad. Tasks werden durch Driver ausgeführt und kann unterschiedliche Typen von ausführbaren Operationen beinhalten.

  • Driver: Auch Task Driver genannt, definiert was genau bei der Ausführung vorausgesetzt wird. Ein bekannter Task Driver wäre die Containerlösung Docker.

Task Driver

Somit unterstützt Nomad offiziell folgende Task Drivers:

  • Docker
  • Isolierte Execution (exec)
  • Raw Execution (raw exec)
  • JVM
  • Podman
  • QEMU-Virtualisierung

Dazu kommen von der Community betreute Task Drivers, welche unter anderem folgende Laufzeiten anbieten:

  • containerd
  • Firecracker
  • LXC
  • WebAssembly
  • FreeBSD Jail
  • Rooktout
  • Singularity (Container Platform)
  • systemd-nspawn
  • Windows IIS
  • AWS ECS als Remote-Target

Eine vollständige Auflistung kann in der offiziellen Dokumentation unter https://www.nomadproject.io/docs/drivers gefunden werden.

Beispielsdeklaration eines Nomad-Jobs

Hier sehen wir eine exemplarische Spezifikation eines Jobs mit dem Namen docs womit ein Webfrontend mithilfe eines Docker-Image hashicorp/web-frontend als Task ausgeführt wird und zur Verfügung gestellt werden kann.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
job "docs" {
  region
  =
  "us"
  datacenters
  = [
  "us-west-1",
  "us-east-1"
]
type = "service"
update {
  stagger
  =
  "30s"
  max_parallel
  =
  2
}
group "webs" {
  count
  =
  5
  network {
  port
  "http" {}
port "https" {
  static
  =
  443
}
}
service {
port = "http"
check {
type     = "http"
path     = "/health"
interval = "10s"
timeout  = "2s"
}
}
task "frontend" {
driver = "docker"
config {
image = "hashicorp/web-frontend"
ports = ["http", "https"]
}
env {
DB_HOST = "db01.example.com"
DB_USER = "web"
DB_PASS = "loremipsum"
}
resources {
cpu    = 500 # MHz
memory = 128 # MB
}
}
}
}

Noch ein Wort zu HCL

Die Manifeste, die wir vorhin gesehen haben sind, alle in einem relativ unbekannten Format verfasst: nämlich HCL. HCL steht als Akronym für HashiCorp Configuration Language und ist ein hauseigener Standard, welcher gerne für Konfigurationsbedürfnisse HashiCorps Produktpalette Verwendung findet.

HCL ist ein mit JSON-verwandtes Format und wird von HashiCorp als Fortführung von JSON verstanden. Als nativer Syntax, soll HCL maschinen-, wie auch menschenfreundlich sein und effizient von beiden gelesen werden können. Weitere Informationen und Spezifikationen können in der offiziellen Github-Repo von hashicorp/hcl gefunden werden.

HashiCorp Stack

HashiCorp ist eine aus San Francisco stammende Firma, die im Jahre 2012 von namensgebenden Mitchell Hashimoto und Armon Dadgar gegründet worden ist. Diese Silicon-Valley-Firma bietet Software-Lösungen im Bereich Cloud-Computing an und haben sich einen Namen mit ihrem relativ bekannten Infrastructure-as-Code-Produkt Terraform gemacht.

Hashicorp hat neben Terraform aber noch weitere Lösungen am Start und bietet eigentlich gesonderte Teillösungen, die jeweils einen Teilbereich der Cloud-Infrastruktur abdecken, die sich aber in der Kombination eine Gesamtlösung für eine vollwertige Cloud-Umgebung eignen. Genau dies ist mit dem HashiCorp-Stack gemeint: Das Ausrollen und Betreiben einer kompletten Cloud-Infrastruktur. Eine Gesamtauflistung dieser Teilkomponenten und Produkten von HashiCorp können wie folgt zusammengefasst werden:

  • Terraform: Automatisierung der Provisionierung einer Infrastruktur auf einem Cloud- und/oder Service-Provider basierend auf dem Prinzip von Infrastructure-as-Code.
  • Packer: Bauen von Machine-Images als Container mit einer einzigen Source-Konfigurationsdatei.
  • Vagrant: Bereitstellung von reproduzierbaren Entwicklungsumgebungen mithilfe von Virtualisierung.
  • Consul: Implementierung eines klassischen Service-Mesh und DNS-basierte Service Discovery.
  • Vault: Bereitstellung von Secrets Management, Encryption von Applikationsdaten und weiteren Security-Mechanismen.
  • Boundary: Bereitstellung einer Plattform für Identity-based Access.
  • Waypoint: Abstraktionslayer für das eine komplette Pipeline (Build&Deploy) auf unterschiedlichen Plattformen wie Kubernetes oder AWS ECS.
  • Nomad: Das hier thematisierte Orchestrierungstool für Workloads aller Art.

Figure: Quelle: https://www.hashicorp.com/resources/unlocking-the-cloud-operating-model-with-microsoft-azure (aufgerufen 03.11.2021)

HashiCorp Cloud Platform

Es gibt auch eine hauseigene Cloud-Platform womit man die Teillösung gleich bei HashiCorp selber hosten lassen kann: Das ist die HashiCorp Cloud Platform, oder kurz HCP.

In der jetzigen Fassung werden (noch) nicht alle Produkte als Service angeboten, aber man kann davon ausgehen, dass langfristig alle eigenen Produkte angeboten werden. Zurzeit steht noch kein Angebot zu HashiCorps Nomad zur Verfügung. Somit muss Nomad weiterhin auf einer Cloud-basierten Umgebung wie AWS ECS oder gleich auf Bare-Metals gehosted werden.

Praktischer Grundkurs mit Nomad

Installation auf macOS:

1
2
❯ brew tap hashicorp/tap
❯ brew install hashicorp/tap/nomad

Um eine lokale Instanz von Nomad zu starten, einfach den folgenden Befehl ausführen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
❯ sudo nomad agent -dev
Password:
==> No configuration files loaded
==> Starting Nomad agent...
==> Nomad agent configuration:

       Advertise Addrs: HTTP: 127.0.0.1:4646; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
            Bind Addrs: HTTP: 127.0.0.1:4646; RPC: 127.0.0.1:4647; Serf: 127.0.0.1:4648
                Client: true
             Log Level: DEBUG
                Region: global (DC: dc1)
                Server: true
               Version: 1.1.6

==> Nomad agent started! Log data will stream in below:

Prüfe den Status von der Nomad-Instanz wie folgt:

1
2
3
❯ nomad node status
ID        DC   Name            Class   Drain  Eligibility  Status
5750399b  dc1  spacegrey.home  <none>  false  eligible     ready

Mit einem Abruf der Members des Servers sehen wir, dass der Agent tatsächlich im Server-Modus läuft und als Leader Teil des Gossip-Protocols ist:

1
2
3
❯ nomad server members
Name                   Address    Port  Status  Leader  Protocol  Build  Datacenter  Region
spacegrey.home.global  127.0.0.1  4648  alive   true    2         1.1.6  dc1         global

Nun können wir unseren ersten Job für den Nomad-Cluster definieren. Dazu bietet die Nomad-CLI einen nützlichen Befehl. Switchen wir dazu schnell ins /tmp-Verzeichnis und führen nomad job init aus.

1
2
3
cd /tmp
❯ nomad job init
Example job file written to example.nomad

Das generiert uns ein Beispiels-Job ins lokale Verzeichnis mit dem Dateinamen example.nomad. Wenn wir die Datei mit less oder cat inspecten bekommen wir Folgendes zu sehen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
job "example" {
  datacenters
  = [
  "dc1"
]

group "cache" {
  network {
  port
  "db" {
  to
  =
  6379
}
}

task "redis" {
driver = "docker"

config {
image = "redis:3.2"

ports = ["db"]
}

resources {
cpu    = 500
memory = 256
}
}
}
}

Falls Sie den job init-Befehl genau gleich ausgeführt haben wie dieser oben steht, werden Sie viel mehr Kommentarzeilen in ihrem Beispiel vorfinden. Ich habe die Ausgabe für die Ansicht hier im Artikel mit dem -short -Flag abkürzen lassen, sodass nur für Nomad signifikante Teil generiert werden.

Der Job ist recht übersichtlich und deklariert einen Job mit dem Namen example, darin eine Group mit dem Namen cache , sowie –als kleinste Einheit einer Nomad-Job-Definition– einen Task mit dem Namen redis, welcher das Docker-Image redis:3.2 ausrollen soll. Ein Port, wird auch definiert, sowie eine Ressourcen-Angabe für CPU und Memory. Wichtig hierbei anzumerken, ist die explizite Deklaration eines docker-Drivers. Wie eingangs bereits erwähnt, kann Nomad nicht nur Docker-Images, sondern eine ganze Bandbreite von Workloads orchestrieren. Hier bleiben wir aber bei einem klassischen Docker-Image. So weit, so gut.

Jetzt rollen wir den Job aus. Das geht ganz einfach wie folgt:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
❯ nomad job run example.nomad
==> 2021-10-25T14:37:38+02:00: Monitoring evaluation "871ad9d0"
    2021-10-25T14:37:38+02:00: Evaluation triggered by job "example"
==> 2021-10-25T14:37:39+02:00: Monitoring evaluation "871ad9d0"
    2021-10-25T14:37:39+02:00: Evaluation within deployment: "7572841a"
    2021-10-25T14:37:39+02:00: Allocation "0c88d2ef" created: node "5750399b", group "cache"
    2021-10-25T14:37:39+02:00: Evaluation status changed: "pending" -> "complete"
==> 2021-10-25T14:37:39+02:00: Evaluation "871ad9d0" finished with status "complete"
==> 2021-10-25T14:37:39+02:00: Monitoring deployment "7572841a"
  ⠴ Deployment "7572841a" in progress...
  ⠦ Deployment "7572841a" in progress...
  ✓ Deployment "7572841a" successful
    
    2021-10-25T14:37:55+02:00
    ID          = 7572841a
    Job ID      = example
    Job Version = 0
    Status      = successful
    Description = Deployment completed successfully
    
    Deployed
    Task Group  Desired  Placed  Healthy  Unhealthy  Progress Deadline
    cache       1        1       1        0          2021-10-25T14:47:54+02:00

Nachdem das Image gezogen und ausgerollt wurde, prüfen wir den Status von allen vorhandenen Jobs:

1
2
3
❯ nomad job status
ID       Type     Priority  Status   Submit Date
example  service  50        running  2021-10-25T14:37:38+02:00

Sowie spezifisch des Jobs mit der ID example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
❯ nomad job status example
ID            = example
Name          = example
Submit Date   = 2021-10-25T14:37:38+02:00
Type          = service
Priority      = 50
Datacenters   = dc1
Namespace     = default
Status        = running
Periodic      = false
Parameterized = false

Summary
Task Group  Queued  Starting  Running  Failed  Complete  Lost
cache       0       0         1        0       0         0

Latest Deployment
ID          = 7572841a
Status      = successful
Description = Deployment completed successfully

Deployed
Task Group  Desired  Placed  Healthy  Unhealthy  Progress Deadline
cache       1        1       1        0          2021-10-25T14:47:54+02:00

Allocations
ID        Node ID   Task Group  Version  Desired  Status   Created  Modified
0c88d2ef  5750399b  cache       0        run      running  29s ago  13s ago

In meinem Fall hat der Job eine Allokation mit der ID 0c88d2ef. Damit kann ich weitere Zustände prüfen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
❯ nomad alloc status 0c88d2ef
ID                  = 0c88d2ef-db85-44da-9460-106be0e3ffb9
Eval ID             = 871ad9d0
Name                = example.cache[0]
Node ID             = 5750399b
Node Name           = spacegrey.home
Job ID              = example
Job Version         = 0
Client Status       = running
Client Description  = Tasks are running
Desired Status      = run
Desired Description = <none>
Created             = 1m12s ago
Modified            = 56s ago
Deployment ID       = 7572841a
Deployment Health   = healthy

Allocation Addresses
Label  Dynamic  Address
*db    yes      127.0.0.1:25415 -> 6379

Task "redis" is "running"
Task Resources
CPU        Memory           Disk     Addresses
9/500 MHz  792 KiB/256 MiB  300 MiB  

Task Events:
Started At     = 2021-10-25T12:37:44Z
Finished At    = N/A
Total Restarts = 0
Last Restart   = N/A

Recent Events:
Time                       Type        Description
2021-10-25T14:37:44+02:00  Started     Task started by client
2021-10-25T14:37:38+02:00  Driver      Downloading image
2021-10-25T14:37:38+02:00  Task Setup  Building Task Directory
2021-10-25T14:37:38+02:00  Received    Task received by client

Oder auch die Logs des Jobs mit der gleichen Allokations-ID:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
❯ nomad alloc logs 0c88d2ef
1:C 25 Oct 12:37:44.444 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
                _._                                                  
           _.-``__ ''-._                                             
      _.-``    `.  `_.  ''-._           Redis 3.2.12 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._                                   
 (    '      ,       .-`  | `,    )     Running in standalone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6379
 |    `-._   `._    /     _.-'    |     PID: 1
  `-._    `-._  `-./  _.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |           http://redis.io        
  `-._    `-._`-.__.-'_.-'    _.-'                                   
 |`-._`-._    `-.__.-'    _.-'_.-'|                                  
 |    `-._`-._        _.-'_.-'    |                                  
  `-._    `-._`-.__.-'_.-'    _.-'                                   
      `-._    `-.__.-'    _.-'                                       
          `-._        _.-'                                           
              `-.__.-'                                               

1:M 25 Oct 12:37:44.446 # Server started, Redis version 3.2.12
1:M 25 Oct 12:37:44.446 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
1:M 25 Oct 12:37:44.446 * The server is now ready to accept connections on port 6379

Genau wie bei Kubernetes, gibt es bei Nomad auch eine graphische Oberfläche. Diese läuft unter dem Port und kann mit folgendem Link aufgerufen werden.

http://127.0.0.1:4646/ui/

Unter Jobs können wir unseren example-Workload genauer anschauen.

Fazit

Nomad hat bei uns einen guten Eindruck hinterlassen. Es hat weder die Grösse, noch die Masse eines Kubernetes, aber glänzt durch seinen Minimalismus mit Flexibilität und Effizienz. Nomad kann nicht alles, aber was es kann, macht es gut. In unserer Einschätzung eignet sich Nomad in spezifischen Use-Cases wo Kubernetes etwa zu gross und zu träge wäre. Das beinhaltet kleine Entwickler-Teams oder die Notwendigkeit eine gewisse Flexibilität bei der Verwendung von Applikationsumgebungen –wo ein Container vielleicht nicht zwingend der kürzeste Weg zum Ziel ist– haben zu wollen.

Ein weiterer Use-Case ist sicherlich der Umstand, dass Nomad direkt auf GPU-Leistung zugreifen kann und somit Workloads interessant werden, welche auf dessen Leistung angewiesen sind . Darunter fallen zum Beispiel Applikationen wie Machine Learning, Cryptocurrency-Mining oder allgemein Ressourcen-intensives Scientific Computing.

Wir bei b-nova haben Nomad zwar nicht im Einsatz, denken aber, dass sich Nomad am besten für kleine Startup-Teams, KMU im Allgemeinen oder in ganz spezifischen Nischenbereichen einsetzen lässt. In den meisten anderen Fällen ist Kubernetes sicherlich die bessere Lösung. Falls Sie Beratung und Unterstützung bei Nomad, Kubernetes oder allgemein im Container- und Cloud-Umfeld brauchen, so stehen wir Ihnen jederzeit zur Verfügung. Stay tuned!

https://www.nomadproject.io/

https://learn.hashicorp.com/nomad

https://learn.hashicorp.com/collections/nomad/get-started

https://www.hashicorp.com/blog/a-kubernetes-user-s-guide-to-hashicorp-nomad

https://atodorov.me/2021/02/27/why-you-should-take-a-look-at-nomad-before-jumping-on-kubernetes/

https://endler.dev/2019/maybe-you-dont-need-kubernetes/

https://medium.com/hashicorp-engineering/hashicorp-nomad-from-zero-to-wow-1615345aa539

https://aws.amazon.com/quickstart/architecture/nomad/

https://manicminer.io/posts/getting-started-with-hashicorp-nomad/