L'orchestration par Hashicorp - 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 :
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_rsa.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_rsa
dest: /home/vagrant/.ssh/id_rsa
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 :
data_dir = "/opt/nomad/data"
datacenter = "dc1"
client {
enabled = true
servers = ["10.240.0.10"]
}
Et voilà tout est prêt à être lancé :
vagrant up
Présentation du dashboard Nomad
Le dashboard est disponible sur le port 4646 du master node.
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 :
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.
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
:
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
:
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 :
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 :
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é : Docker, QEMU, Java et des binaires statiques.
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 :
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.
Pour stopper un job :
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) :
nomad system gc