Problema

Mantener configuraciones de usuario (bashrc, scripts auxiliares y archivos sensibles) sincronizadas entre varios equipos Linux es una tarea repetitiva. Cada máquina tiene pequeñas variaciones: algunos hosts requieren paquetes extra, otros necesitan secretos que no deben compartirse. Copiar manualmente los archivos o ejecutar git clone && ./setup.sh genera divergencias, errores de permisos y pérdida de tiempo, sobre todo cuando el número de hosts crece de 1 a 5 o más.

Causa

  1. Gestión ad‑hoc de archivos – Sin un control de versiones central, cada host evoluciona de forma independiente.
  2. Falta de parametrización – Los scripts se asumen idénticos para todos los nodos, lo que impide añadir excepciones por host o por grupo.
  3. Ausencia de secreto seguro – Guardar claves o tokens en texto plano dentro del repositorio compromete la seguridad y desalienta el versionado.
  4. Entorno heterogéneo – Un NAS sin gestor de paquetes necesita una ruta distinta para los scripts, mientras que los servidores con apt pueden instalar dependencias automáticamente.

Solución

Utilizar Ansible como motor de orquestación permite describir qué debe existir en cada máquina y cómo adaptarlo a sus particularidades. La clave está en estructurar el proyecto con:

  • Inventario grupal que refleje las funciones (laptop, workstation, server, nas).
  • Variables de grupo y de host para diferenciar dotfiles comunes, extensiones opcionales y secretos.
  • Roles que encapsulen tareas reutilizables: dotfiles, scripts, secrets.
  • Ansible Vault para almacenar claves que solo se despliegan en los hosts autorizados.

Paso a paso esencial

  1. Crear la raíz del proyecto

    mkdir -p ansible/{inventories,roles/{dotfiles,tasks,scripts,secrets},group_vars,host_vars}
    cd ansible
    
  2. Definir el inventario (inventories/hosts.ini)

    [laptops]
    laptop1 ansible_host=192.168.1.10
    
    [workstations]
    desktop1 ansible_host=192.168.1.20
    
    [servers]
    home_server ansible_host=192.168.1.30
    vps ansible_host=203.0.113.5
    
    [nas]
    nas01 ansible_host=192.168.1.40
    
    [all:vars]
    ansible_user=admin
    ansible_python_interpreter=/usr/bin/python3
    

    Los nombres de los grupos son arbitrarios; la convención site.yml para el playbook principal proviene de la tradición de Ansible de considerar el “sitio” completo como un único despliegue.

  3. Variables comunes (group_vars/all.yml)

    dotfiles_repo: "[email protected]:miusuario/dotfiles.git"
    scripts_repo: "[email protected]:miusuario/scripts.git"
    
  4. Variables específicas (group_vars/nas.yml)

    extra_dotfiles:
      - "nas_extra.sh"
    install_packages: false   # NAS sin apt
    

    Host‑level override (host_vars/laptop1.yml)

    secret_vault_path: "vault/laptop_secrets.yml"
    
  5. Playbook principal (site.yml)

    - hosts: all
      become: true
      roles:
        - dotfiles
        - scripts
        - secrets
    
  6. Rol dotfiles
    Estructura: roles/dotfiles/tasks/main.yml

    - name: Clonar repositorio de dotfiles
      git:
        repo: "{{ dotfiles_repo }}"
        dest: "/home/{{ ansible_user }}/.dotfiles"
        version: master
        force: yes
    
    - name: Crear enlace simbólico de .bashrc
      file:
        src: "/home/{{ ansible_user }}/.dotfiles/bashrc"
        dest: "/home/{{ ansible_user }}/.bashrc"
        state: link
        force: yes
    
    - name: Aplicar extensiones opcionales
      when: extra_dotfiles is defined
      loop: "{{ extra_dotfiles }}"
      file:
        src: "/home/{{ ansible_user }}/.dotfiles/{{ item }}"
        dest: "/home/{{ ansible_user }}/{{ item }}"
        state: link
        force: yes
    
  7. Rol scripts (similar, clona y enlaza)

    - name: Clonar repositorio de scripts
      git:
        repo: "{{ scripts_repo }}"
        dest: "/opt/scripts"
        version: master
        force: yes
    
    - name: Añadir scripts al PATH
      lineinfile:
        path: "/home/{{ ansible_user }}/.bashrc"
        regexp: '^export PATH='
        line: 'export PATH=$PATH:/opt/scripts'
        state: present
    
  8. Rol secrets

    - name: Decrypt vault file (only on hosts that define it)
      when: secret_vault_path is defined
      include_vars:
        file: "{{ secret_vault_path }}"
        name: secret_vars
    
    - name: Deploy private script
      when: secret_vars.private_script is defined
      copy:
        content: "{{ secret_vars.private_script }}"
        dest: "/usr/local/bin/private.sh"
        mode: '0755'
    
  9. Crear el vault (ejemplo para laptop)

    ansible-vault create vault/laptop_secrets.yml
    

    Dentro, definir:

    private_script: |
      #!/bin/bash
      echo "Información sensible"
    
  10. Ejecutar

    ansible-playbook -i inventories/hosts.ini site.yml --ask-vault-pass
    

Cuándo aplicar esta solución

  • Múltiples máquinas con configuración similar – Cuando al menos dos hosts comparten la mayor parte del entorno de usuario.
  • Necesidad de variaciones controladas – Si algunos equipos requieren archivos extra o paquetes opcionales.
  • Gestión de secretos – Cuando al menos un host necesita credenciales que no deben quedar en texto plano.
  • Entorno Debian/Ubuntu – Los módulos apt y git funcionan sin ajustes; para sistemas sin gestor de paquetes, basta con desactivar la tarea de instalación (install_packages: false).

No es la mejor opción si solo se administra un único equipo y la sobrecarga de Ansible supera el beneficio percibido.

Código

# Estructura mínima del proyecto
tree -L 3 ansible
ansible
├── inventories
│   └── hosts.ini
├── group_vars
│   ├── all.yml
│   └── nas.yml
├── host_vars
│   └── laptop1.yml
├── roles
│   ├── dotfiles
│   │   └── tasks
│   │       └── main.yml
│   ├── scripts
│   │   └── tasks
│   │       └── main.yml
│   └── secrets
│       └── tasks
│           └── main.yml
└── site.yml

Verificación

  1. Comprobación de enlaces – En cada host, ls -l ~/.bashrc debe apuntar a .dotfiles/bashrc.
  2. Presencia de scriptswhich private.sh solo debe devolver una ruta en los hosts que tienen el vault.
  3. Salida de Ansible – El playbook debe terminar con ok=... changed=... sin errores.
  4. Re‑ejecución idempotente – Ejecutar de nuevo el playbook no debe modificar archivos ya alineados.

Notas adicionales

  • Orden de roles: colocar dotfiles antes de scripts evita que un script dependa de un enlace que aún no existe.
  • Cache de git: usar force: yes garantiza que cambios en el repositorio se reflejen aunque el commit sea el mismo.
  • NAS sin apt: la variable install_packages permite saltar tareas de instalación; basta con envolver esas tareas en when: install_packages.
  • Mantenimiento – Añadir un nuevo host solo requiere crear su entrada en inventories/hosts.ini y, si necesita excepciones, un archivo en host_vars/. El resto del repositorio permanece intacto.
  • Escalado – Para más de 20 máquinas, considera dividir el inventario en varios archivos (hosts_laptops.ini, hosts_servers.ini) y usar -i múltiple.

Con esta estructura, un sysadmin puede desplegar dotfiles, scripts y secretos de forma reproducible, mantener diferencias controladas y evitar la tediosa copia manual que suele convertirse en la fuente de inconsistencias.