Aller au contenu

L'orchestration par Hashicorp - Nomad

Mise à jour :

logo nomad

Nomad est un orchestrateur qui permet d’utiliser d’autres ressources que les containers, comme les GPU Nvidia, les applications JAVA, les outils de virtualisation QEMU, firecrackers, …

Nomad intègre Consul, un autre outil d’HashiCorp qui offre des fonctionnalités de découverte et de configuration de services d’une infrastructure. Il permet aussi de stocker des données de type Clé/Valeur.

De la même manière Vault, aussi autre outil d’HashiCorp qui permet entre de stocker des secrets de manière sécurisé est facilement intégrable à Nomad.

Grâce à sa conception Nomad peut être déployé sur plusieurs datacenters et régions lui permettant d’être hautement disponible et tolérant aux pannes.

Nomad est fourni sous la forme d’un seul binaire permettant de configurer soit des masters nodes (3 ou 5), soit des workers nodes (les masters nodes peuvent aussi assurer la fonction de worker). Ce binaire est donc installé sur toutes les machines du cluster et recueille les informations sur les ressources disponibles (CPU, mémoire, disque) pour les envoyer au controller du cluster Nomad. C’est aussi ce même binaire qui permet de charger les workloards via des manifests écrits en HCL (comme pour Terraform )

Je pense que cela suffit pour vous convaincre que Nomad système semble bien plus simple à mettre en œuvre qu’un cluster Kubernetes. Et cela, sans pour autant faire l’impasse sur les fonctionnalités. En effet, on retrouve les mêmes fonctions que celles offertes par un cluster Kubernetes “from scratch”.

Installation d’un cluster Nomad

Je vais comme d’habitude utiliser une stack Vagrant pour déployer un master node et un worker node sur une machine Linux avec Libvirt comme hyperviseur. Vous pouvez adapter le VagrantFile pour qu’il fonctionne avec d’autres hyperviseurs comme VirtualBox. Vous pouvez vous inspirer du playbook pour l’installer sur d’autres plateformes comme AWS, GCP, Azure, …

Le code source se trouve ici. Commençons par le récupérer :

Terminal window
git clone https://github.com/stephrobert/nomad.git

Le contenu du VagrantFile :

# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure(2) do |config|
base_ip_str = "10.240.0.1"
number_masters = 1 # Number of master nodes kubernetes
number_workers = 2 # Number of workers nodes kubernetes
cpu = 1
mem = 1024
config.vm.box = "generic/ubuntu2004" # Image for all installations
# Compute nodes
number_machines = number_masters + number_workers
nodes = []
(0..number_workers).each do |i|
case i
when 0
nodes[i] = {
"name" => "master#{i + 1}",
"ip" => "#{base_ip_str}#{i}"
}
when 1..number_workers
nodes[i] = {
"name" => "worker#{i }",
"ip" => "#{base_ip_str}#{i}"
}
end
end
nodes.each do |node|
config.vm.define node["name"] do |machine|
machine.vm.hostname = node["name"]
machine.vm.network "private_network", ip: node["ip"]
machine.vm.synced_folder '.', '/vagrant', disabled: true
if (node["name"] =~ /master/)
machine.vm.network "forwarded_port", guest: 4646, host: 4646, auto_correct: true, host_ip: "127.0.0.1"
machine.vm.network "forwarded_port", guest: 8500, host: 8500, auto_correct: true, host_ip: "127.0.0.1"
else
machine.vm.network "forwarded_port", guest: 80, host: 80, auto_correct: true, host_ip: "127.0.0.1"
end
machine.vm.provider "libvirt" do |lv|
lv.cpus = cpu
lv.memory = mem
end
machine.vm.provision "ansible" do |ansible|
ansible.playbook = "playbooks/provision.yml"
ansible.groups = {
"master" => ["master1"],
"workers" => ["worker[1:#{number_workers}]"],
"nomad:children" => ["master", "workers"],
}
end
end
end
end

Le playbook Ansible :

---
- hosts: nomad
gather_facts: true
become: true
tasks:
- name: Replace a localhost entry with our own | {{ inventory_hostname }}
lineinfile:
path: /etc/hosts
regexp: '^127\.0\.0\.1'
line: 127.0.0.1 localhost
owner: root
group: root
mode: '0644'
- name: Allow password authentication |{{ inventory_hostname }}
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^PasswordAuthentication"
line: "PasswordAuthentication yes"
state: present
notify: restart sshd
- name: Set authorized key took from file | {{ inventory_hostname }}
authorized_key:
user: vagrant
state: present
key: "{{ lookup('file', '/home/vagrant/.ssh/id_ed25519.pub') }}"
- name: Add IP address of all hosts to all hosts | {{ inventory_hostname }}
lineinfile:
dest: /etc/hosts
regexp: '.*{{ item }}$'
line: "{{ hostvars[item].ansible_host }} {{ item }}"
state: present
when: hostvars[item].ansible_host is defined
with_items: "{{ groups.all }}"
- name: Copy SSH key
ansible.builtin.copy:
src: ~/.ssh/id_ed25519
dest: /home/vagrant/.ssh/id_ed25519
mode: 0600
owner: vagrant
group: vagrant
- name: Copy SSH config
ansible.builtin.copy:
src: files/ssh-config
dest: /home/vagrant/.ssh/config
mode: 0600
owner: vagrant
group: vagrant
- name: Check swap State
ansible.builtin.stat:
path: /swapfile
register: swap_file_check
- name: Umount swap | {{ inventory_hostname }}
ansible.posix.mount:
name: swap
fstype: swap
state: absent
when: swap_file_check.stat.exists
- name: Swap Off | {{ inventory_hostname }}
ansible.builtin.shell:
cmd: swapoff -a
when: ansible_swaptotal_mb > 0
- name: Add Docker GPG key | {{ inventory_hostname }}
apt_key:
url: https://download.docker.com/linux/ubuntu/gpg
- name: Add Hashicorp GPG key | {{ inventory_hostname }}
apt_key:
url: https://apt.releases.hashicorp.com/gpg
- name: Add Docker repository | {{ inventory_hostname }}
ansible.builtin.apt_repository:
repo: deb [arch=amd64] https://download.docker.com/{{ ansible_system | lower }}/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable
state: present
update_cache: false
- name: Add Hashicorp repository | {{ inventory_hostname }}
ansible.builtin.apt_repository:
repo: deb [arch=amd64] https://apt.releases.hashicorp.com {{ ansible_distribution_release }} main
state: present
update_cache: false
- name: Install packages | {{ inventory_hostname }}
ansible.builtin.package:
name:
- docker-ce
- docker-ce-cli
- nomad
- consul
- qemu-kvm
- qemu
state: present
update_cache: true
- name: hold version | {{ inventory_hostname }}
ansible.builtin.dpkg_selections:
name: "{{ item }}"
selection: hold
with_items:
- docker-ce
- nomad
- consul
- name: Add vagrant to group Docker | {{ inventory_hostname }}
ansible.builtin.user:
name: vagrant
group: docker
- name: Install Cloudflare SSL tools
ansible.builtin.uri:
url: "https://pkg.cfssl.org/R1.2/{{ item }}_linux-amd64"
dest: "/usr/local/bin/{{ item }}"
mode: 0755
status_code: [200, 304]
loop:
- cfssl
- cfssl-certinfo
- cfssljson
handlers:
- name: restart sshd
service:
name: sshd
state: restarted
########################################################################
# Initiate Nomad Server
########################################################################
- name: Initiate Nomade Server
hosts: master
gather_facts: true
become: true
tags: server
tasks:
- name: Create consul service
ansible.builtin.copy:
src: files/consul.service
dest: /etc/systemd/system/consul.service
mode: 0644
- name: Start service consul
ansible.builtin.service:
name: consul
state: started
enabled: true
- name: Create nomad config
ansible.builtin.copy:
src: files/nomad.hcl-master
dest: /etc/nomad.d/nomad.hcl
mode: 0644
- name: Start service nomad
ansible.builtin.service:
name: nomad
state: restarted
enabled: true
########################################################################
# Join Node to Nomad Server
########################################################################
- name: Initiate Nomade Node
hosts: workers
gather_facts: true
become: true
tags: node
tasks:
- name: Create nomad config
ansible.builtin.copy:
src: files/nomad.hcl-worker
dest: /etc/nomad.d/nomad.hcl
mode: 0644
- name: Start service nomad
ansible.builtin.service:
name: nomad
state: restarted
enabled: true

On commence par installer les repository Docker et Hashicorp pour installer docker, consul et nomad sur tous les noeuds. Docker est démarré sur tous les noeuds.

Ensuite vient la partie configuration des masters nodes avec le démarrage du service Consul et Nomad. Pour le cluster Nomad il copie le fichier de configuration nomad.hcl dans le répertoire /etc/nomad.d/.

data_dir = "/opt/nomad/data"
datacenter = "dc1"
server {
enabled = true
bootstrap_expect = 1
}
client {
enabled = true
}

On indique bien que nous attendons qu’un seul noeud master et que ce noeud peut aussi faire office de worker node client enabled = true.

La configuration des workers nodes est un peu différente puisque nous devons lui indiquer l’adresse des masters nodes :

hcldata_dir = “/opt/nomad/data” datacenter = “dc1”

client { enabled = true servers = [“10.240.0.10”] }

Et voilà tout est prêt à être lancé :
```bash
vagrant up

Présentation du dashboard Nomad

Le dashboard est disponible sur le port 4646 du master node.

dashboard nomad

Ce dashboard permet de retrouver toutes les informations des objets du cluster Nomad mais aussi de les gérer. On peut ainsi gérer les masters et workers nodes, les jobs et la partie stockage.

La vue Topology offre une vue synthétique du cluster :

dashboard nomad

Présentation du dashboard Consul

Pour rappel, Consul est un outil de découverte et de configuration de services et offre également une base de données Clés/Valeurs. Dans le cluster Nomad c’est lui qui s’interfacera avec un load balancer.

dashboard nomad

Utilisation de la CLI Nomad

Comme dis plus haut, le binaire nomad permet de gérer le cluster Nomad et ses ressources. Pour accéder à un cluster depuis une machine distante il suffit d’indiquer son adresse via la variable NOMAD_ADDR :

Terminal window
export NOMAD_ADDR=http://10.240.0.10:4646
nomad -autocomplete-install

Vous l’aurez compris, cette première commande permet d’installer la partie auto-completion (vérifiez votre fichier .bashrc et .zshrc).

Pour connaitre le status d’un cluster, on utilise la commande agent-info :

Terminal window
nomad agent-info
client
heartbeat_ttl = 15.91538535s
known_servers = 192.168.121.100:4647
last_heartbeat = 8.604064093s
node_id = f91b3190-822c-924a-a601-22d33dcb0e57
num_allocations = 0
nomad
bootstrap = true
known_regions = 1
leader = true
leader_addr = 192.168.121.100:4647
server = true
raft
applied_index = 156
commit_index = 156
fsm_pending = 0
last_contact = 0
last_log_index = 156
last_log_term = 2
last_snapshot_index = 0
last_snapshot_term = 0
latest_configuration = [{Suffrage:Voter ID:192.168.121.100:4647 Address:192.168.121.100:4647}]
latest_configuration_index = 0
num_peers = 0
protocol_version = 2
protocol_version_max = 3
protocol_version_min = 0
snapshot_version_max = 1
snapshot_version_min = 0
state = Leader
term = 2
runtime
arch = amd64
cpu_count = 1
goroutines = 144
kernel.name = linux
max_procs = 1
version = go1.17.5
serf
coordinate_resets = 0
encrypted = false
event_queue = 0
event_time = 1
failed = 0
health_score = 0
intent_queue = 0
left = 0
member_time = 1
members = 1
query_queue = 0
query_time = 1
vault
token_expire_time =
token_ttl = 0s
tracked_for_revoked = 0

Pour afficher le status des masters nodes :

Terminal window
nomad server members
Name Address Port Status Leader Protocol Build Datacenter Region
node-1.global 192.168.121.100 4648 alive true 2 1.2.5 dc1 global

Pour les workers nodes :

Terminal window
nomad node status
ID DC Name Class Drain Eligibility Status
f91b3190 dc1 node-1 <none> false eligible ready
e46c9306 dc1 node-2 <none> false eligible ready

Lancer vos premiers workloads

Avant de lancer un premier job, quelques définitions des objets Nomad :

job : Un job est l’object de plus haut niveau qui définit un ou plusieurs groupes de tasks qui contient une ou plusieurs tasks.

task group : Un groupe de tasks est un ensemble de tasks qui doivent être exécutées ensemble. Un groupe de tasks est l’unité de planification, ce qui signifie que le groupe entier doit s’exécuter sur le même nœud client et ne peut pas être divisé.

task : Une task est la plus petite unité de travail dans Nomad. Les tasks sont exécutées par des task drivers.

task driver : Un pilote de task indique le type de ressource qui est utilisé

evaluation : Les évaluations sont les mécanismes par lequel Nomad prend des décisions de planification. Comme pour kubernetes, Nomad, se charge de réconcilier l’état actuel du cluster avec celui déclaré dans les manifests.

Création de nombre premier job

Je vais utiliser un exemple que j’ai trouvé sur le site medium. C’est le déploiement d’un jeu de 2048 fournit sous la forme d’une application web.

Voici la spécification du job :

job "2048-game" {
datacenters = ["dc1"]
type = "service"
group "game" {
count = 1 # number of instances
network {
port "http" {
static = 80
}
}
task "2048" {
driver = "docker"
config {
image = "alexwhen/docker-2048"
ports = [
"http"
]
}
resources {
cpu = 500 # 500 MHz
memory = 256 # 256MB
}
}
}
}

Quelques explications : On déploie le job qui est du type service sur le datacenter dc1. Ce job est composé d’un groupe game dont le nombre d’instance est fixé à 1. Ce groupe expose le port 80 et est composé d’une task. Cette task utilise le driver docker et fait appel à une image “alexwhen/docker-2048”. Les limites de ressources sont fixés à 500Mhz pour le CPU et 256MB pour la mémoire.

Vous voyez nous sommes très proches de ce que l’on retrouve sur du Kubernetes.

Lançons le job :

Terminal window
nomad job run 2048.nomad
==> 2022-02-08T16:41:53Z: Monitoring evaluation "1abb0780"
2022-02-08T16:41:53Z: Evaluation triggered by job "2048-game"
2022-02-08T16:41:53Z: Evaluation within deployment: "efd8cb0a"
2022-02-08T16:41:53Z: Evaluation status changed: "pending" -> "complete"
==> 2022-02-08T16:41:53Z: Evaluation "1abb0780" finished with status "complete"
==> 2022-02-08T16:41:53Z: Monitoring deployment "efd8cb0a"
Deployment "efd8cb0a" successful
2022-02-08T16:41:53Z
ID = efd8cb0a
Job ID = 2048-game
Job Version = 0
Status = successful
description : Deployment completed successfully
Deployed
Task Group Desired Placed Healthy Unhealthy Progress Deadline
game 1 1 1 0 2022-02-08T14:43:35Z

Notre premier Job est déployé. Pour accéder à l’application depuis votre navigateur, il faut indiquer l’adresse ip du node où est déployé le job. Par exemple : http://192.168.121.235. Je ne maitrise pas encore la partie ingress. Cela fera l’objet de prochains billets.

dashboard nomad

Pour stopper un job :

Terminal window
nomad job stop 2048-game
nomad job status
ID Type Priority Status Submit Date
2048-game service 50 dead (stopped) 2022-02-08T14:33:21Z

Le Job reste au plan, mais n’utilise plus de ressources. Vous pouvez la redémarrer à tout moment avec la commande start. Si vous souhaitez le faire disparaître il suffit d’utiliser la commande suivante (comme avec Docker) :

Terminal window
nomad system gc

Plus d’infos

Sites