L'orchestration par Hashicorp - Nomad
Mise à jour :
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 endend
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é :
```bashvagrant 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 = 0nomad bootstrap = true known_regions = 1 leader = true leader_addr = 192.168.121.100:4647 server = trueraft 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 = 2runtime arch = amd64 cpu_count = 1 goroutines = 144 kernel.name = linux max_procs = 1 version = go1.17.5serf 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 = 1vault token_expire_time = token_ttl = 0s tracked_for_revoked = 0
Pour afficher le status des masters nodes :
nomad server membersName Address Port Status Leader Protocol Build Datacenter Regionnode-1.global 192.168.121.100 4648 alive true 2 1.2.5 dc1 global
Pour les workers nodes :
nomad node statusID DC Name Class Drain Eligibility Statusf91b3190 dc1 node-1 <none> false eligible readye46c9306 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 :
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-gamenomad job status
ID Type Priority Status Submit Date2048-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