Skip to content

Latest commit

 

History

History
427 lines (302 loc) · 11.5 KB

File metadata and controls

427 lines (302 loc) · 11.5 KB

Lessons Learned - Molecule Testing Implementation

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.

Índice


Docker y /tmp con noexec

Problema

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.script con executable
  • ✗ Copiar a /usr/local/bin
  • ✗ Diferentes combinaciones de become

Root Cause

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)
                                        ^^^^^^^^

Solución

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/bash

Lección

Cuando 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>'

Idempotencia y get_url

Problema

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

Root Cause

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 existe

Razón: El módulo no puede determinar si el contenido remoto cambió sin descargarlo. Prioriza detectar cambios sobre idempotencia local.

Intentos Fallidos

  1. Usar creates parameter: Módulo ejecuta igual y marca changed
  2. Checksum verification: Añade complejidad sin resolver el problema
  3. Conditional logic compleja: Difícil de mantener

Solución Pragmática

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

Justificación

  • ✅ 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

Lección

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:

  1. Son temporales/de limpieza
  2. No representan cambio funcional del sistema
  3. Su propósito es setup, no configuración

VS Code V8 Crashes

Problema

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.

Root Cause

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)

Solución

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"

Lección

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.


systemd en Contenedores

Problema

Failed to connect to bus: No such file or directory
Failed to start systemd-resolved.service: Connection timed out

Root Cause

systemd tiene limitaciones en contenedores Docker:

  • Requiere privileged mode
  • Requiere /sys/fs/cgroup montado
  • Algunos servicios no funcionan completamente (networking, journald)

Solución

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))

Alternativa para Tests

Usar variable de skip en converge.yml:

vars:
  skip_dns_config: true
  sysadmin_skip_virtualbox_service: true
  developer_skip_docker_service: true

Lección

Docker 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

Workflow Iterativo

Problema Inicial

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

Solución Descubierta

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 test

Resultado: 24 min → 5 min por iteración (-80% tiempo)

Por Qué Funciona

Molecule test ejecuta 8 fases secuenciales:

  1. dependency (30s)
  2. syntax (5s)
  3. create (30s) ← Creación de contenedores
  4. prepare (6s)
  5. converge (25 min) ← La parte lenta
  6. idempotence (25 min)
  7. verify (10s)
  8. destroy (10s)

Durante debugging solo necesitas fase 5 (converge).

Cuándo Usar Cada Comando

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

Lección

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.


Shell vs Bash

Problema

/bin/sh: 1: set: Illegal option -o pipefail

Root Cause

Código original usaba:

- name: Convertir GPG
  ansible.builtin.shell: |
    set -o pipefail
    gpg --dearmor < /tmp/file.gpg > /output

Ansible usa /bin/sh por defecto, que en Debian/Ubuntu es dash, no bash.
dash no soporta pipefail.

Solución

Opción 1 - Simplificar (mejor):

- name: Convertir GPG
  ansible.builtin.shell: gpg --dearmor < /tmp/file.gpg > /output
  args:
    executable: /bin/bash

Opció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

Lección

/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.


Bonus: Estrategia General de Debugging

Pattern Observado

Cada problema tuvo múltiples intentos fallidos antes de encontrar root cause:

  1. Helm permission denied: 8 intentos
  2. Idempotencia get_url: 3 intentos
  3. VS Code rc=134: 2 intentos

Lo Que Funcionó

  1. 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"
  2. 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
  3. Buscar issues conocidos

    • VS Code rc=134 → GitHub issues existente
    • Docker /tmp noexec → Problema documentado
    • systemd en Docker → Limitación conocida
  4. 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

Anti-Patterns

  • 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"

Pro-Patterns

  • 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

Conclusión

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