Copias de Seguridad de Ghost

Como buen administrador de sistemas y sabiendo la juventud del proyecto Ghost, sabía que tendría que tener controladas las actualizaciones de la plataforma. Evidentemente estas actualizaciones no serán nada traumáticas si tenemos un sistema de backup que nos dé tranquilidad.

Así que vamos a empezar por este punto. Para hacer pruebas no utilicé mi montaje principal, sino mi 'Isla Sorna'. Para los que no hayáis visto 'Jurassic Park: The Lost World', os diré que es un sitio paralelo a la isla principal en el que se hace todo por duplicado, en mi caso es una pequeña máquina virtual en casa, réplica del sitio principal. En otros posts ya hablaremos de virtualización, sólo os adelantaré que no utilizaremos VMWare.

Vamos al lío, partimos de la base proporcionada por el propio proyecto. Nuestro objetivo es automatizar todo lo automatizable.

Lo primero es el backup de la base de datos. En nuestro caso, MySQL. Para ello en varios tutoriales he visto que se suele utilizar el paquete 'automysqlbackup' y me pareció una buena opción.

La instalación del paquete nos obliga a configurar un servidor de correo, como teníamos previsto utilizar uno para las notificaciones nos ahorraremos un paso en el futuro. Por defecto se instala postfix, aunque yo siempre he preferido exim4, lo usamos en la oficina para el envío de faxes y tengo más experiencia con él. Postfix también lo usamos extensivamente en otra parte de la aplicación, así que, de momento, con la instalación básica de postfix y un buen firewall vamos a ir tirando.

Volviendo al backup, según este post vamos a crear un usuario limitado sólo a los backups. Accedemos a la consola de administración de mysql y ejecutamos los siguientes comandos:

# mysql -uroot -p
mysql> create user 'ghost_backup'@'localhost' identified by 'GHOST_BACKUP_PASSWORD';
Query OK, 0 rows affected (0.12 sec)
mysql> GRANT SELECT, RELOAD, SHOW DATABASES, LOCK TABLES ON *.* to 'ghost_backup'@'localhost' IDENTIFIED BY 'GHOST_BACKUP_PASSWORD';
Query OK, 0 rows affected (0.02 sec)
mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)
mysql> quit
Bye

Con esto ya tenemos preparada la base de datos para la invocación del script. Ahora editamos el fichero que contiene los valores por defecto del paquete, '/etc/default/automysqlbackup', para que se adecúe a nuestras necesidades, añadimos/modificamos las siguientes líneas:

[...]
USERNAME='ghost_backup'
PASSWORD='GHOST_BACKUP_PASSWORD'
[...]
MAILADDR="myname@mydomain.com"
[...]
COMP=bzip2
[...]
LATEST=yes
[...]

Con los comentarios que aparecen junto a las variables que estamos cambiando os podéis hacer una idea de para qué sirve cada variable, aunque sólo con su nombre creo que es más que suficiente.

Según este archivo las copias de seguridad se guardarán en '/var/lib/automysqldump'. Si observamos el archivo '/etc/crontab', podremos ver las horas a las que se inician las tareas diarias, semanales y mensuales de las que forma parte este script. Con estas horas por defecto nos va bien, ya que suelen ocurrir de madrugada.

Ahora vamos con la copia de los archivos que conforman el resto de datos de nuestro blog. Por ahí también he leído que se puede utilizar GIT para hacer el backup de nuestro 'fantasma' pero me lo apunto para hacerlo más adelante, de momento rsync y tar serán suficientes.

Necesitaremos la configuración de nuestro servidor web, todo el directorio '/etc/nginx' nos vendría bien, también necesitamos el directorio '/var/www/content' de nuestra aplicación y su archivo de configuración '/var/www/config.js'. Podemos optar por guardar estos directorios y los archivos en un paquete comprimido en nuestro servidor y después sincronizarlo externamente por si el principal falla.

Para ello crearemos un par de scripts y los activaremos mediante el cron para tener la información por duplicado. Necesitamos sacar del servidor los backups de la base de datos, por lo que el directorio en el que se guardan también lo añadiremos a esta copia.

El primero de los scripts es muy simple ya que sólo copia y comprime los archivos y el directorio que hemos comentado al principio dejando el resultado en un directorio diferente. También le hemos añadido un control de errores para que nos avise por correo si algo fue mal. Me he basado en el método que desarrollé para Psyche en el pasado con el que manteníamos el backup de las webs que manejábamos, y que con unos pocos cambios me sirve perfectamente para mi cometido actual.

En otro artículo futuro explicaré más profundamente cómo lo he montado, de momento dejamos nuestros viejos y queridos 'echoes':

#!/bin/bash

# Script to copy/compress files and directories from ghost blogging application

# Sources, individual files and/or directories
SOURCE_FILES="/var/www/config.js"
SOURCE_DIRECTORIES="/var/www/content /etc/nginx"

# Target, directory where we keep our files
TARGET="/opt/ghost_backup_files"
[ ! -d $TARGET ] && mkdir -p $TARGET

# Compression, wheter or not we want compression, argument for tar command
COMPRESS="j"

# Number of copies kept and deletion of older files
COPIES="30"

# File name format
PREFIX_FILENAME="ghost_files"
FILENAME="$PREFIX_FILENAME-`date +%Y-%m-%d`"
OLD_FILENAME="$PREFIX_FILENAME-`date --date "${COPIES} day ago" +%Y-%m-%d`"
case $COMPRESS in
        'j')    FILENAME="$FILENAME.tar.bz2"
                OLD_FILENAME="$OLD_FILENAME.tar.bz2"
                ;;
        'z')    FILENAME="$FILENAME.tar.gz"
                OLD_FILENAME="$OLD_FILENAME.tar.gz"
                ;;
        '')     FILENAME="$FILENAME.tar"
                OLD_FILENAME="$OLD_FILENAME.tar"
                ;;
        *)      # TODO make warnings by python script
                echo "Error: Incorrect invocation of tar command."
                exit 1
                ;;
esac
# Deletion of old files
rm -f $TARGET/$OLD_FILENAME

# Make the compression of all sources
tar c${COMPRESS}f $TARGET/$FILENAME $SOURCE_FILES $SOURCE_DIRECTORIES >/dev/null 2>&1
# TODO make warnings by python script
[ $? -ne 0 ] && echo "Error: Tar execution failed." && exit 1

Y no tiene más, son unos sencillos pasos para crear el nombre del archivo y montar la ejecución de la compresión de los ficheros que queremos guardar. En el directorio 'target' tendremos guardados los datos de los anteriores 30 días, de momento como son pequeños archivos podemos permitirnos tantos, pero tampoco serían necesarios, quizás en el destino, donde los almacenemos externamente nos podamos permitir guardar más cantidad.

Una vez que tenemos el script lo añadimos al cron. Debemos recordar que los scripts diarios, semanales y mensuales se realizan a cierta hora y como el backup de MySQL se arranca en base a éstos, sería ideal que la copia de los archivos se produjera cercana a la otra para que la sincronización fuera lo más correcta posible. Tampoco pasaría nada porque los cambios que podamos realizar en el blog, tanto en diseño como en entradas, no las haremos de madrugada.

Invocamos el editor de cron del root:

# crontab -e

y añadimos la siguiente línea:

# For ghost files backup
15 4    * * *   /usr/local/bin/backup_ghost_files_daily.sh 

Con esto sólo nos faltaría sacar toda esta información del servidor hacia otra ubicación para tenerla duplicada. Con otro script, en este caso de sincronización, mantendremos los mismos directorios de backup del servidor en otro diferente.

Lo primero que necesitamos para la correcta ejecución del script es hacer que la autenticación entre los dos servidores sea sin petición de password. Primero nos creamos una clave pública para poder copiarla al servidor:

$ ssh-keygen -t dsa
Generating public/private dsa key pair.
Enter file in which to save the key (/home/username/.ssh/id_dsa): 
Enter passphrase (empty for no passphrase): 
Enter same passphrase again: 
Your identification has been saved in...

Después de que esta operación se haya realizado con éxito necesitaremos copiar el archivo 'id_dsa.pub' al servidor principal mediante el siguiente comando:

$ ssh-copy-id -i ~/.ssh/id_dsa.pub root@servidor

Con esto ya podremos probar a autenticarnos:

$ ssh root@servidor
servidor #

Ahora montaremos el script de sincronización, es más sencillo que el anterior:

#!/bin/bash

# We must assure that we can login in the remote host without password
GHOST_SERVER="<server_name>"

# Directories to synchronize separated by blanks
REMOTE_DIRS="/var/lib/automysqlbackup /opt/ghost_backup_files"

# Local directory to maintain the files
TARGET="/mnt/store/ghost_backup"

# Command to synchronization
RSYNC="/usr/bin/rsync"

# Variable to limit the bandwidth
BANDWIDTH_LIMIT=""

# We loop through the remote dirs to make the rsync
for i in $REMOTE_DIRS
do
        LOCAL_DIR=`echo $i | rev | cut -d'/' -f1 | rev`
        $RSYNC $BANDWIDTH_LIMIT -e ssh -ravz --delete ${GHOST_SERVER}:$i $TARGET/ >/dev/null 2>&1
	# TODO make warnings by python script
        [ $? -ne 0 ] && echo "Error: Rsync execution failed, remote dir was: $i" && exit 1
done

Simplemente enumeramos los directorios remotos a sincronizar, dónde queremos sincronizar localmente y pocas cosillas más, hacemos un loop y listo. Por ahora suspendemos toda salida por consola y comprobamos si el rsync ha tenido éxito, ya volveremos sobre las notificaciones en otros artículos.

Lo ponemos en un cron para que se ejecute cada noche, preferiblemente después de que haya terminado el del servidor principal y ya hemos terminado:

# To synchronize ghost files from ghost server
30 5	* * *	/usr/local/bin/sync_ghost_files.sh

A partir de ahora ya podemos actualizar sin miedo.

Permitidme recuperar una tradición iniciada en mi blog personal, aconsejaros la canción del día, en este caso ha sido fácil, está sonando ahora mismo en mis cascos: Hey Man, Nice Shot, gran canción de un mejor disco Short Bus de la banda formada en los noventa Filter, muy aconsejable, disfrutad.