Linux things 🐧

un blog sur les technologies des logiciels libres et autres digressions

Une infra avec Nomad, Consul et Tailscale

Une infra avec Nomad, Consul et Tailscale

Sat, 06 Jan 2024 23:22:12 +0100
# nomad   # consul   # tailscale   # docker  

[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).