Linux things 🐧

un blog sur les technologies des logiciels libres et autres digressions

Nomad batch

Fri, 16 Feb 2024 19:27:00 +0100
# nomad   # docker   # semaphore   # ansible  

Voici un nouvel article sur Nomad qui fait suite à Une infra avec Nomad, Consul et Tailscale.

Batch

En plus du type service que l’on a vu précédemment, Nomad propose le type batch qui est idéal pour lancer un tâche régulièrement via un cron intégré.

EXEC

La dernière fois j’utilisais le driver docker, mais c’est complètement overkill pour lancer une simple tâche batch. Or Nomad propose le driver exec, et exec c’est un bon vieux chroot ! Léger et rapide donc. On va ici faire quelque chose d’utile, programmer une tâche qui fait un dump régulier d’une base de données qui tourne dans un conteneur Docker quelque part sur un noeud Nomad.

Pour cela je me suis fortement appuyé sur cette doc : How to backup Postgres database with Nomad, que j’ai simplifié car je dump simplement dans un répertoire du host et que je n’utilise pas encore vault (gruik!), mais c’est prévu. De plus il utilise le driver raw_exec plutôt que exec sans doute pour pouvoir appeler Docker.

Comme exemple je vais utiliser Semaphore UI, qui est une superbe alternative à AWX, écrit en Go bien sûr (whatelse ?).

Semaphore UI

Voici le fichier hcl que j’utilise pour instancier Semaphore avec sa base Postgresql

semaphore.hcl

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

  group "app" {
    count = 1

    network {
      mode = "bridge"
      port "http" {
        to     = 3000 # container port the app runs on
        static = 80   # host port to expose
      }
      port "postgresql" {
        to = 5432 # container port the app runs on
      }
    }

    task "web" {
      driver = "docker"

      constraint {
        attribute = "${attr.unique.hostname}"
        value     = "vm-semaphore"
      }

      env {
        SEMAPHORE_DB_USER = "semaphore"
        SEMAPHORE_DB_PASS = "PASS"
        SEMAPHORE_DB_HOST = "127.0.0.1"
        SEMAPHORE_DB_PORT = "5432"
        SEMAPHORE_DB_DIALECT = "postgres"
        SEMAPHORE_DB = "semaphore"
        SEMAPHORE_PLAYBOOK_PATH = "/tmp/semaphore/"
        SEMAPHORE_ADMIN_PASSWORD = "PASS"
        SEMAPHORE_ADMIN_NAME = "admin"
        SEMAPHORE_ADMIN_EMAIL = "admin@localhost"
        SEMAPHORE_ADMIN = "admin"
        SEMAPHORE_ACCESS_KEY_ENCRYPTION = "TOKEN"
        SEMAPHORE_TELEGRAM_ALERT = true
        SEMAPHORE_TELEGRAM_CHAT = "CHATID"
        SEMAPHORE_TELEGRAM_TOKEN = "TOKEN"
        http_proxy = "http://user:PASS@IP_TAILSCALE_NODE1:3128"
        https_proxy = "http://user:PASS@IP_TAILSCALE_NODE1:3128"        
      }

      config {
        image = "semaphoreui/semaphore:latest"
        ports = ["http"]
      }

        resources {
         cpu = 1000
         memory = 2000
        }

        service {
           name = "semaphore"
           tags = ["global", "app"]
           provider = "consul"
           port = "http"

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



    task "postgresql" {
      driver = "docker"

      constraint {
        attribute = "${attr.unique.hostname}"
        value     = "vm-semaphore"
      }

      env {
        POSTGRES_USER = "semaphore"
        POSTGRES_PASSWORD = "PASS"
        POSTGRES_DB = "semaphore"
      }


      config {
        image = "postgres:16"

        mounts = [
           {
                type = "volume"
                target = "/var/lib/postgresql/data"
                source = "semaphore-postgresql"
            },
          {
                type = "bind"
                target = "/dump"
                source = "/volume/dump/semaphore"
                readonly = false
                bind_options = {
                  propagation = "rshared"
                }                
            }            
        ]


        ports = ["postgresql"]

      }

        resources {
          cpu = 1000
          memory = 1000
        }


        service {
          name = "semaphore-postgresql"
          provider = "consul"
          port = "postgresql"

          tags = ["alloc=${NOMAD_ALLOC_ID}"]           
        }
      
    }


  }
}

On a donc 2 conteneurs positionnés sur la même VM vm-semaphore. A noter que via la directive mode = "bridge" semaphore communique avec sa base de donnée sur l’interface localhost. Il y a dans ce hcl 3 paramètres importants pour que le script batch puisse fonctionner :

  • le nom du service name = "semaphore-postgresql"
  • le tag tags = ["alloc=${NOMAD_ALLOC_ID}"]
  • le montage de type bind vers un répertoire /dump sur le host

Le service s’enregiste auprès de Consul, ce qui va permettre au serveur Nomad de l’interroger pour récupérer le contenu du tag associé au service semaphore-postgresql. Le tag contient un identifiant unique créé par Nomad lors de la création du conteneur et grâce à cet identifiant Nomad va pouvoir lancer le dump directement dans le conteneur Postgresql.

Le script batch

semaphore-db-backup.hcl

job "semaphore-db-backup" {
  datacenters = ["dc1"]
  type        = "batch"

  periodic {
    crons = ["0 20 * * *"]
    time_zone = "Europe/Paris"
    prohibit_overlap = true
  }

  group "db-backup" {  
    task "postgres-backup" {
      driver = "exec"

      config {
        command = "/bin/bash"
        args    = ["local/script.sh"]
      }

      template {
        data        = <<EOH
        set -e
        DATE_BIN=$(command -v date)
        DATE=`${DATE_BIN} +%d-%m-%Y---%H-%M-%S`

        nomad alloc exec -task postgresql $DB_ALLOC_ID \
        /bin/bash -c "PGPASSWORD=PASS PGUSER=semaphore PGDATABASE=semaphore pg_dump --compress=4 > /dump/semaphore_${DATE}.dump.gz && /bin/chown 1001:1001 /dump/semaphore_${DATE}.dump.gz"
        EOH
        destination = "local/script.sh"
      }

      template {
        data = <<EOH
		#### GRUIK ! ######
		NOMAD_TOKEN="TOKEN"
		###################
		# as service 'semaphore-postgresql' is registered in Consul
		# we want to grab its 'alloc' tag
		{{- range $tag, $services := service "semaphore-postgresql" | byTag -}}
		  {{if $tag | contains "alloc"}}
		    {{$allocId := index ($tag | split "=") 1}}
		    DB_ALLOC_ID="{{ $allocId }}"
		  {{end}}
		{{end}}
		EOH
        destination = "secrets/file.env"
        env         = true        
      }
      resources {
        cpu    = 200
        memory = 200
      }
    }
  }
}

Et voilà le script qui sera instancié comme d’habitude par un nomad job run semaphore-db-backup.hcl

La section periodic est évidente, elle permet de le lancer tous les jours à 20h00.
On spécifie ensuite le driver exec et ce qu’il doit lancer, ici bash avec un script.sh dans le répertoire local du chroot. Le script.sh n’existe pas encore, il est décrit ensuite dans une première section template. C’est un simple bash qui fait un pg_dump suivit par un chown pour attribuer le fichier à l’utilisateur ansible sur mon host. En effet c’est une tâche ansible lancé par la suite par Semaphore qui va envoyer le dump.gz quelque part.
La partie importante est nomad alloc exec -task postgresql $DB_ALLOC_ID car c’est cette commande qui va instancier la commande bash dans le conteneur Postgresql et ce où que soit le conteneur dans la grappe Nomad.
Le deuxième template permet d’initialiser les variables d’environnement, interroge Consul pour extraire du service semaphore-postgresql le tag qui contient alloc afin d’extraire le NOMAD_ALLOC_ID et de créer la variable $DB_ALLOC_ID. Tout cela est stocké dans un fichier secrets/file.env dans le chroot et sera utilisé par le local/script.sh.
Si vous n’utilisez pas les ACL il n’y a pas la NOMAD_TOKEN, sans doute dans un prochain article je montrerai comment l’initialiser avec vault.

Si tout va bien vous devriez avoir tous les soirs un précieux semaphore_${DATE}.dump.gz à sauvegarder précieusement car il contient tous vos inventaires, tâches et planifications.

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