Backup automático de base de datos
Sistema de backup automático de MySQL que corre cada 24 horas via Ansible + cron en el servidor de backup. Aplica a la gran mayoría de clientes que usan Histrix.
Qué hace en cada ejecución
- Genera una contraseña del día con 3 palabras en español separadas por punto (ej:
perro.gato.casa) - Notifica la contraseña nueva por email a los administradores y por Google Chat
- Dump de MySQL — se conecta a cada cliente, saca el dump, lo comprime (
.sql.gz) y lo trae al servidor de backup. Los clientes se procesan en paralelo - Rota la contraseña del usuario
histrixen la base de producción con la nueva clave del día - Empaqueta todos los dumps en un
.tgzcon la fecha - Sube a S3 (
mundoit.sitios) - Limpia backups locales con más de 10 días
- Sube las bases al MySQL secundario
Backups desde Docker
Si el cliente tiene MySQL dockerizado el proceso es el mismo — el script de dump
se conecta al host del contenedor con las credenciales configuradas en targets.txt.
No hace falta hacer nada especial, alcanza con que el puerto MySQL esté accesible
desde el servidor de backup via SSH tunnel.
Configurar un cliente nuevo
1. Acceso SSH
Agregar la clave pública del servidor de backup al servidor del cliente:
Y configurar el ~/.ssh/config del servidor de backup para que reconozca el host.
2. MySQL externo o remoto
Si la base de datos no está en el servidor del cliente ni dockerizada (ej: DO Managed DB), instalar el cliente MySQL en el servidor del cliente para que el dump pueda conectarse:
3. Agregar a targets.txt
En el servidor de backup editar /home/automata/ansible-db-backup/targets.txt:
Valores por defecto
Si usás localhost, puerto 3306, usuario root y password genus2041 no hace falta declararlo, se asume automáticamente. La línea mínima sería solo el nombre del cliente:
4. Listo
El backup se ejecuta automáticamente en el próximo ciclo (cada 24 horas). No hay que reiniciar nada.
Archivos del sistema
Repo: ansible-db-backup
Todo vive en /home/automata/ansible-db-backup/ en el servidor de backup:
| Archivo | Descripción |
|---|---|
mysql_dump.yml |
Playbook principal de Ansible |
targets.txt |
Lista de clientes a backupear |
generate_pass.sh |
Genera la contraseña del día |
dump.sh |
Script que ejecuta el mysqldump en cada cliente |
updpass.sh |
Rota la contraseña del usuario histrix |
uploadmysql.sh |
Sube las bases al MySQL secundario |
dumps/ |
Dumps temporales antes de empaquetar |
backups/ |
.tgz listos para subir a S3 |
Playbook completo
# Play 1) Generar la clave en localhost, loguear y avisar por mail
- name: Generate today's password, log it, and email it
hosts: localhost
gather_facts: true
vars:
password_file: /tmp/todaypass.log
tasks:
- name: Generate password (script returns the password on stdout)
ansible.builtin.command: ./generate_pass.sh
register: genpass
changed_when: genpass.rc == 0
tags: [password]
- name: Set todaypass fact
ansible.builtin.set_fact:
todaypass: "{{ genpass.stdout | trim }}"
tags: [password]
- name: Save password to temp file
ansible.builtin.copy:
dest: "{{ password_file }}"
content: "{{ todaypass }}\n"
mode: '0600'
tags: [password]
- name: Send notification email
ansible.builtin.mail:
host: smtp.mailgun.org
port: 587
username: "postmaster@YOUR_DOMAIN_HERE"
password: "MAILGUN_API_KEY_HERE"
from: "postmaster@YOUR_DOMAIN_HERE"
to: "TO_EMAIL_HERE"
subject: "[PASSWD] Nueva Clave Generada: {{ ansible_date_time.date }}"
subtype: html
body: |
<h1>Nueva Clave Generada:</h1><br />
<h2>{{ todaypass }}</h2>
<br/> por Ansible backup.
delegate_to: localhost
tags: [notify, password, email]
- name: send Google chat notification
ansible.builtin.uri:
url: "URL_DEL_WEBHOOK_DE_GOOGLE_CHAT"
method: POST
headers:
Content-Type: application/json
body_format: json
body:
text: |-
🔐 *Nueva Clave Generada*
*Fecha:* {{ ansible_date_time.date }}
*Password:* `{{ todaypass }}`
*Host:* {{ ansible_hostname }}
Backup automático iniciado por Ansible
status_code: 200
delegate_to: localhost
tags: [notify, password, chat]
# Play 2) En los hosts MySQL: dump en paralelo y traer los .sql.gz
- name: Dump MySQL databases on DB hosts (in parallel) and fetch artifacts
hosts: mysql
strategy: free
gather_facts: false
vars:
todaypass: "{{ lookup('file', '/tmp/todaypass.log') }}"
remote_dump: "/tmp/{{ inventory_hostname }}.sql.gz"
tasks:
- name: Copy dump script
ansible.builtin.copy:
src: dump.sh
dest: /tmp/dump.sh
mode: '0755'
tags: [mysql]
- name: Start dump script asynchronously
ansible.builtin.command: >
bash /tmp/dump.sh {{ inventory_hostname }}
MYSQL_USER={{mysql_user | default('root')}}
MYSQL_PASS={{ mysql_password | default('genus2041') }}
MYSQL_HOST={{ mysql_host | default('localhost') }}
MYSQL_PORT={{ mysql_port | default('3306') }}
TODAY_PASS={{ todaypass }}
async: 7200
poll: 0
register: dump_job
tags: [mysql]
- name: Wait for dump to complete
ansible.builtin.async_status:
jid: "{{ dump_job.ansible_job_id }}"
register: dump_wait
until: dump_wait.finished
retries: 750
delay: 5
tags: [mysql]
- name: Fetch dump artifact
ansible.builtin.fetch:
src: "{{ remote_dump }}"
dest: "dumps/"
flat: true
tags: [mysql]
# Play 3) Rotar pass, empaquetar, subir a S3, limpiar viejos, subir a MySQL secundario
- name: Rotate local tasks, archive and upload
hosts: localhost
gather_facts: true
vars:
backups_dir: backups
dumps_glob: "dumps/*.gz"
tar_name: "mysql-{{ ansible_date_time.date }}.tgz"
environment:
AWS_ACCESS_KEY: ACCESS_KEY_HERE
AWS_SECRET_KEY: SECRET_KEY_HERE
tasks:
- name: Ensure Python3 path
ansible.builtin.set_fact:
ansible_python_interpreter: "/usr/bin/python3"
- name: Change 'histrix' password
ansible.builtin.command: bash updpass.sh
- name: Ensure backups directory exists
ansible.builtin.file:
path: "{{ backups_dir }}/"
state: directory
mode: '0755'
- name: Archive dumps
ansible.builtin.archive:
path: "{{ dumps_glob }}"
dest: "{{ backups_dir }}/{{ tar_name }}"
when: lookup('ansible.builtin.fileglob', dumps_glob) | length > 0
- name: Upload archives to S3
community.aws.s3_sync:
bucket: mundoit.sitios
file_root: "{{ backups_dir }}/"
- name: Find backup files older than 10 days
ansible.builtin.find:
paths: "{{ backups_dir }}/"
age: 10d
recurse: yes
register: old_backups
- name: Remove backup files older than 10 days
ansible.builtin.file:
path: "{{ item.path }}"
state: absent
loop: "{{ old_backups.files }}"
- name: Upload Databases to secondary MySQL (custom script)
ansible.builtin.command: bash uploadmysql.sh
tags: [mysql]