Introduction

Chose promie, chose due ! J’écris un article sur mes pérégrinations en informatique.

Cette fois, je fais suite à l’état des lieux 2022 de mon espace numérique. Certains de mes services/sites nécessitent régulièrement d’être mis à jour. Pour cela je souhaite utiliser Cron, un service sous GNU/Linux qui permet de planifier le lancement de script(s) à des fréquences définies.

Comme je souhaite utiliser Docker, je me suis demandé s’il était possible de créer un service dédié avec Cron, Docker et Alpine.

Cet article explique quelques règles que j’ai dû suivre pour y parvenir. Il vous faudra a minima connaître Docker, voire Docker Compose pour comprendre quelque chose. À la rigueur vous connaissez déjà Cron et souhaitez l’utiliser sous Docker.

Nous allons d’abord réflechir sur le sujet. Puis nous utiliserons l’exemple d’un site pour expliquer la solution choisie avant de conclure sur cette histoire.

Pour les pressés, j’ai fais un chapitre « En bref » à la fin de cet article.

Montre de poignet avec un stylo

Photo trouvée sur le profil de Duc Ly sur Flickr sous licence CC BY-SA 2.0.

Réflexion sur le sujet

Ce que nous souhaitons faire, c’est lancer un service, par exemple almanax, et le mettre à jour régulièrement avec une tâche dans le service cron. Je vais expliquer un peu ma réflexion, si vous voulez entrer plus rapidement dans le vif du sujet, rendez-vous au chapitre suivant.

Tout d’abord il faut savoir que l’environnement choisi est Docker. Un système qui permet, à partir d’une image de générer plusieurs conteneurs ayant des points de montage différents pour changer les données qui s’y trouvent.

Docker propose plusieurs bonnes pratiques, parmi lesquelles :

  • ne lancer qu’un seul processus/service dans chaque conteneur
  • réutiliser les images ou les parties d’images au maximum pour réduire à la fois la maintenance des images/conteneurs, le travail effectué et la place système utilisée pour les images de base

Sachant que nous ne lançons qu’un seul processus par conteneur Docker, et que je souhaite utiliser Cron, je vais devoir lancer cron dans un autre service. Concrètement, Docker propose Docker Compose qui est un outil permettant de lancer plusieurs services à la fois, avec des dépendances entre eux (si besoin), une description des points de montage, des commandes à lancer sur chaque service, les ports ouverts, etc.

On peut également choisir l’image qu’on souhaite utiliser pour chaque service.

Certes nous allons lancer Cron dans un service à part, pour lancer les scripts que notre autre service a besoin pour mettre à jour ce dernier - je pense notamment à un script qui recompile et reconstitue mes pages webs statiques -, mais nous allons devoir avoir accès à notre service almanax qui contient la logique principale du site.

Pour cela, l’idée est de modifier l’image de almanax pour qu’elle contienne elle aussi cron. Ainsi on pourra lancer la même image sous Docker Compose, avec une commande de lancement différente.

On utilise des services Docker. Chacun ayant déjà leur propre image sous Alpine. Ainsi, chaque image possède déjà Cron inclut dans Alpine.

J’allais dire qu’une image vaut mieux que 1 000 mots, mais là nous allons donner un exemple avec Docker Compose pour comprendre de quoi il s’agit.

Mise en place, exemple avec l’Almanax

Peut-être le savez-vous déjà : j’ai crée une page statique qui liste une quinzaine de quêtes de l’Almanax dans un jeu nommé Dofus.

Cet outil est un de mes services fourni aux utilisateurs, mais également un projet Open Source, dofus-almanax que je fournis via Github. C’est dans ce dépôt Github que vous trouverez la plupart des fichiers nécessaires au bon lancement d’un service Cron que nous allons décrire.

On va étudier le fichier docker-compose.yml suivant :

version: '3'
services:
  generator:
    build: .
    image: dofus-almanax:0.2
    volumes:
      - almanax_public:/opt/almanax/public:rw
      - almanax_data:/opt/almanax/dl:rw
  web:
    image: caddy:2-alpine
    restart: always
    depends_on:
      - generator
    volumes:
      - almanax_public:/usr/share/caddy:ro
      - ${PWD}/Caddyfile:/etc/caddy/Caddyfile
    ports:
      - 8888:80
  cron:
    image: dofus-almanax:0.2
    restart: always
    entrypoint: /usr/sbin/crond
    command: ["-f", "-l", "2", "-L", "/dev/stdout"]
    volumes:
      - almanax_public:/opt/almanax/public:rw
      - almanax_data:/opt/almanax/dl:rw

volumes:
  almanax_public:
  almanax_data:

Nous avons 3 services :

  • generator : qui génère le site web au lancement, suite à quoi il s’éteint,
  • web : un service pour mettre à disposition les fichiers statiques générés par generator,
  • cron : un service pour mettre à jour régulièrement les fichiers statiques.

La particularité de la solution :

  • generator et cron utilisent la même image : dofus-almanax:0.2
  • ces deux services n’ont cependant pas la même commande au lancement : l’un prend la commande par défaut de l’image dofus-almanax:0.2, l’autre lance spécifiquement cron
  • pour le service cron, il spécifie avec le mot clé entrypoint d’utiliser /usr/sbin/crond (chemin absolu du binaire sous Alpine) et donne les paramètres -f -l 2 -L /dev/stdout

Que font les paramètres ?

  • -f définit de lancer le service en foreground, nécessaire sous Docker, sinon cela ne fonctionne pas
  • -l 2 définit un niveau de journalisation, habituellement c’est 8 (Cf. une explication des niveaux crond sous Stackoverflow
  • -L /dev/stdout permet d’avoir une sortie à l’écran dans Docker Compose

Ça, c’est les éléments de base. Cependant comment fonctionne la mise à jour, la configuration de cette dernière, etc.?

Fonctionnement de Cron dans l’image dofus-almanax:0.2

Nous l’avions dit, l’idée est de tout mettre dans l’image dofus-almanax:0.2 (à la fois nos scripts, nos fichiers et crond). Mais comment configurer le service Cron pour lui indiquer la fréquence à laquelle mettre à jour et comment mettre à jour nos fichiers ?

Regardons l’image Docker, écrite sous le fichier Dockerfile suivant :

FROM alpine:3.16

RUN apk update && \
    apk add --no-cache \
        py3-lxml \
	py3-mechanize \
	tzdata && \
    rm -rf /var/cache/apk/*

ENV TZ=Europe/Paris

WORKDIR /opt/almanax

VOLUME /opt/almanax/public
VOLUME /opt/almanax/dl

ENTRYPOINT ["python3", "almanax_next_week.py"]
CMD ["/opt/almanax/public/index.html"]

COPY ./src /opt/almanax
COPY ./crontabs /etc/crontabs/root

RUN chown root:root /etc/crontabs/root && \
    chmod 600 /etc/crontabs/root

COPY ./cron_scripts/generate.sh /opt/generate

Étudions ce fichier dans les grandes lignes concernant le service Cron notamment.

  1. Nous partons d’une image Alpine 3.16 qui contient déjà crond (le service Cron)
  2. Nous installons tzdata qui nous permettra de choisir correctement un fuseau horaire
  3. ENV TZ=Europe/Paris définit notre fuseau horaire, histoire de lancer le script au moment où nous nous y attendons
  4. Le fichier le plus important, cronbtabs est copié vers /etc/crontabs/root, la ligne COPY ./crontabs /etc/crontabs/root est déterminante pour remplacer le fichier du service crond d’Alpine Linux
  5. On copie aussi un script generate.sh dont nous parlerons après
  6. Ce même fichier nécessite des permissions spécifiques, notifiée vers la fin du fichier Dockerfile :
RUN chown root:root /etc/crontabs/root && \
    chmod 600 /etc/crontabs/root

Que contient le fichier crontabs et generate.sh ?

La configuration du service crond

Sous Alpine, le fichier utilisé pour configurer les éléments à lancer par crond est /etc/crontabs/root. Dans le chapitre précédent nous parlions de comment le remplacer, voici désormais son contenu :

5       0       *       *       *       /bin/sh /opt/generate
  1. Nous voyons que nous utilisons effectivement le script generate.sh - qui a d’ailleurs été renommé generate.
  2. Nous utilisons /bin/sh devant notre script :
    • c’est un chemin absolu vers le binaire sh
    • et nous utilisons sh, pas bash (car bash n’est, à défaut, pas délivré sous Alpine Linux)
  3. Nous utilisons des caractères spécifique, formatés spécifiquement pour dire que nous lançons le service à 00h05 du matin

Pour générer un fichier compatible avec crond, je vous suggère Crontab Generator.

Il va falloir appliquer quelques règles pour être sûr que tout cela fonctionne évidemment.

En bref, les règles à appliquer pour crond

Voici les règles à retenir pour rédiger le fichier que le service crond va étudier :

  • 7 espaces entre chaque élément défini dans le crontab (le fichier se nomme ainsi)
  • utiliser /bin/sh sous Alpine pour lancer un script, puis le nom de votre script
  • ne pas mettre de point dans le script qu’on lance (mettre par exemple generate au lieu de generate.sh)
  • le fichier /etc/crontabs/root doit appartenir à root (root:root) et avoir les permissions 600 (chmod 600)
  • utilisez -L /dev/stdout sous votre Docker Compose pour avoir une sortie lisible et -l 2 pour plus de logs, mais -l 8 suffit

Ce qui fait déjà pas mal de choses à savoir !

Conclusion

Bien que la réflexion ait été longue (et croyez moi j’ai pris plusieurs jours pour tester au fur et à mesure ce que je souhaitais), nous avons réuni les seules règles à appliquer pour que cela fonctionne, tout en trouvant une solution sympathique qui a plusieurs avantages :

  • le service Cron accède à tous les fichiers communs de notre service initial
  • le service Cron accède également à tous les scripts utiles de notre service initial
  • il suffit de changer le script d’entrée de l’image initiale pour lancer Cron
  • on a peu de changement à faire pour utiliser Cron finalement !

L’inconvénient reste cependant qu’il faut pouvoir modifier l’image initiale. Si nous n’avions pas la possibilité de faire ça, j’imagine qu’on devrait créer un point de montage commun entre le service initial et le service cron pour accéder à la fois aux fichiers mais aussi aux binaires… ce qui est bien plus complexe/casse-tête !

Liens utiles

Je me suis grandement inspiré, pour mes pérégrinations sur Cron dans Alpine sous Docker par :