viernes, 20 de febrero de 2009

De como hacer buenos backups usando un simple script(o KISS bakcup)

Supongamos un server recién instalado, llevó horas configurarlo, dejarlo afinado como un violín, pero cuando creemos haber terminado aún hay como una sensación, algo que no está bien, pero... que será?

"Puede fallar" decía Tusam, y todos comprobamos la verdad de estas sabias palabras alguna vez en la vida.

Pero...qué pude fallar?
short answer: "todo puede fallar"

Lo bueno es que muchas de las cosas son reemplazables, mother, memorias, fuentes, discos se consiguen a buen precio hoy, pero que hay de las horas de configuración?, los datos acumulados?, los emails?, las bases de datos?

La posibilidad de que esa información valiosa se pierdan siempre ha sido un itch to scratch.

1- Objetivos

Hacer un script de backup que

  • sea tan simple de usar como poner una linea en cron
  • permita backup versionado en el tiempo
  • optimice el espacio de los backups
  • pueda hacer mirror remoto de los mismos
  • borre automaticamente los viejos para hacer espacio a los nuevos
  • salga limpiamente de los errores que se puedan producir y garantice la integridad
  • avise a alguien del error enviandole un detalle del fallo
  • que mientras funcione bien sea silencioso y pase desapercibido

2- Requisitos

  • bash
  • tar
  • lftp

3- Desarrollo

Tener un itch to scratch nunca es garantía de que llegará el scratch, ahí es cuando el cliente exijente aparece y de alguna manera fuerza y/o financia el scratch:

cliente- muy bueno como quedó todo, ahora tengo una pregunta, de esto hay backup?
geek- eh... sí, naturalmente, como no va a haber backup!
Todo comenzó con un simple script para salir del paso, y a medida que pasaron los años las necesidades de backup se volvieron cada vez más complejas.

Varios checkpoints en el tiempo, mirror remoto, aviso en caso de fallos, backups incremental fueron algunas de las exigencias, y el pequeño script fue creciendo y evolucionando cada vez más.

3.1- Configuración
La primera parte del script está dedicada la configuración, esto es lo que da al script flexibilidad.

Lo primero es elegir el directorio en donde se van a guardar los backups, la ventaja de esto es que ese directorio puede caer en un disco diferente al principal, o incluso puede ser una unidad de red montada via smbfs o nfs por ejemplo.
#!/bin/bash
# directorio donde van a parar los backups
backup_dir="/backup"
Luego viene la parte de remote mirror de los bakcups, el script usa ftp para mantener en syncro el directorio de backups local con el remoto. Si dejamos en blanco estas variables el script realiza solo el backup en el directorio local.
# datos ftp, si están en blanco el backup es sólo local
ftp_host=10.0.0.1
ftp_user=backup
ftp_pass=******
La siguiente variable nos pide el nombre de un usuario o un email al cual enviar un aviso en caso de que el script falle en alguno de sus pasos.
# mail a donde avisar si algo sale mal, puede ser un usuario del sistema
mail_for_errors="root"
backup_days representa el numero de días de los cuales vamos a tener backups, a mayor cantidad de días, más espacio en disco necesitaremos.
# tiempo en dias que conserva copia de los backups
backup_days=15
A continuación configuramos que cosas vamos a incluir y excluir dentro del bakcup. En un backup completo tradicional incluiremos el directorio raiz "/" y excluiremos cosas como /tmp,/proc y /sys entre otros.

Hay que acordarse de excluir cualquier directorio que contenga mucha información que pueda inflar innecesariamente los backups, por ejemplo el cache de squid o el fuente de un kernel en /usr/src, en fin,cosas que no vale la pena tener por duplicado.

Obviamente se excluye el $backup_dir para no crear una recursividad tratando de backupear el propio directorio de backups.
exclude=$(mktemp)
include=$(mktemp)
cat > $include <<EOF
/
EOF
cat > $exclude <<EOF
/tmp/*
/sys/*
/proc/*
/var/spool/squid/*
$backup_dir
EOF
Después de esto comienza realmente el script, así que llegamos a la linea divisoria, si estás buscando una solución Copy and Paste, podés saltar todo esto y descargar el script completo
3.2- Setup inicial
Voy a explicar la solución paso a paso, en el orden de ejecución. Para esto me voy a tomar la libertad de explicar las funciones cuando son llamadas y no en el orden en que aparecen en el script original.

Comenzamos creando un logfile donde redireccionar la stderr de los programas que usamos a lo largo del script, para poder recopilar los errores de los mismos y despues enviarlos por mail.

También creamos un directorio .tar, dentro de nuestro directorio de bakcups. Este directorio lo usaremos para guardar el archivo para backups incremental y unos lock que me aseguraran de que el script pueda recuperarse de un fallo.
# Scripts Begins Here
logfile=$(mktemp)
inc_dir="${backup_dir}/.tar"
inc_file="${inc_dir}/incremental.info"
[ -d "$backup_dir" ] || mkdir -p "$backup_dir"
[ -d "$inc_dir" ] || mkdir -p "$inc_dir"
El pŕoximo paso es borrar los backups que ya son viejos segun $backup_days
# borro los backups vencidos
find $backup_dir -name "backup-$(hostname)-*" -ctime +$backup_days -a -type f -exec rm -f {} \;
En esta linea hago la primera comprobación de error. Cuando comienzo a crear un backup completo, creo un archivo .lock, que se borrará al final si todo sale exitosamente.

Pero si algo falla, entonces el archivo .lock permanece creado hasta la próxima ejecución del script. Si encuentro ese archivo es porque algo salío mal y sé que debo hacer nuevamente el backup completo (full).
# si encuentro .lock es porque fallo el full backup, borro inc_file para que se haga de nuevo
[ -f "${inc_file}.lock" -a -f "$inc_file" ] && rm -f "$inc_file"
El script maneja dos tipos de backup, full e incremental.

El backup full se realiza una vez a la semana, y es un backup de tar autocontenido, que contiene toda la información necesaria para ser usado y extraido en cualquier lugar.

El backup incremental se realiza a diario, y se usa como referencia siempre el último backup full. Esto quiere decir que el lunes se realiza un backup incremental teniendo en cuenta el último backup full que es el del domingo. Y si quiciera restaurar el sistema al día lunes debería primero extraer el backup full del domingo y luego arriba de ese usar el del lunes.
Los backups incrementales son independientes, esto quiere decir que están siempre hechos contra el último backup full, por lo que si tengo un backup incremental del viernes y quiero restaurar el sistema a ese día, entonces sólo necesito el último backup full (domingo) más el incremental del viernes.
Esto está hecho así para asegurar la efectividad de los backups, ya que si para poder recuperar el viernes dependiera de que el lunes, martes, miércoles y jueves se hayan realizado con éxito estoy siendo muy optimista por default.

Cada backup tiene su nombre, que incluye el hostname, indica si es full o incremental y la fecha a la cual corresponde. En el caso de los incrementales se usa unicamente el día de la semana para que se vayan pisando con los anteriores y así tener sólo la última semana en formato incremental.
#setup full or incremental
if [ $(date +%u) -eq 7 -o ! -f "$inc_file" ];then
  #backup-full
  backup_name="${backup_dir}/backup-$(hostname)-full-$(date +%Y-%m-%d).tar.gz"
  #borro inc_file para que el backup sea full
  [ -f "$inc_file" ] && rm -f "$inc_file"
  touch "${inc_file}.lock"
else
  #backup-inc
  backup_name="${backup_dir}/backup-$(hostname)-inc-$(date +%A).tar.gz"
  cp $inc_file ${inc_file}.temp
  inc_file=${inc_file}.temp
fi
3.3- Creando el backup
Finalmente una vez que todo está listo, hago el backup
#finally, do the backup
tarme "$inc_file" "$backup_name"
tarme es una función, veamosla en detalle, lo que hace es invocar a tar con ciertos parámetros especiales, como que archivos incluir y excluir del backup, que preserve owners y permisos o que envíe stderr a logfile.
tarme() {
  local inc_file=$1 file=$2
  tar --listed-incremental="$inc_file" \
    --files-from="$include" \
    --exclude-from="$exclude" \
    --ignore-failed-read \
    --absolute-names \
    -zpcf "$file"   2>$logfile || do_exit "tar" $? $file
  return 0
}
3.4- A prueba de errores
Como se puede ver al final de la invocación de tar, hay un "|| do_exit ..."

do_exit es otra función, que la vamos a utlizar en caso de que algo salga mal, en bash cuando un comando devuelve 0 es que todo fue bien en su ejecución, en cambio si el valor es != de cero es que algo salió mal. Al poner "|| do_exit" me estoy asegurando que si algo sale mal la función do_exit se ejecute.

do_exit va a ser usado por tar pero también por otros comandos que fallen, la idea es que haga un clenaup si el backup quedó inconsistente y que envíe un mail al administrador avisando que algo salío mal.
do_exit() {
  local app=$1 exit_status=$2 file=$3
  case $app in
  tar) #exit status 1 en tar no es un error fatal:
       #If tar was given ‘--create’, ‘--append’ or ‘--update’ option,
       #this exit code means that some files were changed while being
       #archived and so the resulting archive does not contain the
       #exact copy of the file set.
       [ $exit_status -eq 1 ] && return
       #borro el backup que fallo xq esta incompleto
       [ -n "$file" -a -f "$file" ] && rm -f "$file"
  ;;
  esac
  cat $logfile | mail -s "Backup error host:$(hostname) app:$app exit_status:$exit_status file:$file" $mail_for_errors
  exit 1
}
3.5- Mirror remoto
Luego de que tar corrio exitosamente, hacemos el upload remoto si es que corresponde
[ -n "$ftp_host" ] && uploadme "$backup_name"
uploadme es otra función, lo que hace es hacer un mirror del directorio local de backups contra un ftp remoto, en este sentido el comando mirror de lftp es de gran ayuda.
Lo bueno de esto es que la sincronía puede fallar un día, pero en cuanto el script vuelva a correr el directorio local y remoto de backups terminaran perfectamente sincronizados.
uploadme() {
  local file=$1
  lftp -c "open ftp://$ftp_user:$ftp_pass@$ftp_host;mirror -R --delete-first $backup_dir (hostname)"  2>>$logfile || do_exit "lftp" $? $file
  return 0
}
3.6- Check final
Una vez subido el backup, entonces estamos en condices de borrar el .lock file, si es que lo habiamos creado (sólo se usa para los full backups)
# si todo fue bien borro el .lock
[ -f "${inc_file}.lock" ] && rm -f "${inc_file}.lock"
3.7- El script completo(backup.sh)
Descargar el script completo: backup.sh

4- Invocando el script desde crontab

Generalmente coloco el script en /usr/local/bin/backu.sh y luego agrego esta linea a alguan crontab, puede ser la de root(crontab -e) o en /etc/cron.d/ en alguan crontab personal.
La linea de cron es simplemente para ejecutarlo una vez al día preferentemente en un horario de poco impacto para el servidor, como son las horas de la madrugada
0 2 * * * root /usr/local/bin/backup.sh

5- Ejemplo de salida

Pego la salida de ls sobre un directorio de bakcups, en donde se han acumlado tres semanas.
Como se puede observar hay una gran diferencia de tamaño entre un backup full y uno incremental. También se ve como los incrementales solo cubren la primer semana y luego quedan sólo full 3 semanas hacia atras.
-rw-r--r-- 1 root root 1.4G 2009-04-19 03:16 backup-sequre-0001-full-2009-04-19.tar.gz
-rw-r--r-- 1 root root 1.3G 2009-04-26 03:15 backup-sequre-0001-full-2009-04-26.tar.gz
-rw-r--r-- 1 root root  16M 2009-04-27 03:02 backup-sequre-0001-inc-Monday.tar.gz
-rw-r--r-- 1 root root  16M 2009-04-28 03:02 backup-sequre-0001-inc-Tuesday.tar.gz
-rw-r--r-- 1 root root  30M 2009-04-29 03:02 backup-sequre-0001-inc-Wednesday.tar.gz
-rw-r--r-- 1 root root  30M 2009-04-30 03:02 backup-sequre-0001-inc-Thursday.tar.gz
-rw-r--r-- 1 root root  30M 2009-05-01 03:02 backup-sequre-0001-inc-Friday.tar.gz
-rw-r--r-- 1 root root  50M 2009-05-02 03:02 backup-sequre-0001-inc-Saturday.tar.gz
-rw-r--r-- 1 root root 1.3G 2009-05-03 03:16 backup-sequre-0001-full-2009-05-03.tar.gz

6- Conclusión

Tener backups es garantía de buen sueño para un sys-admin y, es además, una marca de calidad en lo que a infraestructura respecta.

Si bien hay soluciones de backup muy interesantes como bacula o backuppc, hay escenarios en donde no se justifica hacer semejante despliegue de configuración e infraestructura.
Escenarios donde lo único que necesitamos es tener una copia fresca y con archivo de un servidor, en donde dedicar más de 5 miutos de setup sería perder tiempo y dinero.

Este script hace que tener backups sea algo sencillo de implementar, sin descuidar la calidad de los resultados, así que ya no hay más excusas para tener un sólo server sin backup.

Good luck and good backups :)

14 comentarios:

Hola Amigo.. muy bune post.. voy a implementarlo te cuento

Muy bueno! Es mas de lo que estaba buscando :O

Segui así con el blog q esta muy groso!

saludos

Buenas , primero de todo, muchas gracias, era exactamente lo que buscaba, pero me aguarda una duda, la primera vez q he ejecutado el script, este me hace uno completo. SI quiero q vaya haciendome incrementales como debo llamar al script?

Gracias
Eduard

@Eduard4D la primera vez siempre hace un backup full porque es imposible hacer incrementales si no hay uno completo de referencia.
El script se llama siempre igual, sin ningun parametro especial. La inteligencia interna del mismo hará backups incrementales hasta llegar al día 7 de la semana(domingo) donde realizara un nuevo backup completo y así empezar de nuevo.

muy buen script amigo

una pregunta, el backup incremental se basa solamente en si el archivo existe o no? se da cuenta si yo modifico el archivo para meterlo en el nuevo backup?

Por ejemplo, yo tengo un directorio con el archivo "hola" está vacio, el dia 1 hago un backup full, el segundo dia yo escribo algo en el archivo "hola", el backup incremental se da cuenta que el archivo es diferente?

Excelente el script. Me gustaria saber que variaciones debo hacerle si quiero hacer backups de los switches cisco de la empresa donde trabajo.
De antemano gracias

@Sabas
Es muy distinto hacer backups de switchs o routers ya que cada uno tendrá una manera distinta de acceder al dump. Para cisco especificamente google siempre te dara una mano

buenas noches. la verdad me gusto el codigo preo tengo 2 pregunta :

una es que no me envia notificaciones a mi correo}y la otra es que quisiera saver si se puede reducir la comprencion mas

Estaba buscando algo sencillo y efectivo, este script encaja a la perfección.

Muchas gracias por el articulo y las explicaciones, muy bueno.

un saludo.

Muy bueno lo implemente y me va de maravillas.
Ahora soy muy limitado en linux, mi duda es. Como restauro uno de estos backups?? ejemplo, tengo como el ls que muestras, estoy en el dia viernes y quiero restaurar el backup del dia miercoles, como hago eso?
Saludos a la distancia!

Para resutaurar usas el comando tar. Siempre debes restaurar primero uno de los backup full, y luego basta con un único incremental del día al que quieras llegar. El comando sería algo así
tar --numeric-owner -zxpvf backup.tar.gz

sigo sin entender perdon, podrias ser mas grafico? tengo estos archivos: backup-mdh-full-2012-12-02.tar.gz
backup-mdh-inc-sábado.tar.gz
backup-mdh-inc-lunes.tar.gz
backup-mdh-inc-martes.tar.gz
backup-mdh-inc-miercoles.tar.gz

Supongamos que estoy a viernes y quiero restaurar el incremental del dia martes. Como hago esta operacion?
Gracias y disculpa las molestias.

hola como puedo hacer un scrips linux, que me permita copiar el contenido de varios archivos logs. y ubicarlos en uno solo ....automaticamente cada 5 minutos ...... tomando en cuent que los archivos se modifican constantemente ....

Hola amigo, te felicito por el post, lástima que el enlace al script completo no funciona. Podrias colocar un enlace válido?

Gracias