Este documento captura las lecciones aprendidas durante la implementación de tests Molecule para el proyecto ansible_notebooks, especialmente los desafíos encontrados con el rol sysadmin.
- Docker y /tmp con noexec
- Idempotencia y get_url
- VS Code V8 Crashes
- systemd en Contenedores
- Workflow Iterativo
- Shell vs Bash
FAILED! => {"changed": false, "msg": "non-zero return code", "rc": 126,
"stderr": "/tmp/get_helm.sh: Permission denied"}8 intentos fallidos con diferentes enfoques:
- ✗ Cambiar permisos a 0777
- ✗ Usar
ansible.builtin.scriptconexecutable - ✗ Copiar a /usr/local/bin
- ✗ Diferentes combinaciones de
become
Docker monta /tmp con flag noexec por seguridad:
$ docker exec debian13-sysadmin mount | grep '/tmp'
tmpfs on /tmp type tmpfs (rw,nosuid,nodev,noexec,relatime,inode64)
^^^^^^^^No intentar ejecutar directamente, usar bash como intérprete:
# ❌ Incorrecto - Permission denied
- name: Ejecutar script
ansible.builtin.script: /tmp/get_helm.sh
# ❌ Incorrecto - Sigue fallando
- name: Ejecutar script
ansible.builtin.shell: /tmp/get_helm.sh
# ✅ Correcto - Bash como intérprete
- name: Ejecutar script
ansible.builtin.shell: bash /tmp/get_helm.sh
args:
executable: /bin/bashCuando uses scripts en /tmp en Docker: Siempre invócalos con su intérprete explícito (bash, python, etc.), nunca confíes en shebang o permisos de ejecución.
Debugging tip: Verifica mount flags antes de asumir problemas de permisos:
docker exec <container> mount | grep '<path>'Test de idempotencia fallando después de converge exitoso:
ERROR Idempotence test failed because of the following tasks:
* => sysadmin : Pulumi | Fetch latest stable release tag from GitHub
* => sysadmin : Helm | Descargar script de instalación
* => sysadmin : NordVPN | Descargar la clave GPG del repositorio
Primera ejecución: 146 tasks OK, 86 changed
Segunda ejecución: 146 tasks OK, 6 changed ← FAIL
ansible.builtin.get_url siempre reporta changed: true al descargar:
- name: Descargar archivo
ansible.builtin.get_url:
url: https://example.com/file.gpg
dest: /tmp/file.gpg
# Resultado: changed=true incluso si archivo existeRazón: El módulo no puede determinar si el contenido remoto cambió sin descargarlo. Prioriza detectar cambios sobre idempotencia local.
- Usar
createsparameter: Módulo ejecuta igual y marca changed - Checksum verification: Añade complejidad sin resolver el problema
- Conditional logic compleja: Difícil de mantener
Para archivos temporales que se limpian inmediatamente:
- name: Descargar archivo temporal
ansible.builtin.get_url:
url: https://example.com/file.gpg
dest: /tmp/file.gpg
mode: '0644'
changed_when: false # ← Marca como no-cambio
- name: Convertir archivo
ansible.builtin.shell: gpg --dearmor < /tmp/file.gpg > /final/path
args:
creates: /final/path
- name: Limpiar temporal
ansible.builtin.file:
path: /tmp/file.gpg
state: absent
changed_when: false # ← Operación de limpieza- ✅ Los archivos son temporales (se borran inmediatamente)
- ✅ La idempotencia real está en el resultado funcional (binario instalado)
- ✅ La validación está en tasks posteriores (conversión GPG, instalación)
- ✅ Alternativas añaden complejidad sin beneficio práctico
Idempotencia != Sin cambios en cada tarea
La idempotencia se valida en el resultado final del sistema, no necesariamente en operaciones intermedias temporales. Usa changed_when: false con criterio para operaciones que:
- Son temporales/de limpieza
- No representan cambio funcional del sistema
- Su propósito es setup, no configuración
fatal error: all goroutines are asleep - deadlock!
FATAL ERROR: v8::ToLocalChecked Empty MaybeLocal.
Return code: 134 (V8 crash)
Paradoja: Extensión instalada exitosamente ANTES del crash.
Bug conocido de V8/Electron en VS Code:
- microsoft/vscode#159035
- Ocurre al salir después de instalar extensión
- No afecta funcionalidad (extensión queda instalada)
Manejar rc=134 como éxito:
- name: Instalar extensiones VS Code
ansible.builtin.command: "code --install-extension {{ item }}"
register: result
failed_when:
- result.rc not in [0, 134] # ← Permitir ambos códigos
- "'already installed' not in result.stdout"
changed_when: "'already installed' not in result.stdout"No todas las return codes != 0 son errores reales. Investiga el contexto antes de asumir fallo. En este caso:
- rc=0: Instalación exitosa limpia
- rc=134: Instalación exitosa + crash al salir (bug de VS Code)
- rc=1: Error real de instalación
Pattern reutilizable: Lista de rc codes aceptables cuando herramienta tiene bugs conocidos pero funcionales.
Failed to connect to bus: No such file or directory
Failed to start systemd-resolved.service: Connection timed out
systemd tiene limitaciones en contenedores Docker:
- Requiere privileged mode
- Requiere
/sys/fs/cgroupmontado - Algunos servicios no funcionan completamente (networking, journald)
Skip logic con detección de virtualización:
- name: Configurar DNS
ansible.builtin.systemd:
name: systemd-resolved
state: started
when: not (ansible_virtualization_type == 'docker' or skip_dns_config | default(false))Usar variable de skip en converge.yml:
vars:
skip_dns_config: true
sysadmin_skip_virtualbox_service: true
developer_skip_docker_service: trueDocker containers != VMs. Acepta las limitaciones:
- No todos los servicios systemd funcionarán
- Usa skip logic en lugar de intentar "arreglar" Docker
- Las imágenes geerlingguy incluyen systemd mínimo funcional
- Para tests, lo importante es validar instalación, no ejecución de servicios
Ciclo de debugging: Editar → Test → Ver error → Repetir
Con molecule test completo: 24 minutos por iteración 😱
9+ iteraciones = 3.6 horas de espera acumulada
Separar fases de Molecule:
# Una vez: Crear contenedores (30s)
molecule create
# Iterar: Solo converge (5 min)
molecule converge
# Editar archivos...
molecule converge
# Editar más...
molecule converge
# Final: Test completo (50 min)
molecule testResultado: 24 min → 5 min por iteración (-80% tiempo)
Molecule test ejecuta 8 fases secuenciales:
- dependency (30s)
- syntax (5s)
- create (30s) ← Creación de contenedores
- prepare (6s)
- converge (25 min) ← La parte lenta
- idempotence (25 min)
- verify (10s)
- destroy (10s)
Durante debugging solo necesitas fase 5 (converge).
| Comando | Uso | Duración |
|---|---|---|
molecule create |
Una vez al empezar sesión | 30s |
molecule converge |
Cada cambio durante desarrollo | 5 min |
molecule converge x2 |
Validar idempotencia manual | 10 min |
molecule verify |
Comprobar validaciones | 10s |
molecule destroy |
Limpiar al cambiar prepare.yml | 10s |
molecule test |
Test completo pre-commit | 50 min |
No uses bazooka para matar mosquito. Tools complejos tienen workflows granulares por algo. Lee la documentación de fases antes de iterar 9 veces con el comando completo.
Molecule no es "test" monolítico, es un pipeline de 8 fases independientes.
/bin/sh: 1: set: Illegal option -o pipefail
Código original usaba:
- name: Convertir GPG
ansible.builtin.shell: |
set -o pipefail
gpg --dearmor < /tmp/file.gpg > /outputAnsible usa /bin/sh por defecto, que en Debian/Ubuntu es dash, no bash.
dash no soporta pipefail.
Opción 1 - Simplificar (mejor):
- name: Convertir GPG
ansible.builtin.shell: gpg --dearmor < /tmp/file.gpg > /output
args:
executable: /bin/bashOpción 2 - Explicit bash (si pipefail es necesario):
- name: Convertir GPG
ansible.builtin.shell: |
set -o pipefail
gpg --dearmor < /tmp/file.gpg > /output
args:
executable: /bin/bash/bin/sh != bash. En Debian/Ubuntu:
/bin/sh→ dash (POSIX strict, rápido, sin extensiones)/bin/bash→ bash (extensiones GNU, pipefail, arrays, etc.)
Regla de oro:
- Si tu script funciona en cualquier shell POSIX →
/bin/sh(por defecto) - Si necesitas bashisms (pipefail, arrays, etc.) →
executable: /bin/bash
Debugging tip: Si ves "Illegal option" o "command not found" en shell tasks, agrega executable: /bin/bash.
Cada problema tuvo múltiples intentos fallidos antes de encontrar root cause:
- Helm permission denied: 8 intentos
- Idempotencia get_url: 3 intentos
- VS Code rc=134: 2 intentos
-
Investigar root cause ANTES de intentar fixes
- Bad: "Let me try chmod 777... let me try become... let me try..."
- Good: "Let me check why /tmp fails → mount flags → noexec → bash workaround"
-
Usar herramientas de debugging
docker exec <container> <command> # Ver qué pasa realmente molecule --debug converge # Más verbosidad grep -A 10 "FAILED" # Contexto de errores
-
Buscar issues conocidos
- VS Code rc=134 → GitHub issues existente
- Docker /tmp noexec → Problema documentado
- systemd en Docker → Limitación conocida
-
Aceptar workarounds pragmáticos
- changed_when: false en temporales → OK
- rc codes múltiples → OK si están justificados
- Skip logic en Docker → OK, no es el entorno real
- ❌ Shotgun debugging: Cambiar 5 cosas a la vez
- ❌ Cargo cult: Copiar código sin entender
- ❌ Perfectionism: Buscar solución "pura" cuando workaround funciona
- ❌ No leer logs completos: Solo ver línea "FAILED"
- ✅ Cambio mínimo: Un cambio → test → evaluar
- ✅ Entender herramienta: Leer docs de Molecule, Docker, Ansible
- ✅ Pragmatismo: Si funciona y está justificado, ship it
- ✅ Documentar: Escribir lecciones aprendidas para el futuro
Tiempo total de debugging: ~4-5 horas
Problemas únicos resueltos: 6 mayores
Intentos fallidos: 20+
Tests ejecutados: 12+
Key Takeaway: La mayoría del tiempo se perdió en trial-and-error. Invertir 10 minutos investigando root cause hubiera ahorrado 2 horas de iteraciones.
Documento vivo: Este archivo debe actualizarse con nuevos problemas/soluciones descubiertos.
Última actualización: Noviembre 2025
Autor: Debugging session with Claude AI Agent