Bien que k8s soit très popularisé et majoritairement utilisé dans le rôle d’orchestration de containers, il n’est pas le seul. Une alternative proposé par Hashicorp, Nomad, permet également une orchestration de ce type qui se dit plus simple et répondant à des besoins parfois différents types batch.
Pour ma part je me suis retrouvé confronté au besoin de monter plusieurs petits services nécessitant l’utilisation de containers. Afin de ne pas avoir encore un nouveau serveur avec un Docker standelone, et ne souhaitant pas encore aborder le sujet k8s dans ce contexte, je me suis rapproché de Nomad.
L’objectif étant de mettre en place un serveur Nomad de manière très simple et de construire et faire évoluer celui petit à petit s’il correspond bien à mon besoin. Les applications que j’ai a installer sont principalements des pods Stateful
.
Pour ça j’aurais à disposition:
L’installation de la CNI (Container Network Interface) se fait très facilement dans Nomad. Contrairement à Kub où l’on trouve pas mal de choix, la documentation ne fera référence que d’un seul choix.
Si vous souhaitez faire des configurations plus poussez, il existe également la possibilité d’utiliser des alternatives à cette CNI, notamment avec Cilium, exemple ici.
Au même titre que la CNI, la CSI (Container Storage Interface) vas vous permettre la gestion du stockage, et ça ce complique un petit peu à partir d’ici.
Avec une documentation claire et très simple sur la CNI, je m’attendais à la même chose pour la CSI, mais j’ai été un peu déçu. La documentation nous indique qu’elle suit les standards et qu’il faut se référer à la documentation Kubernetes. Le problème est que certaines CSI, tel qu’OpenEBS, ne supportent pas/plus Nomad, et c’est à nous le savoir en lisant les specs de toutes les CSI.
Bien que ma machine nomad était dans un contexte Scaleway et qu’il existe une CSI, je souhaitais avoir un stockage persistant local dans un premier temps, histoire de ne pas se faire facturer une deuxième fois. Sans OpenEBS j’ai testé trois alternatives.
Portworx est une solution de Pure Sotage. L’installation se fait en deux parties, un node et un controller, une solution assez intéressante et séduisante. Mais bien que ça s’appuie sur une lib opensource openstorage, il y a un modèle B2B que j’ai du mal à comprendre et je n’ai pas pris le temps de plus creuser.
Hostpath, utilisé comme exemple dans la documentation, ça me semblait assez facile d’utilisation. Tout fonctionnais bien jusqu’au restart du serveur, où mes volumes deviennent inutilisables. Après quelques recherches il est fort probable que je sois tombé sur un bug en cours de correction lors de mes tests.
Nomad, le stockage propre à Nomad, où il faut déclarer notre volume dans la conf client. Pour le coup très simple à mettre en place et relativement bien documenté.
Le principe de toutes ces solutions restent les mêmes. On créer un volume, on le réclame dans notre groupe, puis on l’attache à notre/nos task(s).
Un ingress controller est une ressource qui permet de centraliser, controller et d’exposer un ou plusieurs service(s). Au final il s’agit ni plus ni moins d’un reverse proxy avec les features que la solution dispose (loadbalancer, controller des headers http, forward tcp ou udp, restriction d’accès…). Habitué à l’ingress controller nginx dans k8s, j’ai voulu partir sur la même solution. Bon il m’a fallu peu de temps pour comprendre que c’était vite compliqué et que malgrès nomad pack (j’en parle juste après) je n’allais pas vraiment pouvoir faire tout ce que je souhaitais.
C’est par la suite que je suis partie sur traefik. Avec une intégration native de nomad et un support de letsencrypt, j’avais tout ce que souhaitais et je ne pense pas qu’il soit nécessaire de tester plus de solutions sur nomad.
L’un des plus gros problème (à mes yeux) est qu’il n’y ait pas de capitalisation de connaissance et d’outils pour simplifier le déploiement de briques qui peuvent être complexe (csi, cni, ingress…). À chaque fois il est nécessaire de faire son propre job contrairement à k8s qui dispose de helm avec une grande quantité de charts disponibles, notamment grâce aux charts bitnami.
C’est là que Nomad pack intervient, il s’agit d’un produit plutôt équivalent pour nomad. Dans les utilisations ça semble équivalent, une registry par default est à disposition et plusieurs packs sont déjà prêts à consommer. Le seul problème est que c’est une technical-release et que la release actuelle ne correspond plus au contenue de la registry. Ça fail et tout est inutilisable (cc #346. Malgrès un patch en local (dispo sur la master) il existe un autre bug, du fait de cette instabilité j’ai écarté totalement cette solution (mais disponible dans mon role ansible).
Les jobs nomad seront la configuration nécessaire pour mettre en place un service. Dans ces derniers on va définir un ou plusieur group(s) disposant de la configuration des volumes, des services mais aussi d’une ou plusieur task(s).
Bon après tout ça il était intéressant de partir sur un vrai use-case. On va mettre en place 2 services web, citizen qui une registry terraform et un forum discourse.
Dans un premier temps il est nécessaire de mettre en place le serveur Nomad. J’avais à ma disposition uniquement Ansible pour m’appuyer sur ce besoin. Après avoir fait le tour de ce qui se passe au niveau des rôles, je n’ai rien trouvé de pertinent. Peu, voir pas, maintenu sur l’existant j’ai pris la décision de faire le mien avec un scope assez réduit car je prendrais en compte que Ubuntu 22. L’installation se fait assez rapidement en suivant la documentation. Les étapes seront les suivants:
J’ai mis à disposition le code source de ce que j’ai fait sur github fe80/fe80.nomad. Je vais utiliser les variables suivantes.
---
# List of require host volume
nomad_host_volumes:
postgresql:
path: /var/lib/postgresql
discourse:
path: /srv/discourse
citizen:
path: /srv/citizen
acme:
path: /etc/acme
# Vault
nomad_vault:
enabled: true
address: https://my.vault.io
task_token_ttl: 12h
create_from_role: nomad-servers
token: "<my token>"
# Tls
nomad_tls:
ca_file: /etc/ssl/certs/consul-ca.pem
cert_file: /etc/ssl/certs/nomad.pem
key_file: /etc/ssl/private/nomad.key
Il est bon de noter que je vais faire une pre-task qui va aller créer un certificat dans une pki vault. Ce qui donne une task du style:
---
- name: Check if private key already exist
ansible.builtin.stat:
path: "{{ ssl_path.private_key }}"
register: private_key
- name: Create tls context
when: not private_key.stat.exists
block:
- name: Generate tls certificat
community.hashi_vault.vault_pki_generate_certificate:
role_name: "{{ pki.role }}"
engine_mount_point: "{{ pki.mount }}"
url: "{{ lookup('ansible.builtin.env', 'VAULT_ADDR') }}"
token: "{{ lookup('ansible.builtin.env', 'VAULT_TOKEN') }}"
common_name: nomad.my.cluster
ttl: 3650d
alt_names: "alt_names"
ip_sans: "{{ ['127.0.0.1'] + ansible_all_ipv4_addresses }}"
register: tls
delegate_to: localhost
become: false
- name: Copy content for "{{ item }}"
ansible.builtin.copy:
content: "{{ tls.data.data[item] }}"
dest: "{{ ssl_path[item] }}"
owner: root
group: root
mode: '0444'
no_log: true
loop: "{{ ssl_path.keys() | list }}"
- name: Fix private key permission
ansible.builtin.file:
path: "{{ ssl_path.private_key }}"
state: file
mode: '0400'
owner: root
group: root
Il est important de mettre en place les ip du serveur dans ip_sans
, puisque ce sera avec les IP que mon traefik va intérroger nomad.
Inspiré de Capistrano j’ai mis en place le déploiement et la gestion de ma CNI dans mon role (cc plugins.yml).
Une fois le playbook appliqué vous devriez avoir accès à https://<ip>:4646
qui sera votre console nomad/
Après ça, il me reste plus qu’à utiliser le provider nomad de terraform pour pouvoir gérer la création de mes jobs. À noter que si vous utilisez une CA propre à votre cluster (comme ce qui est expliqué dans la documentation) il sera nécessaire de soit l’inclure dans votre bibliothèque de CA, ou d’avoir la variable d’environnement NOMAD_SKIP_VERIFY=true
(ou skip_verify pour le provider >= 2.0.0).
La configuration traefik se distingue du fait qu’elle peut être écrite à plusieurs niveaux, statique, dynamique et enfin (pour le cas de nomad) avec les tags ou les metadatas des services. Et on va utiliser les trois types. La configuration par default sera déposé dans /etc/traefik/traefik.yml
. Elle disposera de la des entrypoints, des providers et de la conf letsencrypt (je mettrais du route53 et du http challenge pour l’exemple). Dans la liste des providers sera mit en place l’endroit de la configuration dynamique afin de mettre plusieurs routers par default pour l’accès à la consol nomad et au dashboard traefik. Et enfin j’utiliserais les tags pour configurer mes ingress avec mes services.
Suivant les concepts, traefik utilisera le chemin suivant lors de l’arriver d’une requête: EntryPoints
(comme le port http 80) -> Routers
(utilisé pour savoir sur quel service envoyé la requête), Middlewares
(optionnel, permet de définir des traitements particulier à sa requête tel qu’une redirection), Services
(contenant la définition du service où la requête doit aller)
Dans le job suivant je vais créer mon ingress traefik (il s’agira de mon job le plus complexe en terme de feature). La sécurisation des accès se fera par filtrage ipv4 et 1.2.3.4
sera notre ip d’admin et nomad.p0l.io
notre accès public nomad. Lire les commentaires pour les détails.
Ingress job
job "ingress" {
# Using my nomad DC (default value)
datacenters = ["dc1"]
# Define the scheduler type
# Refere to citizen job for more informations
type = "system"
group "ingress" {
# We require only one pod
count = 1
# Claim volume acme defined from client configuration
volume "acme" {
type = "host"
source = "acme"
read_only = false
}
# Creat my network configuration
network {
# Http port use by traefik for public trafic map on port 80 on the host
port "http" {
static = 80
}
# Https port use by traefik for public trafic map on port 443 on the host
port "https" {
static = 443
}
# Traefik dashboard's port used for rescue map on port 8081 on the host
port "traefik" {
static = 8081
}
}
# Expose my diffrents services
# I don't use consul, so I declare the provider name `nomad`
service {
provider = "nomad"
name = "http"
port = "http"
}
service {
provider = "nomad"
name = "https"
port = "https"
}
service {
provider = "nomad"
port = "traefik"
}
# Create my task whith my container configuration
task "traefik" {
# Define the driver
driver = "docker"
# Claim my vault policy I need to get my secrets
vault {
policies = ["route53"]
}
# Set up some cpu/ram limitation
resources {
cpu = 200
memory = 128
}
# Mount my volume acme on my pod in /etc/acme
volume_mount {
volume = "acme"
destination = "/etc/acme"
}
# Container configuration
config {
# Image name
image = "traefik:v2.10.3"
# Exposed port (used by services)
ports = ["https", "http", "traefik"]
# Mount my volumes
volumes = [
"/etc/ssl/certs:/certs", # This volume is directly on the host, require for nomad CA
"local/traefik.yml:/etc/traefik/traefik.yml",
]
}
# Create configuration file shared on the container
template {
data = <<EOH
---
http:
# Default routers
routers:
# Set up traefk public dashboard access
api:
# Use tls
tls: true
# Limited on nomad.p0l.io/traefik and nomad.p0l.io/api for IP 1.2.3.4
rule: Host(`nomad.p0l.io`) && (PathPrefix(`/traefik`) || PathPrefix(`/api`)) && ClientIP(`1.2.3.4`)
# Use service api@internal
# This is a intenal service created by traefik when api is on
service: api@internal
# Use custom middlewares traefik
middlewares: traefik
# Set up nomad consol access (pretty same configuration)
nomad:
tls: true
rule: Host(`nomad.p0l.io`) && (PathPrefix(`/v1`) || PathPrefix(`/ui`)) && ClientIP(`1.2.3.4`)
# Use my service created on my file provider
service: nomad@file
middlewares: nomad
# Services
services:
# Create my service nomad, it's can be used by service `nomad@file`
nomad:
loadBalancer:
servers:
- url: https://{{ env "NOMAD_IP_traefik" }}:4646
# Adding my nomad CA for my service
serversTransports:
mytransport:
rootCAs:
- /certs/consul-ca.pem
# Middlewares
middlewares:
# Create a stripprefix to allo my dashboard on /traefik
# https://doc.traefik.io/traefik/middlewares/http/stripprefix
traefik:
stripprefix:
prefixes: /traefik
nomad:
stripprefix:
prefixes: /nomad
...
EOH
# My template location
destination = "/local/dynamic.yml"
}
template {
data = <<EOF
---
# Setup default endpoints
entryPoints:
# Enable http trafic on port 80
http:
address: ':80'
http:
# Redirect all my trafic on entryPoints https
redirections:
entryPoint:
to: https
scheme: https
permanent: true
# Enable https trafic on port 443
https:
address: ':443'
# Enable traefik consol on port 8081
traefik:
address: ':8081'
log:
level: INFO
# Enable acces log
accessLog: {}
# Enable api/dashboard
api: {}
# Enable healtthcheck endpoint
ping: {}
providers:
providersThrottleDuration: 5s
# Create my file provider for dynamic configuration
file:
watch: true
# Related with my previous template location
filename: '/local/dynamic.yml'
# Enable nomad provider
nomad:
refreshInterval: 5s
# Define nomad endpoint to scrape
endpoint:
address: https://{{ env "NOMAD_IP_traefik" }}:4646
# Require if you have a custom CA.
# The file location is related by
# * My volume shared "/etc/ssl/certs:/certs"
# * My tls nomad configuration
tls:
ca: /certs/consul-ca.pem
# Use a tag filter to enable traefik ingress
constraints: "Tag(`traefik.enable=true`)"
# Creaticate configuration
# acme = letsencrypt
# https://doc.traefik.io/traefik/https/acme/#the-different-acme-challenges
certificatesResolvers:
# My resolver name
route53:
acme:
# Set up email address used for your LE certificat
email: fake@p0l.io
# Add additional chain
preferredChain: 'ISRG Root X1'
# Define a cache storage (require if you destroy you container)
# This directory is related by a host volume
storage: /etc/acme/acme.json
# Use dns challenge with route 53 provider
dnsChallenge:
provider: route53
# Add a additional resolver
http:
acme:
email: fake@p0l.io
preferredChain: 'ISRG Root X1'
# Use a diffrent cache file
storage: /etc/acme/http.json
# User http challenge
httpChallenge:
entryPoint: http
EOF
destination = "local/traefik.yml"
}
# Create a environment variable configuration
template {
data = <<EOF
# Use my vault secret `from/vault/path`
{{ with secret "from/vault/path" }}
AWS_ACCESS_KEY_ID={{ .Data.data.id }}
AWS_SECRET_ACCESS_KEY={{ .Data.data.secret }}
AWS_REGION={{ .Data.data.region }}
{{ end }}
EOF
# Should be on a diffrent destination path
destination = "secrets/file.env"
# Define is a environment file
env = true
}
}
}
}
Une fois votre task en place:
https://nomad.p0l.io/traefik
qui contiendra votre accès au dashboard traefik (ou http://<ip>:8081
en cas de rescue)nomad job status ingress
, contiendra le status de votre jobnomad alloc fs -job ingress traefik/local
, vous permettras de lister et d’accéder en lecture à votre configurationnomad alloc fs -job ingress traefik/secrets
, listera vos secrets sans accès en lecturenomad service list
devrait vous retourner la list des 3 services nomad service info http
, l’accès httpnomad service info https
, l’accès httpsnomad service info ingress-ingress
, l’accès à la consol traefikCitizen sera notre premier job que l’on va exposer sur notre serveur. Il s’agit d’une registry terraform en nodejs. Pour nos besoins le stockage des datas et la base sqlite se feront sur le même volume.
Citizen job
job "citizen" {
datacenters = ["dc1"]
type = "service"
group "citizen" {
count = 1
volume "citizen" {
type = "host"
source = "citizen"
read_only = false
}
network {
port "citizen" {
to = 3000
}
}
service {
provider = "nomad"
tags = [
"citizen", "app",
"traefik.enable=true",
"traefik.http.routers.citizen.rule=(Host(`terraform.p0l.io`) && Method(`GET`)) || (Host(`terraform.p0l.io`) && ClientIP(`1.2.3.4`))",
"traefik.http.routers.citizen.tls=true",
"traefik.http.routers.citizen.tls.certresolver=http"
]
port = "citizen"
check {
type = "http"
name = "citizen"
path = "/health"
interval = "10s"
timeout = "5s"
check_restart {
limit = 2
grace = "20s"
}
}
}
task "citizen" {
driver = "docker"
env {
CITIZEN_DATABASE_TYPE = "sqlite"
CITIZEN_DATABASE_URL = "/citizen/data"
CITIZEN_STORAGE = "file"
CITIZEN_STORAGE_PATH = "/citizen/data"
}
config {
image = "ghcr.io/outsideris/citizen:0.5.2"
ports = ["citizen"]
}
resources {
cpu = 128
memory = 128
memory_max = 1024
}
volume_mount {
volume = "citizen"
destination = "/citizen/data"
}
restart {
attempts = 3
interval = "5m"
delay = "2m"
mode = "delay"
}
}
update {
max_parallel = 1
health_check = "checks"
min_healthy_time = "5s"
healthy_deadline = "30s"
auto_revert = true
canary = 0
}
}
}
Dans ce job plusieurs blocks sont ajoutés ou modifiés.
Dans un premier temps notre type de scheduler change. Sur le job d’ingress il est définit en tant que system
, il sera déployé systématiquement sur touts les clients et aura une priorité sur le déploiement. En étant sur service
, notre job indique que notre task sera continuellement en running et le nombre de container sera équivalent à ce que l’on définis dans count
.
type = "service"
Puis, notre network utilise le bridge local de Docker (par default) avec notre CNI. Ensuite, il n’y a pas de port de définit sur le host mais un port de référence du container (3000) qui correspond au port d’écoute du service citizen.
network {
port "citizen" {
to = 3000
}
}
En soit on n’a pas besoin de connaitre le port qui sera utilisé, ça sera le boulot de notre ingress controller de faire ça. Il est néanmoins possible de récupérer le port utilisé avec la commande nomad service info citizen-citizen
.
Ensuite, notre service dispose de tags qui servent à la configuration de notre ingress. Le provider nomad de treafik va lire ces tags afin de créer sa configuration. Les tags en place sont l’équivalent de la config yaml suivante:
http:
routers:
citizen:
rule: (Host(`terraform.p0l.io`) && Method(`GET`)) || (Host(`terraform.p0l.io`) && ClientIP(`1.2.3.4`))
tls:
certresolver: http
Avec cette configuration notre service sera disponible:
1.2.3.4
Enfin un certificat sera créé par un challenge http. Ce challenge sera pris en compte par le router acme-http@internal
et le service acme-http@internal
mit en place par la configuration traefik.
Un check de mon service est aussi en place sur le endpoint /health
de mes containers afin de s’assurer qu’il soit fonctionnel.
Plusieurs variables d’environnement sont en places avec le block env. Définis dans la tasks elle seront propres à mes containers.
env {
CITIZEN_DATABASE_TYPE = "sqlite"
CITIZEN_DATABASE_URL = "/citizen/data"
CITIZEN_STORAGE = "file"
CITIZEN_STORAGE_PATH = "/citizen/data"
}
Enfin un block update et restart sont en place pour le lifecycle de notre task/job.
...
restart {
attempts = 3
interval = "5m"
delay = "2m"
mode = "delay"
}
}
update {
max_parallel = 1
health_check = "checks"
min_healthy_time = "5s"
healthy_deadline = "30s"
auto_revert = true
canary = 0
}
Discourse est un forum écrit en Rails. La documentation et les procédures d’installation de l’app fournit pas l’éditeur ne sont pas très utilisable sur des infrastructure moderne (tout les services sont dans une image docker à buildé soit même). Heureusement Bitnami à fait une image docker plus légère et une Chart helm pour le déploiement dont je me suis gandement inspiré.
L’application se déploie en 3 parties.
Un Redis est nécessaire pour discourse. Il n’y a rien de très compliqué à effectuer pour ce dernier, mais deux nouveaux blocks et paramètres apparaisent:
Redis job
job "redis" {
datacenters = ["dc1"]
priority = 55
type = "service"
update {
max_parallel = 1
min_healthy_time = "10s"
healthy_deadline = "3m"
auto_revert = false
canary = 0
}
group "cache" {
count = 1
restart {
attempts = 10
interval = "5m"
delay = "25s"
mode = "delay"
}
ephemeral_disk {
size = 300
}
network {
port "redis" {
to = 6379
}
}
service {
provider = "nomad"
tags = ["redis", "cache"]
port = "redis"
check {
name = "redis"
type = "tcp"
interval = "10s"
timeout = "1s"
}
}
task "redis" {
driver = "docker"
config {
image = "redis:7.0.11-alpine3.18"
ports = ["redis"]
labels {
app = "discourse"
service = "cache"
}
}
resources {
cpu = 500
memory = 128
memory_max = 256
}
}
}
}
Ensuite, un PostgresQL est nécessaire en tant que base de données. Là non plus pas de grande subtilité. Néamoins il est possible de configurer son postgresql avec un combo assez intéressant nomad + vault + consul. Pour ma part je n’ai pas consul à disposition et mon vault n’est pas fait pour accéder à mon nomad mais l’inverse. Je suis donc passé par un script de bootstrap disponible avec l’image docker.
config {
image = "postgres:15.3-alpine3.18"
ports = ["postgresql"]
volumes = [
"local/bootstrap.sql:/docker-entrypoint-initdb.d/bootstrap.sql",
]
}
template {
data = <<EOF
{{ with secret "my/vault/path" }}
CREATE DATABASE discourse;
CREATE USER discourse WITH ENCRYPTED PASSWORD '{{ .Data.data.discourse }}';
GRANT ALL PRIVILEGES ON DATABASE discourse TO discourse;
ALTER DATABASE discourse OWNER TO discourse;
GRANT ALL ON schema public TO discourse;
{{ end }}
EOF
destination = "local/bootstrap.sql"
}
La mise en place de variable d’environnement par template sera également utilisé pour définir le mot de passe root. Le hcl complet:
Postgresql job
job "postgres" {
datacenters = ["dc1"]
type = "service"
priority = 60
group "database" {
count = 1
volume "data" {
type = "host"
source = "postgresql"
read_only = false
}
network {
port "postgresql" {
to = 5432
}
}
service {
provider = "nomad"
tags = ["postgresql", "database"]
port = "postgresql"
check {
name = "postgres"
type = "tcp"
interval = "10s"
timeout = "2s"
}
}
task "postgres" {
driver = "docker"
vault {
policies = ["nomad-postgres"]
}
config {
image = "postgres:15.3-alpine3.18"
ports = ["postgresql"]
volumes = [
"local/bootstrap.sql:/docker-entrypoint-initdb.d/bootstrap.sql",
]
}
template {
data = <<EOF
{{ with secret "my/vault/path" }}
CREATE DATABASE discourse;
CREATE USER discourse WITH ENCRYPTED PASSWORD '{{ .Data.data.discourse }}';
GRANT ALL PRIVILEGES ON DATABASE discourse TO discourse;
ALTER DATABASE discourse OWNER TO discourse;
GRANT ALL ON schema public TO discourse;
{{ end }}
EOF
destination = "local/bootstrap.sql"
}
template {
data = <<EOF
{{ with secret "my/vault/path" }}
POSTGRES_USER=root
POSTGRES_PASSWORD="{{ .Data.data.root }}"
{{ end }}
EOF
destination = "secret/file.env"
env = true
}
logs {
max_files = 5
max_file_size = 15
}
resources {
cpu = 500
memory = 256
memory_max = 512
}
volume_mount {
volume = "data"
destination = "/var/lib/postgresql/data"
}
}
}
}
Si vous souhaitez ajouter des utilisateurs ou bases de données, je vous conseille de mettre à jour le fichier de bootstrap pour de le reload en cli.
nomad alloc exec -i -t -task postgres -job postgres sh
/ $ psql -f /docker-entrypoint-initdb.d/bootstrap.sql
Il est également possible d’intéragir directement dans le postgresql avec la commande nomad alloc exec -i -t -task postgres -job postgres psql
.
Dans le cas où vous souhaiteriez mettre en place des backup de votre postgresql, je vous invite à lire cet article.
Enfin la dernière partie est l’application rails ainsi que le container sidekiq. En suivant la Chart Helm, j’ai mis en place un job contenant deux tasks séparées.
Il reste une configuration particulière à prendre en compte. Comme expliqué précédemment, mes services (hors mis l’ingress controller) ont des ports attribués dynamiquement. Pour mettre en place les variables d’environnements avec les bonnes valeurs il va falloir regarder les services nomad en place et récupérer les informations qui nous intéressent. Pour ça, et vus qu’on n’utilise pas Consul, on va utiliser nomadService
disponible dans en tant que template.
{{- range nomadService "postgres-database" }}
DISCOURSE_DATABASE_HOST="{{ .Address}}"
DISCOURSE_DATABASE_PORT_NUMBER="{{ .Port }}"
{{ end -}}
{{- range nomadService "redis-cache" }}
DISCOURSE_REDIS_HOST="{{ .Address}}"
DISCOURSE_REDIS_PORT_NUMBER="{{ .Port }}"
{{ end -}}
J’ai également mis en place grace = "10m"
dans le block check_restart, qui va permettre de laisser le temps à task de démarrer avant de la considérer comme unhealthy.
Le hcl complet donnera quant à lui:
Discourse job
job "discourse" {
datacenters = ["dc1"]
type = "service"
group "discourse" {
count = 1
vault {
policies = ["nomad-postgres", "apps"]
}
volume "discourse" {
type = "host"
source = "discourse"
read_only = false
}
network {
port "discourse" {
to = 3000
}
}
service {
provider = "nomad"
tags = [
"discourse", "app",
"traefik.enable=true",
"traefik.http.routers.discourse.rule=(Host(`discourse.p0l.io`) && ClientIP(`1.2.3.4`))",
"traefik.http.routers.discourse.tls=true",
"traefik.http.routers.discourse.tls.certresolver=route53"
]
port = "discourse"
check {
type = "http"
name = "rails"
path = "/srv/status"
interval = "10s"
timeout = "5s"
}
check_restart {
limit = 3
grace = "10m"
}
}
task "sidekiq" {
driver = "docker"
config {
image = "bitnami/discourse:3.0.4"
command = "/opt/bitnami/scripts/discourse-sidekiq/run.sh"
}
env {
# Apps
DISCOURSE_HOST = "discourse.p0l.io"
DISCOURSE_ENABLE_HTTPS = "no"
DISCOURSE_ENV = "production"
# Database
DISCOURSE_DATABASE_NAME = "discourse"
DISCOURSE_DATABASE_USER = "discourse"
# Redis
ALLOW_EMPTY_PASSWORD = "yes"
}
template {
data = <<EOH
{{ with secret "my/vault/path" }}
DISCOURSE_EMAIL="{{ .Data.data.mail }}"
DISCOURSE_USERNAME="{{ .Data.data.name }}"
DISCOURSE_PASSWORD="{{ .Data.data.password }}"
{{ end -}}
{{ with secret "my/vault/path" }}
DISCOURSE_DATABASE_PASSWORD="{{ .Data.data.discourse }}"
{{ end }}
{{- range nomadService "postgres-database" }}
DISCOURSE_DATABASE_HOST="{{ .Address}}"
DISCOURSE_DATABASE_PORT_NUMBER="{{ .Port }}"
{{ end -}}
{{- range nomadService "redis-cache" }}
DISCOURSE_REDIS_HOST="{{ .Address}}"
DISCOURSE_REDIS_PORT_NUMBER="{{ .Port }}"
{{ end -}}
EOH
destination = "secrets/file.env"
env = true
}
volume_mount {
volume = "discourse"
destination = "/bitnami/discourse"
}
resources {
cpu = 250
memory = 512
memory_max = 1024
}
}
task "rails" {
driver = "docker"
env {
BITNAMI_DEBUG = "true"
# Apps
DISCOURSE_PRECOMPILE_ASSETS = "no"
DISCOURSE_HOST = "discourse.p0l.io"
DISCOURSE_ENABLE_HTTPS = "no"
DISCOURSE_ENV = "production"
# Database
DISCOURSE_DATABASE_NAME = "discourse"
DISCOURSE_DATABASE_USER = "discourse"
# Redis
ALLOW_EMPTY_PASSWORD = "yes"
}
template {
data = <<EOH
{{ with secret "my/vault/path" }}
DISCOURSE_EMAIL="{{ .Data.data.mail }}"
DISCOURSE_USERNAME="{{ .Data.data.name }}"
DISCOURSE_PASSWORD="{{ .Data.data.password }}"
{{ end -}}
{{ with secret "my/vault/path" }}
DISCOURSE_DATABASE_PASSWORD="{{ .Data.data.discourse }}"
{{ end -}}
{{- range nomadService "postgres-database" }}
DISCOURSE_DATABASE_HOST="{{ .Address}}"
DISCOURSE_DATABASE_PORT_NUMBER="{{ .Port }}"
{{ end -}}
{{- range nomadService "redis-cache" }}
DISCOURSE_REDIS_HOST="{{ .Address}}"
DISCOURSE_REDIS_PORT_NUMBER="{{ .Port }}"
{{ end -}}
EOH
destination = "secrets/file.env"
env = true
}
config {
image = "bitnami/discourse:3.0.4"
ports = ["discourse"]
}
resources {
cpu = 800
memory = 512
}
volume_mount {
volume = "discourse"
destination = "/bitnami/discourse"
}
restart {
attempts = 3
interval = "1m"
delay = "2m"
mode = "delay"
}
}
update {
max_parallel = 1
min_healthy_time = "5s"
healthy_deadline = "5m"
auto_revert = false
canary = 0
}
}
}
Nomad s’affiche comme plus simple dans sa mise en place que Kubernetes. Bien qu’il soit vrai que l’installation reste facile d’accès, dès que l’on souhaite mettre en place un service ça devient tout de suite tout aussi complexe. La gestion des CNI et CSI restent assez peu documenté et c’est assez compliqué de trouver des jobs tout prêt.
À mes yeux le vrai problème se trouve ici; le manque de capitalisation de jobs. Bien qu’il semble y avoir une volonté de partir sur ce sujet avec Nomad Pack, on est très loin de ce qu’on trouve en chart Helm.
C’est également une grosse déception sur le combo ingress controller/génération de certificats qui sont la base pour mettre à disposition un service. Heureusement que Traefik est là pour nous offrir ces features.
Évidemment je n’avais pas utilisé Consul qui semble nous offrir tout un tas de possibilités intéressantes sur la consommation et l’exposition de nos services. Il n’est d’ailleurs pas évident de trouver des documentations sans Consul.