Linux things 🐧

un blog sur les technologies des logiciels libres et autres digressions

Nomad et Caddy

Transmission automatique de port entre Nomad et Caddy

Sun, 15 Sep 2024 23:22:12 +0100
# nomad   # consul   # caddy  

Cet article fait suite à Une infra avec Nomad, Consul et Tailscale

Introduction

Dans l’article précédent vous avez peut être remarqué une limitation dans le cas d’un service déporté qui ne tourne pas dans la même VM que Caddy. En effet lorsque j’ai écris le fichier nostromo.hcl j’ai imposé le port d’écoute sur le hôte dans la section network :

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

static = 8080 , pourquoi faire puisque Docker peut attribuer un port disponible automatiquement ?

C’était pour permettre la connection vers le service depuis Caddy, car dans les labels pour Caddy on a besoin de positionner l’URL et le port du service cible :

"caddy.reverse_proxy" = "http://nostromo.service.consul:8080"

Le problème que cela implique est que vous avez besoin de maintenir à la main un registre des ports alloués dans chaque VM puisque vous allez certainement déployer de multiples conteneurs par VM et donc de multiples services qui nécessitent un port d’écoute différent.

ps : J’ai préféré écrire un nouvel article qui propose une solution plutôt que de modifier l’ancien qui peut servir en l’état dans certains cas.

Tags

A l’époque de Docker swarm et Traefik tout cela était automatique, mais on va obtenir la même chose avec l’aide des tags. Dans l’article Nomad batch j’ai utilisé un tag afin de stocker le numéro d’identifiant unique NOMAD_ALLOC_ID, numéro qui permet au script batch de s’exécuter sur le bon serveur Nomad puis dans le conteneur postgresql.

De la même manière on va utiliser un tag pour simplement stocker le numéro de port du service :

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

     # Add an update stanza to enable rolling updates of the service
     update {
       max_parallel     = 2
       min_healthy_time = "30s"
       healthy_deadline = "5m"

       # Enable automatically reverting to the last stable job on a failed
       # deployment.
       auto_revert = true       
     }


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

     task "nostromo" {
     		driver = "docker"

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

			config {
			   image = "fredix/nostromo.social:0.3.1"

			   ports = [
			      "http"
			   ]
			}

			resources {
			   cpu = 100
			   memory = 64
			}

			service {
			   name = "nostromo"
			   provider = "consul"
			   port = "http"

			   tags = ["allocport=${NOMAD_HOST_PORT_http}"]

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

Cette fois-ci le static port est commenté et j’ai ajouté le tag allocport qui contient la valeur de la variable NOMAD_HOST_PORT_http. Ce tag est stocké dans Consul et contient le numéro du port attribué par Docker.

Rolling update

Petite digression, car j’ai ajouté une section update qui permet une mise à jour du service sans interruption avec un rollback si nécessaire (job-rolling-update). Par exemple si la version de l’image change et donc qu’une nouvelle image doit être déployée, on applique un plan :

nomad job plan nostromo.hcl

Il affiche les changements ainsi que la commande pour déployer la nouvelle version :

To submit the job with version verification run:

nomad job run -check-index 502945 nostromo.hcl

Caddy

Côté Caddy j’ajoute une section template qui récupère le tag du service cible et exporte le contenu dans une variable SERVICE_PORT

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

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

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

      template {
        data = <<EOH
			# as service 'nostromo' is registered in Consul
			# we want to grab its 'allocport' tag
			# set a default value
			SERVICE_PORT=0								
			{{- range $tag, $services := service "nostromo" | byTag -}}
				{{if $tag | contains "allocport"}}
				{{$allocId := index ($tag | split "=") 1}}
				SERVICE_PORT="{{ $allocId }}"
				{{end}}
			{{end}}
        EOH
        destination = "secrets/file.env"
        env         = true
      }

	config {
		image = "fredix/sleep"

		labels = {
			"caddy" = "nostromo.social"
			"caddy.reverse_proxy" = "http://nostromo.service.consul:${SERVICE_PORT}"
			# remove the following line when you have verified your setup
			# Otherwise you risk being rate limited by let's encrypt
			"caddy.tls.ca" = "https://acme-v02.api.letsencrypt.org/directory"
		}
	}

	resources {
		cpu = 10
		memory = 10
	}

	service {
		name = "nostromo-caddy"
		tags = ["global", "app"]
		provider = "consul"

	}
    }
}
}

Image sleep

Dernière digression, j’ai créé une image Docker, plus légère que la précédente (Alpine au lieu de Ubuntu) et qui fait juste un sleep infini. Si vous souhaitez utiliser votre propre image vous pouvez supprimer la ligne qui ajoute curl et jq si vous en n’avez pas besoin.

FROM alpine
LABEL org.opencontainers.image.authors="fredix@protonmail.com"

RUN apk add -U --no-cache curl jq

ENTRYPOINT ["sleep", "infinity"]

Conclusion

Si vous avez bien suivi vous aurez noté que je pourrais mettre dans le tag la variable NOMAD_HOST_ADDR_http qui contient l’IP et le port du service : TAILSCALE_IP:PORT et passer à Caddy cette variable :

"caddy.reverse_proxy" = "http://${SERVICE_PORT}"

Ce qui rendrait inutile la résolution DNS via Consul. Personnellement je préfère la conserver ce qui peut être utile dans certains cas, mais la simplicité étant la loi essayez comme cela.

Contrainte

Je rajoute une section contrainte car il y en a une. En effet si vous devez relancer votre conteneur Nostromo un nouveau port va lui être attibué par Docker, or entre temps il peut arriver que le conteneur nostromo-caddy plante car il ne trouve plus la variable SERVICE_PORT, il devrait être relancé par Nomad mais tout dépend du temps d’indisponibilité du conteneur Nostromo. Dans ce cas la bonne manière est de stopper le conteneur nostromo-caddy, stopper nostromo, le relancer, et ensuite lancer le conteneur nostromo-caddy.
A vous de voir si maintenir un annuaire des ports statiques par VM n’est finalement pas moins contraignant ?

Désolé j’expérimente en même temps que j’écris.. Donc à priori j’ai corrigé le problème de plantage en ajoutant avant le range dans le template :

SERVICE_PORT=0

Ainsi si le template ne trouve pas le service (dans l’exemple nostromo) parce qu’il est stoppé, ou s’il ne trouve pas le tag, la variable SERVICE_PORT est quand même créé et initialisé à zéro, ainsi le conteneur caddy ne plante plus. J’ai ensuite lancé le conteneur nostromo qui obtient donc un nouveau port et le stock dans Consul, le conteneur nostromo-caddy se met bien à jour en prenant en compte le nouveau port, il écrase donc bien le 0 par le numéro du port.
A moins que je détecte un nouveau problème, le static port est maintenant inutile.

(Ce texte a été écrit avec VNote)