Aller au contenu principal

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_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 :

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

Plus d'infos

Sites