Pourquoi exporter ses conteneurs ?

La conteneurisation c'est extrêmement pratique pour déployer des services presque "clé en main". Mais qu'en est-il de la sécurité des données et de la résilience des services lorsqu'il n'y a pas de cluster ?

Pour la petite histoire, j'ai mis en place pour un projet un forum basé sur NodeBB. Cela ne faisait que quelques mois que j'avais découvert docker et il m'a apparu évident de le déployer ainsi.
Tout fonctionne pour le mieux, jusqu'au jour où une mise à jour de l'image semble perturber le fonctionnement du forum. Celui-ci détecte à chaque lancement du conteneur docker qu'il s'agit d'une nouvelle installation et refuse de lire le fichier de configuration que je lui donne.
En résultat, je me suis retrouvé avec un forum qui n'arrivait pas à lire la base de données. J'ai alors été contraint de le réinstaller.

C'est à ce moment là que je me suis posé cette question : comment mettre en place un système de rallback, permettant d'annuler une mise à jour qui n'a pas fonctionné par exemple ?

Comment ?

1 - La théorie

J'ai travaillé sur un petit script qui peut doit être paufiné. Mais il s'agit là d'une base fonctionnelle.
L'idée est simple. Avant d'arrêter un conteneur, de le supprimer puis d'en créer un nouveau pour appliquer l'image précédemment téléchargée, il faut effectuer une suite d'actions permettant une restauration de l'état actuel après la mise à jour (en cas de problème).

  • commit du conteneur
  • Exporter l'image créée pour la sauvegarder dans un dossier spécifique
  • Sauvegarder le dossier local dans lequel se trouvent les données stockées par le conteneur

Après ces trois points, nous avons un dossier contenant l'image et ses données.

2 - La pratique

Je commence par récupérer la date date=$(date +"%d.%m.%y") que j'utiliserai pour exporter mes conteneurs. Ensuite, j'utilise une fonction que j'ai créée pour sauvegarder le conteneur :

function commit {

  if [ -z $1 ]; then
    echo -e "\n\tProbleme avec la fonction commit : il manque les arguments"
    return 0
  else
    container=$1
  fi

  if [ -z $2 ]; then
    nom_save=$container
  else
    nom_save=$2
  fi

  docker commit --author="Louis MILCENT" --message="Sauvegarde du $date" $container save/$nom_save"_"$date
  docker save save/$nom_save"_"$date > /home/1_save/images/$nom_save"_"$date".tar"

}

Les deux dernières lignes permettent de créer une nouvelle image à partir du conteneur actuel, puis d'exporter cette dernière au format tar dans un dossier.
J'utilise la date du jour au format jour.mois.année pour automatiquement dater les images.

J'utilise un système de log pour garder une trace des manipulations effectuées. touch permet de créer le fichier s'il n'existe pas et de mettre à jour sa date de dernier accès.

log='/home/louis/logs/forum.log'  
touch $log  

Partie du script qu'il faut améliorer : la détection d'une mise à jour. Actuellement, je tente de récupérer la valeur de temps retournée par docker via docker images en tronquant le résultat. Mais ce n'est pas assez précis. S'il y a des idées, je suis preneur !

└─ $ docker images
REPOSITORY                               TAG                 IMAGE ID            CREATED             SIZE  
benlubar/nodebb                          latest              0c40656c90c7        6 days ago          771 MB  

Avec mon système, j'obtiens au final le 6. Mais il suffit que deux images soient récupérées via ce filtre et tout est faussé.

└─ $ docker images | grep benlubar | cut -c 82-83
6  

Je mets ensuite l'image à jour (docker pull benlubar/nodebb:latest). Puis je renouvelle ma précédente commande pour récupérer la valeur de temps, qui devrait avoir changé. Si c'est bien le cas, alors l'image a été modifiée.
J'appelle ma fonction pour commit et exporter mon conteneur : commit <nom du conteneur> <nom donné à la sauvegarde>. Si un seul paramètre est passé, le nom de sauvegarde sera celui du conteneur.

commit nodebb  

Je sauvegarde ensuite le dossier des données, car j'utilise des volume lorsque je crée mon image.

sudo cp -r /home/nodebb /home/1_save/nodebb_$date  

Enfin, pour appliquer la mise à jour, j'arrête, je supprime puis je crée un nouveau conteneur.

docker stop nodebb  
docker rm nodebb  
sh lancement_nodebb.sh           # Contient la commande docker run [...]  

Pour terminer je vérifie si le service s'est correctement lancé avec un test de connectivité http.

sleep 15         # 15 secondes d'attente pour que le service se lance correctement

http_forum=$(curl -sL -w "%{http_code}\\n" "https://forum.lmilcent.com/" -o /dev/null)

if [ "$http_forum" -ne "200" ]; then  
    echo -e "\n\t====================================================================================="
    echo -e "\t/ ! \ ERREUR lors de la mise à jour du forum, celui ci ne semble plus répondre./ ! \ "
    echo -e "\t====================================================================================="
    echo -e "\n\tERREUR le forum ne répond pas" >> $log
else  
    echo -e "\n\tLe forum semble fonctionner correctement, la mise à jour c'est bien passée !"
    echo -e "\t----------------------------------------------------------------------------"
fi  

3 - Restaurer une image

Pour restaurer une image il suffit d'exécuter la commande suivante : docker load < IMAGE.tar pour charger l'image sauvegardée dans un dossier quelconque dans docker.
Vous pouvez ensuite la lancer avec vos commandes habituelles. N'oubliez pas de restaurer le dossier contenant les données relatives à cette version ! Vous pouvez essayer de garder le dossier actuel, mais en fonction des services cela peut fonctionner ou non.

4 - Script complet

# Ce script va permettre de rechercher une mise à jour, l'appliquer
# et en cas de problème, les données précédentes (dont le container)
# pourra être restauré
#
# Algo :
#         1) Récupérer la date
#         2) Tester si une mise à jour est disponible
#             2.1) Télécharger la mise à jour
#             2.2) Commit les deux images REDIS et NODEBB avec
#                  la date en guise de versionning
#             2.3) Backup le dossier /home/nodebb avec la date
#             2.4) Supprimer et re créer les container
#                  pour appliquer les mises à jour
#             2.5) Tester la connectivité au forum pour
#                  vérifier s'il fonctionne bien
#       3) Afficher les résultats et/ou envoyer un mail
#
# Date de mise à jour : 28.04.2016
# Auteur : Louis MILCENT
#

#!/bin/bash

#
# Facultatif
#if (( $EUID != 0 )); then
#  echo "===================================================="
#  echo " Vous devez lancer le script avec les droits root !"
#  echo "===================================================="
#  exit
#fi


log='/home/louis/logs/forum.log'  
date=$(date +"%d.%m.%y")


# ---------
# Fonctions
# ---------
# Commit
# Permet de commit un container donné
# Arguments d'entrée :
#       - Nom du container
#       - Nom de la sauvegarde (date ajoutée ensuite)
#
function commit {

  if [ -z $1 ]; then
    echo -e "\n\tProbleme avec la fonction commit : il manque les arguments"
    return 0
  else
    container=$1
  fi

  if [ -z $2 ]; then
    nom_save=$container
  else
    nom_save=$2
  fi

  docker commit --author="flexbrane" --message="Sauvegarde du $date" $container save/$nom_save"_"$date
  docker save save/$nom_save"_"$date > /home/1_save/images/$nom_save"_"$date".tar"
# Pour récupérer une images : docker load < IMAGE.tar

}


# Mise à jour du fichier log ou création
touch $log

echo -e "\nLancement du script le $date" >> $log

# Test si mise à jour dispo des images
image=$(docker images | grep benlubar | cut -c 82-83)  
image=$(printf %d "$image")

image_redis=$(docker images | grep alpine  | cut -c 82-83)  
image_redis=$(printf %d "$image_redis")

# On met à jour les images
docker pull benlubar/nodebb:latest  
docker pull redis:alpine

image_maj=$(docker images | grep benlubar | cut -c 82-83)  
image_maj=$(printf %d "$image_maj")  
image_redis_maj=$(docker images | grep alpine  | cut -c 82-83) # Alpine car c'est la seule image basé dessus pour le moment  
image_redis_maj=$(printf %d "$image_redis_maj")

# Permet de ne sauvegarder le dossier qu'une seule fois si les deux images sont mises à jour
dossier_save=0

# On verifie si la valeur de temps de l'image a été modifiée
# Si oui, on lance le reste. Sinon on ne modifie rien.
if [ "$image" -ne "$image_maj" ]; then

  echo -e "\n\tL'image benlubar/nodebb à été mise à jour"
  echo -e "\t-----------------------------------------"
  echo -e "Image benlubar/nodebb mise à jour" >> $log

  # on sauvegarde cette version
  echo -e "\n\tSauvegarde du container actuel"
  echo -e "\t------------------------------"
  commit nodebb


  # on sauvegarde le dossier affilié
  echo -e "\n\tSauvegarde du dossier nodebb"
  echo -e "\t----------------------------"
  sudo cp -r /home/nodebb /home/1_save/nodebb_$date
  dossier_save=1


  # on applique la mise à jour
  echo -e "\n\tArrêt et supression de nodebb"
  echo -e "\t-----------------------------"
  docker stop nodebb
  docker rm nodebb


  echo -e "\n\tLancement de NodeBB"
  echo -e "\t-------------------"
  sh lancement_nodebb.sh

  echo -e "\n\tTest du bon fonctionnement du forum"
  echo -e "\t-----------------------------------"
  echo -e "(Attente de 15 secondes avant de tester la connectivité du forum)"
  sleep 15

  http_forum=$(curl -sL -w "%{http_code}\\n" "https://forum.lmilcent.com/" -o /dev/null)


  if [ "$http_forum" -ne "200" ]; then
    echo -e "\n\t====================================================================================="
    echo -e "\t/ ! \ ERREUR lors de la mise à jour du forum, celui ci ne semble plus répondre./ ! \ "
    echo -e "\t====================================================================================="
    echo -e "\n\tERREUR le forum ne répond pas" >> $log
  else
    echo -e "\n\tLe forum semble fonctionner correctement, la mise à jour c'est bien passée !"
    echo -e "\t----------------------------------------------------------------------------"
  fi


else

  echo -e "\n\tL'image benlubar/nodebb est déjà à jour"
  echo -e "\n\t---------------------------------------"
  echo -e "\tImage benlubar/nodebb déjà à jour" >> $log

fi





if [ "$image_redis" -ne "$image_redis_maj" ]; then

  echo -e "\nL'image redis a été mise à jour"
  echo -e "\n\t-----------------------------"
  echo -e "\tImage redis mise à jour" >> $log

  # on sauvegarde cette version                                                                                                                                                                                           
  echo -e "\n\tSauvegarde du container actuel"
  echo -e "\t------------------------------"
  commit nodebb_db


  if [ "$dossier_save" -eq 0 ]; then
    echo -e "\n\tSauvegarde du dossier nodebb"
    echo -e "\t----------------------------"
    sudo cp -r /home/nodebb /home/1_save/nodebb_$date
  fi


  echo -e "\n\tArrêt et supression de nodebb_db"
  echo -e "\t--------------------------------"
  docker stop nodebb_db
  docker rm nodebb_db

  echo -e "\n\tLancement du service"
  echo -e "\t--------------------"
  sh lancement_nodebb_db.sh

else

  echo -e "\n\tL'image redis est déjà à jour"
  echo -e "\t---------------------------"
  echo -e "Image redis déjà à jour" >> $log
fi

echo -e "\nScript terminé\n" >> $log

exit  

Conclusion

J'espère que ce script vous sera utile. Il permet d'éviter un bon nombre de soucis et procure un niveau de résilience de votre service un peu plus important. Prochainement je vous expliquerai simplement comment sauvegarder ces données vers un serveur SSH distant.