Une infra avec Nomad, Consul et Tailscale
Sat, 06 Jan 2024 23:22:12 +0100[Update 10.24]
[Update 09.24]
[Update 01.24] :
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.
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.
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
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 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 :
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 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 "public" {
interface = "ens2"
}
host_network "tailscale" {
cidr = "100.64.0.0/10"
}
}
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
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 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 {
mode = "bridge"
port "internal" {
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 "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",
"/swarm/volumes/nomad:/data"
]
ports = ["internal","http-public","https-public"]
}
resources {
cpu = 400
memory = 1000
}
}
}
}
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
!!! 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 = "fredix/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" = "${NOMAD_HOST_ADDR_http}"
"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
Understanding Networking in Nomad
(Ce texte a été écrit avec Ghostwriter2).