Ansible: combinando become y delegate_to

por | abril 24, 2020

Si en nuestro playbook realizamos una escalada de privilegios en los nodos target y tenemos a su vez handlers que se ejecutan en local con "delegate_to: localhost", éstos intentarán a su vez realizar escalada de privilegios en el nodo de control. Hay dos problemas: por un lado el handler no tiene por qué requerir una escalada y por otro las credenciales de become pueden no coincidir en la máquina de control, por lo que la ejecución del handler fallará.

Para ilustrarlo con un ejemplo, este es un playbook sencillo que simplemente reinicia el servidor Apache y manda un correo notificando en cuántos servidores se ha realizado la tarea:

---
- hosts: "{{ server | default('none') }}"
  become: yes
  tasks:
    - name: Restart Apache Web Server
      service:
        name: apache2
        state: restarted
      notify: email-notification

  handlers:
    - name: email-notification
      mail:
        host: smtp.gmail.com
        port: 587
        username: mail@example.com
        password: *******
        from: SYSTEM
        to: example <mail@example.com>
        subject: Ansible-Report Apache Web Server restarted
        body: |-
          Apache Web Server restarted in:
          {% for item in ansible_play_hosts_all %}
          - Server: {{ item }}
          {% endfor %}
      delegate_to: localhost
      run_once: true

La instrucción "become: yes" aplica a todas las acciones -tasks y handlers- del playbook. El handler en concreto delega a localhost (máquina control) e intenta realizar una escalada de privilegios con la contraseña que introdujimos con become. Si nuestro nodo de control no tiene la misma contraseña de root, fallará la ejecución del handler.

ansible-playbook main.yml -e "server=jota-node01" --ask-become -vv

Aquí el error de ejecución:

RUNNING HANDLER [email-notification] *************************************************************************************************************************************************************************
task path: /ansible/playbooks/apache/restart-httpd/main.yml:13
fatal: [jota-node01 -> localhost]: FAILED! => {"changed": false, "module_stderr": "su: Authentication failure\n", "module_stdout": "", "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error", "rc": 1}

Tenemos varias opciones:

  • Que la contraseña para realizar la escalada de privilegios sea igual en target que en control.
  • Especificar la variable ansible_become_pass para el host en el inventario.
  • localhost ansible_connection=local ansible_become_pass=test123
    

    Y en el handler poner la opción "delegate_facts: True"

      handlers:
        - name: email-notification
          mail:
            host: smtp.gmail.com
            port: 587
            username: mail@example.com
            password: *******
            from: SYSTEM
            to: example <mail@example.com>
            subject: Ansible-Report Apache Web Server restarted
            body: |-
              Apache Web Server restarted in:
              {% for item in ansible_play_hosts_all %}
              - Server: {{ item }}
              {% endfor %}
          delegate_to: localhost
          delegate_facts: True 
          run_once: true
    
  • Si el handler no necesita escalada de privilegios como en nuestro caso, es mejor especificarlo en el scope del handler con "become: no"
  •   handlers:
        - name: email-notification
          become: no                                                                                                                                                                                              
          mail:
            host: smtp.gmail.com
            port: 587
            username: mail@example.com
            password: *******
            from: SYSTEM
            to: example <mail@example.com>
            subject: Ansible-Report Apache Web Server restarted
            body: |-
              Apache Web Server restarted in:
              {% for item in ansible_play_hosts_all %}
              - Server: {{ item }}
              {% endfor %}
          delegate_to: localhost
          run_once: true
    

    Lanzamos:

    ansible-playbook main.yml -e "server=jota-node01" --ask-become -vv
    

    Ahora la ejecución finaliza sin errores:

    RUNNING HANDLER [email-notification] ********************************************************************
    task path: /ansible/playbooks/apache/restart-httpd/main.yml:13
    ok: [jota-node01 -> localhost] => {"changed": false, "msg": "Mail sent successfully", "result": {}}