[Update 01.24] :

  • Mise à jour dans la section Consul de la configuration Docker.
  • Ajout de la section Plugins CNI

Voilà un sujet qui me tient à coeur depuis un moment, ayant parlé à plusieurs reprises ici de Docker et Docker Swarm . L’orchestration de conteneurs est un vaste sujet à la mode depuis quelques années, et s’il existait plusieurs solutions pour y répondre, une seule semble faire l’unanimité : Kubernetes (K8s).

Comme chacun le sait, K8s a été développé par et pour Google, pour déployer, maintenir et faire grossir leurs applicatifs par le code. Et c’est pour cela que K8s est compliqué. Il a été conçu pour répondre à des problèmes complexes d’entreprises complexes.

J’ai pour ma part une nette préférence pour la simplicité. Notre métier est assez compliqué comme cela, pourquoi s’infliger “l’État de l’Art” du moment lorsqu’il existe des solutions plus simple adaptées à la cible ? Certes Rancher a depuis développé K3s mais il simplifie l’installation et la configuration d’un K8s pas son usage.

Cela fait un moment que je regarde Nomad du coin de l’oeil et me disant que je l’utiliserais sans doute un jour mais Docker swarm faisant le job… Cependant inutile de se voiler la face, swarm n’est pas sans problèmes et son avenir très incertain, j’ai décidé de tester Nomad d’Hashicorp.

Introduction

Mon infra est hébergée chez scaleway et composée de 3 VMs. 1 (node1) qui possède une IP public et 2 autres (node2 node3) qui n’en possèdent pas et n’accèdent alors pas à Internet. Pour installer sur node2 et node3 les paquets nomad, consul, etc, il faudra passer par un proxy squid installé sur node1. Voici l’article pour la configuration si vous êtes dans le même cas de figure : docker-swarm-chez-scaleway .
L’architecture est composée d’un serveur Nomad et un client Consul sur node1, d’un agent Nomad et un client Consul sur node2. D’un serveur Consul sur node3. Nomad ne doit pas communiquer directement avec un serveur Consul il est donc absent sur node3.
Sur une infra plus conséquente il est conseillé d’utiliser un cluster Nomad de 3 noeuds serveurs, idem pour Consul.

Docker

Je rajoute rapidement cette section sur Docker, car j’utilise dans cette présentation Docker via Nomad. Vous trouverez ici une page pour l’installer sur un serveur Ubuntu : Install Docker Engine on Ubuntu

Tailscale

Après squid la deuxième chose est la mise en place d’un VPN (peer-to-peer mesh) comme tailscale . Ainsi les services Nomad/Consul pourront communiquer entre eux que cela soit sur des VMs hébergées que vers des PC/raspberry à la maison. Rien de compliqué pour l’installation il y a tout ici : https://tailscale.com/download/ .
Attention à savoir qu’il faudra ajouter votre proxy dans le fichier /etc/default/tailscaled : https_proxy=http://user:PASS@IP_INTERNE_NODE1:3128 sinon le tailscale up ne fonctionnera pas depuis node2 et node3. Affichez le status de votre noeud tailscale pour vérifier : tailscale status.
Tailscale est bien entendu facultatif, vous pouvez utiliser simplement les IP internes de vos VMs si vous préférez.

Consul

Consul d’Hashicorp est un service qui permet entre autres d’enregistrer un nom de service (comme un DNS). Je l’utilise notamment pour caddy-docker-proxy que l’on verra après. Chaque service Nomad a besoin en local de son propre agent client Consul et chaque agent Consul sera connecté vers un serveur Consul sur node3. Attention il ne faut pas connecter un Nomad vers un serveur Consul sous peine de problèmes.
Pour l’installation il suffit d’ajouter les dépôts d’Hashicorp et d’installer le paquet consul qui n’est qu’un binaire (Go).

Voici la configuration de l’agent Consul sur node1

node1: /etc/consul.d/consul.hcl

data_dir = "/opt/consul"
bind_addr = "IP_TAILSCALE_NODE1"
retry_join = ["IP_TAILSCALE_NODE3"]

node2: /etc/consul.d/consul.hcl

data_dir = "/opt/consul"
bind_addr = "IP_TAILSCALE_NODE2" 
retry_join = ["IP_TAILSCALE_NODE3"]

Et celle du serveur Consul

node3: /etc/consul.d/consul.hcl

data_dir = "/opt/consul"

client_addr = "IP_TAILSCALE_NODE3"

ui_config{
  enabled = true
}

server = true
bind_addr = "IP_TAILSCALE_NODE3"
advertise_addr = "IP_TAILSCALE_NODE3"

bootstrap_expect=1
 
retry_join = ["IP_TAILSCALE_NODE3"]

ports {
  grpc = 8502
}

connect {
  enabled = true
}

Il suffit ensuite de lancer le service avec systemctl start consul sur les 3 noeuds.

Je n’ai qu’un seul serveur Consul à défaut de 3. Aussi il est nécessaire de préciser le nombre : bootstrap_expect=1 et de demander à Consul de se rejoindre lui même sous peine de blocage : retry_join = ["IP_TAILSCALE_NODE3"]

Consul propose une interface web ici : http://IP_TAILSCALE_NODE3:8500/ui/

Une capture qui affiche mes services en cours :

consul

Une fois le 2 clients et le serveur Consul en place il faut ajouter sur le serveur Nomad sur node1 cette configuration systemd-resolved afin qu’il (ou plutôt caddy-docker-proxy) utilise Consul pour la résolution de nom des domaines internes en .consul :

node1 : /etc/systemd/resolved.conf.d/consul.conf

[Resolve]
DNS=127.0.0.1:8600
DNSSEC=false
Domains=~consul

systemctl restart systemd-resolved

Une configuration est encore nécessaire côté Docker. En effet le conteneur caddy-docker-proxy (que l’on verra ensuite) devra utiliser Consul afin de résoudre les domaines en .consul. Pour cela il faut ajouter ceci dans un fichier docker.conf

node1: /etc/systemd/resolved.conf.d/docker.conf

[Resolve]
DNSStubListener=yes
DNSStubListenerExtra=172.17.0.1

systemctl restart systemd-resolved

Et ceci dans le fichier de configuration du daemon Docker

node1: /etc/docker/daemon.json

{
  "dns": ["172.17.0.1"]
}

systemctl restart docker

Nomad

Nomad est donc une alternative à Docker Swarm mais aussi à Kubernetes. Il est capable d’orchestrer 2 millions de conteneurs . Même s’il est moins connu que K8s de nombreux sites en parlent (voir quelques liens en fin d’article) comme une solution crédible. Il fonctionne également sur Windows et permet aussi d’orchestrer des applications qui ne sont pas dans des conteneurs.

Pour l’installation si vous avez déjà installé les dépôts d’hashicorp pour Consul il suffit de faire un apt install nomad. Sinon voir la doc : https://developer.hashicorp.com/nomad/install .

Voici mon fichier de configuration, à noter que Nomad va écouter uniquement sur l’IP de tailscale et non pas 0.0.0.0 comme présenté dans certains tutos.. En effet il ne serait pas très judicieux de donner un accès à tout le monde à votre Nomad.
Le serveur fait aussi office ici de client Nomad. On active le plugin Docker qui permettra à Nomad d’utiliser des volumes docker. Enfin on indique l’ip de l’agent local Consul.

node1: /etc/nomad.d/nomad.hcl

data_dir  = "/opt/nomad"
bind_addr = "IP_TAILSCALE_NODE1"

server {
  # license_path is required for Nomad Enterprise as of Nomad v1.1.1+
  #license_path = "/etc/nomad.d/license.hclic"
  enabled          = true
  bootstrap_expect = 1
}

client {
  enabled = true
  servers = ["IP_TAILSCALE_NODE1"]

  host_network "tailscale" {
    cidr = "IP_TAILSCALE_NODE1/32"
  }
}

plugin "docker" {
  config {
    volumes {
      enabled      = true
    }
    extra_labels = ["job_name", "job_id", "task_group_name", "task_name", "namespace", "node_name", "node_id"]
  }
}

consul {
  address = "127.0.0.1:8500"
}

Il suffit ensuite de lancer le service avec systemctl start nomad.

La configuration de l’agent Nomad sur node2

node2: /etc/nomad.d/nomad.hcl

data_dir  = "/opt/nomad"
bind_addr = "IP_TAILSCALE_NODE2"

advertise {
  # Defaults to the first private IP address.
  http = "IP_TAILSCALE_NODE2"
  rpc  = "IP_TAILSCALE_NODE2"
  serf = "IP_TAILSCALE_NODE2:5648" # non-default ports may be specified
}

server {
  # license_path is required for Nomad Enterprise as of Nomad v1.1.1+
  #license_path = "/etc/nomad.d/license.hclic"
  enabled          = false
  bootstrap_expect = 1
}

client {
  enabled = true
  servers = ["IP_TAILSCALE_NODE1"]

  host_network "tailscale" {
    cidr = "IP_TAILSCALE_NODE2/32"
  }

}

plugin "docker" {
  config {
    volumes {
      enabled      = true
    }
    extra_labels = ["job_name", "job_id", "task_group_name", "task_name", "namespace", "node_name", "node_id"]
  }
}

consul {
  address = "127.0.0.1:8500"
}

Si vous avez installé Tailscale sur votre desktop il suffit alors de lancer un navigateur web vers l’IP du serveur Nomad http://IP_TAILSCALE_NODE1:4646/ui/ pour accéder à l’interface d’admin.

Je conseille également d’installer et configurer Nomad sur le desktop afin de déployer sans se connecter au serveur. Pour cela il suffit de positionner cette variable d’environnement dans votre .bashrc / .zshrc :

export NOMAD_ADDR=http://IP_TAILSCALE_NODE1:4646

nomad node status                                                                         ✔ 
ID        Node Pool  DC   Name   Class   Drain  Eligibility  Status
d89a983a  default    dc1  node2  <none>  false  eligible     ready
6348bbae  default    dc1  node1  <none>  false  eligible     ready

Plugins CNI

Il est fort possible que vous voyez dans les logs de Nomad une erreur car il ne trouve pas le répertoire /opt/cni/bin. En effet il a besoin de ces fichiers binaires pour déployer correctement les conteneurs notamment pour la configuration réseau. Il faut donc récupérer ici le targz selon votre plateforme https://github.com/containernetworking/plugins/releases et le décompresser dans ce répertoire sur chaque VM cliente de Nomad.
Plus d’infos sur la page dédiée chez Hashicorp : CNI

caddy-docker-proxy

Caddy est un serveur web en Go, je vais l’utiliser avec son plugin docker-proxy afin qu’il expose tous mes conteneurs et qu’il gère automatiquement les certificats Let’s Encrypt.
J’avais parlé de caddy-docker-proxy dans un précédent texte : De traefik à caddy . L’objectif est ici de toujours l’utiliser mais avec Nomad. Voici le fichier hcl nécessaire pour déployer caddy-docker-proxy

caddy-docker-proxy.hcl

job "caddy" {
  datacenters = ["dc1"]
  type        = "service"

  group "proxy" {
    count = 1

    network {
      port "http-internal" {
        static       = 80
        to           = 80
        host_network = "tailscale"
      }

      port "https-internal" {
        static       = 443
        to           = 443
        host_network = "tailscale"
      }

      port "http-public" {
        static = 80
        to     = 80

        # host_network = "public"
      }

      port "https-public" {
        static = 443
        to     = 443

        # host_network = "public"
      }
    }

    restart {
      attempts = 2
      interval = "2m"
      delay    = "30s"
      mode     = "fail"
    }

    task "internal" {
      driver = "docker"

      constraint {
       attribute = "${attr.unique.hostname}"
       value     = "node1"
      }

      config {
        image = "lucaslorentz/caddy-docker-proxy:ci-alpine"

        volumes = [
          "/var/run/docker.sock:/var/run/docker.sock",
          "/volumes/caddy:/data"
        ]
        ports = ["http-internal", "https-internal"]
      }

      resources {
        cpu    = 100
        memory = 100
      }

    }

    task "public" {
      driver = "docker"

      constraint {
       attribute = "${attr.unique.hostname}"
       value     = "node1"
      }

      config {
        image = "lucaslorentz/caddy-docker-proxy:ci-alpine"

        volumes = [
          "/var/run/docker.sock:/var/run/docker.sock",
          "/volumes/caddy:/data"
        ]
        ports = ["http-public", "https-public"]
      }

      resources {
        cpu    = 200
        memory = 200
      }

    }
  }
}

Pour déployer ce premier conteneur on lance d’abord un validate pour vérifier qu’il n’y a pas d’erreur dans le hcl :

nomad job validate caddy-docker-proxy.hcl                                               
Job validation successful

et un plan pour vérifier que Nomad peut allouer les ressources nécessaires à ce conteneur :

nomad job plan caddy-docker-proxy.hcl

Job: "caddy"
Task Group: "proxy" (1 ignore)
  Task: "internal"
  Task: "public"

Scheduler dry-run:
- All tasks successfully allocated.

Job Modify Index: 65463
To submit the job with version verification run:

nomad job run -check-index 65463 caddy-docker-proxy.hcl

When running the job with the check-index flag, the job will only be run if the
job modify index given matches the server-side version. If the index has
changed, another user has modified the job and the plan's results are
potentially invalid.

Puis on déploie : nomad job run caddy-docker-proxy.hcl et on vérifie

nomad job status
caddy           service  50        running  2024-01-06T00:17:09+01:00

Nomad propose une interface web qui permet de consulter les jobs en cours et l’état des noeuds. Il suffit de se connecter sur http://IP_TAILSCALE_NODE1:4646/ui/

Une capture qui affiche mes conteneurs en cours

nomad

!!! Il y a quand même une différence avec Docker Swarm !!!.
En effet un conteneur déployé dans n’importe quel noeud du swarm pouvait être vu par caddy-docker-proxy ce qui n’est plus le cas avec Nomad, car chaque noeud docker est isolé. Seul les conteneurs déployés sur le noeud docker (node1) où se trouve caddy-docker-proxy seront vu.
Pour rendre visible un conteneur déployé sur une autre VM j’utilise comme astuce un conteneur qui ne fait rien sur le même Nomad/Docker que caddy-docker-proxy, exemple :

Ici je déploie un simple site web en hugo sur le node2 via l’attribut constraint

nostromo.hcl

job "nostromo" {
  datacenters = ["dc1"]
  type = "service" 
  group "app2" {
     count = 1

     network {
        port "http" {
           to     = 8080 # container port the app runs on
           static = 8080   # host port to expose
           host_network = "tailscale"
        }
      }

     task "nostromo" {
     		driver = "docker"

			constraint {
			 attribute = "${attr.unique.hostname}"
			 value     = "node2"
			}

			config {
			   image = "fredix/nostromo.social"

			   ports = [
			      "http"
			   ]
			}

			resources {
			   cpu = 500
			   memory = 256
			}

			service {
			   name = "nostromo"
			   tags = ["global", "app2"]
			   provider = "consul"
			   port = "http"

			   check {
			      type = "http"
			      name = "app_health"
			      path = "/"
			      interval = "20s"
			      timeout = "10s"
			  }
			}
     	}
  }
}

nomad job run nostromo.hcl

Ce service s’enregistre sur Consul avec comme nom “nostromo”, ainsi il sera accessible en interne via l’url nostromo.service.consul, on peut vérifier depuis node1 :

ping nostromo.service.consul
PING nostromo.service.consul (IP_TAILSCALE_NODE2) 56(84) bytes of data.
64 bytes from node2.node.dc1.consul (IP_TAILSCALE_NODE2): icmp_seq=1 ttl=64 time=1.43 ms

Cependant il n’est pas visible par caddy-docker-proxy et n’est donc pas exposé sur Internet. Pour cela il suffit de lancer un conteneur vide sur node1 (qui fait juste un sleep) mais qui ajoute les labels nécessaires :

nostromo-caddy.hcl

job "nostromo-caddy" {
  datacenters = ["dc1"]
  type = "service" 
  group "app" {
     count = 1

     task "nostromo-caddy" {
     		driver = "docker"

			constraint {
			 attribute = "${attr.unique.hostname}"
			 value     = "node1"
			}

			config {
				image = "kamlando/ubuntu-sleep"
				labels = {
				    "caddy" = "nostromo.social"
				    "caddy.reverse_proxy" = "http://nostromo.service.consul:8080"
				    "caddy.tls.ca" = "https://acme-v02.api.letsencrypt.org/directory"
				}
			}

			resources {
			   cpu = 100
			   memory = 64
			}
	
			service {
			   name = "nostromo-caddy"
			   tags = ["global", "app"]
			   provider = "consul"

			}
     	}
  }
}

Et voilà, nomad job run nostromo-caddy.hcl pour instancier ce conteneur sur la VM node1 qui va exposer sur Internet le conteneur nostromo sur la VM node2.

Si vous n’avez qu’un seul serveur Nomad ou si comme moi vous souhaitez utiliser également le serveur Nomad, voici un exemple tout simple qui va déployer sur node1 un service memos , dans ce cas Consul est inutile :

memos.hcl

job "memos" {
  datacenters = ["dc1"]
  type = "service" 
  group "app" {
     count = 1

     network {
        port "http" {
           to     = 5230 # container port the app runs on
        }
      }

     task "memos" {
     		driver = "docker"

			constraint {
			 attribute = "${attr.unique.hostname}"
			 value     = "node1"
			}

			config {
			   image = "neosmemo/memos:latest"
				labels = {
					"caddy" = "memos.fredix.xyz"
					"caddy.reverse_proxy" = "{{upstreams 5230}}"
					"caddy.tls.ca" = "https://acme-v02.api.letsencrypt.org/directory"
				}
				volumes = [
				 "/swarm/volumes/nomad/memos:/var/opt/memos"
				]				
			   ports = [
			      "http"
			   ]
			}

			resources {
			   cpu = 200
			   memory = 64
			}

			service {
			   tags = ["global", "app"]
			   provider = "nomad"
			   port = "http"

			   check {
			      type = "http"
			      name = "app_health"
			      path = "/"
			      interval = "20s"
			      timeout = "10s"
			  }
			}
     	}
  }
}                                                                          

nomad job run memos.hcl

Vous trouverez sur codeberg mes scripts hcl : https://codeberg.org/fredix/nomad

J’ai abordé ici très succintement et rapidement les possiblités de Nomad, j’y reviendrais sans doute un jour pour apporter des compléments. En espérant que cela vous ai donné envie de tester !

quelques liens :

Introduction to HashiCorp Nomad

Why you should take a look at Nomad before jumping on Kubernetes

How and why to use Nomad for orchestration at your startup

Nomad vs Kubernetes without the complexity

The Two Million Container Challenge

Running Nomad for home server

Understanding Networking in Nomad

Who Uses Nomad

(Ce texte a été écrit avec Ghostwriter2 ).