Dans le cadre du défi “100 jours de DevOps” de la plateforme KodeKloud, une des tâches fournies consistait à installer cronie sur trois serveurs et à créer la même tâche cron pour l’utilisateur root :

The Nautilus system admins team has prepared scripts to automate several day-to-day tasks. They want them to be deployed on all app servers in Stratos DC on a set schedule. Before that they need to test similar functionality with a sample cron job. Therefore, perform the steps below:
a. Install cronie package on all Nautilus app servers and start crond service.
b. Add a cron */5 * * * * echo hello > /tmp/cron_text for root user.

À la lecture de l’énoncé j’entrevoyais déjà la procédure attendue, manuelle et répétitive :

  • récupérer les identifiants du premier serveur et s’y connecter en SSH
  • installer le paquet cronie
  • vérifier l’état du service
  • éditer le crontab root
  • déconnexion et répétition avec les deux serveurs suivants.

J’ai une horreur viscérale de ce genre de répétitions robotiques sans intérêt ; ça fait crier quelque chose au fond de moi : “Je ne suis pas une machine !” Car c’est à la machine que revient d’effectuer ce genre de tâches : le mot informatique n’est-il pas la contraction de information automatique après tout ?

Ce qui m’intéresse dans l’approche DevOps c’est exactement ça : mettre un terme à cette manière manuelle et fastidieuse de procéder, et parachever enfin la raison d’être de l’informatique. On veut de l’industrialisation, de l’automatisation, de l’Infra-as-Code enfin !


J’ai donc commencé par envisager mes alternatives :

  • installer Ansible et créer un playbook : overkill pour un lab unique, entre l’installation et l’écriture ça aurait introduit une complexité inutile à ce stade ; je suis sûr que je dois pouvoir faire cela avec les outils à ma disposition.
  • mon petit chouchou, GNU parallel : pas installé dans l’environnement du lab, suppose une connexion par clé SSH pour se passer de mot de passe.
  • reste le script Bash traditionnel.

J’ai commencé par établir la procédure opératoire précise pour la traduire en commandes impératives :

  • pour se connecter à chaque serveur : ssh user1@server1, acceptation de la clé d’hôte puis entrée du mot de passe de l’utilisateur

  • exécution des trois commandes pour répondre à l’énoncé :

    sudo dnf install -y cronie
    sudo systemctl start crond # pas nécessaire car le service est démarré automatiquement
    echo "*/5 * * * * echo hello > /tmp/cron_text" | sudo tee -a /etc/crontab
    

Pour automatiser cela :

  1. Je peux utiliser l’options -o StrictHostKeyChecking=no pour contourner l’acceptation de la clé d’hôte
  2. Je peux recourir à sshpass pour fournir le mot de passe de manière non-interactive (par miracle il est installé sur la machine du lab)
  3. Une petite recherche m’apprend que sudo accepte l’option -S, --stdin pour “lire le mot de passe depuis l’entrée standard”.

Tout ceci mis ensemble donne pour le premier serveur :

sshpass -p Ir0nM@n ssh -o StrictHostKeyChecking=no tony@stapp01 \
    'sudo -S dnf install -y cronie <<< "Ir0nM@n" &&\
     echo "*/5 * * * * echo hello > /tmp/cron_text" | sudo tee -a /etc/crontab'

Ou sur une seule ligne :

sshpass -p Ir0nM@n ssh -o StrictHostKeyChecking=no tony@stapp01 'sudo -S dnf install -y cronie <<< "Ir0nM@n" && echo "*/5 * * * * echo hello > /tmp/cron_text" | sudo tee -a /etc/crontab'

Ok, c’est un peu hacky mais à ma bonne surprise ça fonctionne !

La deuxième étape a été d’extraire les parties communes dans des variables :

SSH_OPTIONS='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR'
COMMAND='sudo -S dnf install -y cronie <<< $PASSWORD && echo "*/5 * * * * echo hello > /tmp/cron_text" | sudo tee -a /etc/crontab'

sshpass -p Ir0nM@n ssh $SSH_OPTIONS tony@stapp01 $COMMAND
sshpass -p Am3ric@ ssh $SSH_OPTIONS steve@stapp02 $COMMAND
sshpass -p BigGr33n ssh $SSH_OPTIONS banner@stapp03 $COMMAND

Restait à intégrer le mot de passe comme variable, utilisée à la fois pour se connecter en SSH et pour le premier sudo une fois connecté. Le petit défi est :

  1. de faire se correspondre le binôme utilisateur@machine avec le bon mot de passe, et de les changer à chaque itération
  2. de passer le mot de passe à l’intérieur de la variable contenant la commande…

À ce stade je voyais bien qu’il ne me manquait plus qu’un petit effort pour parvenir à automatiser cette procédure et qu’elle fonctionne sur les trois serveurs.

Mon intuition d’une boucle for ne convient pas : même en en imbriquant plusieurs les différents paramètres associés ne se conjugueraient pas correctement.

Attendez un instant, “paramètres associés”…Associatif… Comme un tableau associatif ?

Voilà, c’est ça qu’il me faut : établir un tableau associatif ‘utilisateur-serveur-mot de passe’ et itérer dessus pour exécuter les commandes !

N’utilisant pas souvent cette forme de variable dans Bash, c’était le moment pour moi de recourir à un LLM pour accélérer la conception du script. Je vous laisse admirer le résultat :

SSH_OPTIONS='-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR'

declare -A HOSTS=(
  ["tony@stapp01"]="Ir0nM@n"
  ["steve@stapp02"]="Am3ric@"
  ["banner@stapp03"]="BigGr33n"
)

for USERHOST in "${!HOSTS[@]}"; do
  PASSWORD="${HOSTS[$USERHOST]}"
  COMMAND="echo '$PASSWORD' | sudo -S dnf install -y cronie &&
           sudo systemctl enable --now crond &&
           echo '*/5 * * * * root echo hello > /tmp/cron_text' | sudo tee -a /etc/crontab"
  echo "→ Deploying to $USERHOST..."
  sshpass -p "$PASSWORD" ssh $SSH_OPTIONS "$USERHOST" "$COMMAND"
done

Malin : c’est à l’intérieur de l’itération que se fait l’affection de la variable PASSWORD, en adéquation avec le binôme utilisateur-machine sur lequel on est actuellement, et ensuite la variable COMMAND intègre le mot de passe variabilisé en inversant l’utilisation des guillemets simples et doubles. Du propre dont j’apprends beaucoup !

Voilà, c’est ici que s’achève ce récit d’automatisation. On termine avec quelques titres accrocheurs suggérés par ce même LLM :

Fini le copier-coller et le SSH manuel, on peut tout à fait orchestrer plusieurs serveurs sans Ansible ni configuration complexe !

Pourquoi taper la même suite de commandes X fois quand Bash peut le faire pour vous ?

Apprenez à vous servir de la paresse comme d’un levier de développement !

😁

Happy hacking!

Edit 2026-04-01 : après m’être resservi plusieurs fois de ce script, j’ai amélioré le système pour que l’utilisation de sudo sur les serveurs puisse se faire sans mot de passe. J’en ai profité pour mettre en place la connexion SSH par clé automatisée sans mot de passe.

Le résultat est une suite de commandes que je copie-colle directement dans le terminal du lab :

# Générer une paire de clés SSH
ssh-keygen -t ed25519 -f "$HOME/.ssh/id_ed25519" -N ""

# Désactiver la vérification stricte des clés d'hôte
sudo -S sed -i "/StrictHostKeyChecking/s/^#//;s/ask/no/" /etc/ssh/ssh_config <<< mjolnir123

# tableau associatif avec les identifiants des 3 serveurs applicatifs
declare -A HOSTS=(
  ["tony@stapp01"]="Ir0nM@n"
  ["steve@stapp02"]="Am3ric@"
  ["banner@stapp03"]="BigGr33n"
)

# boucle pour copier la clé SSH publique sur les 3 hôtes et configurer sudo en NOPASSWD
for USERHOST in "${!HOSTS[@]}"; do
  PASSWORD="${HOSTS[$USERHOST]}"
  sshpass -p "$PASSWORD" ssh-copy-id "$USERHOST"
  ssh "$USERHOST" "echo '$PASSWORD' | sudo -S bash -c 'echo %wheel ALL=\(ALL\) NOPASSWD: ALL > /etc/sudoers.d/nopasswd'"
done

J’ai créé un Gist pour faciliter le partage du script. Commentaires et améliorations bienvenus !