Controlar la Ejecución de Playbooks de Ansible en RHEL

Red Hat Enterprise LinuxBeginner
Practicar Ahora

Introducción

En este laboratorio, aprenderá a controlar el flujo de ejecución de los playbooks de Ansible en un sistema Red Hat Enterprise Linux (RHEL). Comenzará escribiendo un playbook que utiliza estructuras de control fundamentales, incluyendo bucles para repetir tareas de manera eficiente y condicionales para ejecutar tareas solo cuando se cumplen criterios específicos. También implementará handlers para activar acciones, como reinicios de servicios, solo cuando ocurre un cambio, haciendo su automatización más inteligente y eficiente.

Basándose en estas habilidades fundamentales, explorará técnicas más avanzadas para gestionar la ejecución de playbooks. Esto incluye el uso de sentencias block y rescue para manejar fallos de tareas de manera elegante y el empleo de changed_when y failed_when para obtener un control detallado sobre el estado de las tareas. Para concluir el laboratorio, aplicará todos estos conceptos en un ejercicio práctico para desplegar un servidor web seguro, consolidando su capacidad para crear automatizaciones de Ansible robustas y confiables.

Este es un Guided Lab, que proporciona instrucciones paso a paso para ayudarte a aprender y practicar. Sigue las instrucciones cuidadosamente para completar cada paso y obtener experiencia práctica. Los datos históricos muestran que este es un laboratorio de nivel principiante con una tasa de finalización del 89%. Ha recibido una tasa de reseñas positivas del 100% por parte de los estudiantes.

Escribir un Playbook con Bucles y Condicionales

En este paso, aprenderá dos conceptos fundamentales en Ansible para controlar la ejecución de tareas: bucles y condicionales. Los bucles le permiten repetir una tarea varias veces con diferentes valores, lo cual es muy eficiente para tareas como instalar múltiples paquetes o crear múltiples usuarios. Los condicionales, utilizando la palabra clave when, le permiten ejecutar una tarea solo cuando se cumplen criterios específicos, como que el sistema operativo sea de una versión particular o que un archivo ya exista.

Primero, asegúrese de que Ansible esté instalado en su VM de LabEx. Utilizaremos el gestor de paquetes DNF para esto.

sudo dnf install -y ansible-core

Debería ver una salida que indica que ansible-core y sus dependencias se están instalando.

...
Installed:
  ansible-core-2.x.x-1.el9.x86_64
  ...
Complete!

Ahora, configuremos nuestro directorio de proyecto. Todo nuestro trabajo para este laboratorio estará dentro de un directorio dedicado para mantener las cosas organizadas.

cd ~/project
mkdir control-flow-lab
cd control-flow-lab

Un proyecto de Ansible necesita un archivo de inventario, que define los hosts que desea gestionar. Para este laboratorio, gestionaremos la máquina local, localhost.

Cree un archivo de inventario llamado inventory usando el editor nano:

nano inventory

Agregue la siguiente línea al archivo. Esto le dice a Ansible que ejecute el playbook en localhost y que se conecte directamente a él en lugar de usar SSH.

localhost ansible_connection=local

Guarde el archivo y salga de nano presionando Ctrl+X, luego Y, y Enter.

A continuación, crearemos nuestro primer playbook, playbook.yml, para demostrar un bucle. Este playbook instalará una lista de herramientas útiles de línea de comandos.

nano playbook.yml

Ingrese el siguiente contenido YAML en el editor. Este playbook define una tarea que utiliza el módulo ansible.builtin.dnf para instalar paquetes. La directiva become: yes le dice a Ansible que ejecute las tareas con privilegios de sudo, lo cual es necesario para instalar paquetes. La palabra clave loop proporciona una lista de nombres de paquetes. Ansible ejecutará esta tarea una vez por cada elemento de la lista, sustituyendo el marcador de posición {{ item }} con el nombre del paquete actual.

---
- name: Install common tools
  hosts: localhost
  become: yes
  tasks:
    - name: Install specified packages
      ansible.builtin.dnf:
        name: "{{ item }}"
        state: present
      loop:
        - git
        - tree
        - wget

Guarde y salga del editor. Ahora, ejecute el playbook usando el comando ansible-playbook, especificando su archivo de inventario con la bandera -i.

ansible-playbook -i inventory playbook.yml

La salida mostrará la ejecución del playbook. Ansible verificará cada paquete y lo instalará si aún no está presente. El PLAY RECAP al final resume los resultados.

PLAY [Install tools and run conditional tasks] *********************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Install specified packages] **********************************************
changed: [localhost] => (item=git)
changed: [localhost] => (item=tree)
changed: [localhost] => (item=wget)

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Ahora, modifiquemos el playbook para incluir una tarea condicional. Agregaremos una tarea que imprima un mensaje, pero solo si el sistema operativo es Red Hat Enterprise Linux. Este es un caso de uso común para adaptar la automatización a entornos específicos.

Abra el archivo playbook.yml nuevamente:

nano playbook.yml

Agregue las siguientes tareas al final del archivo. La palabra clave when evalúa la expresión dada. ansible_facts['distribution'] es una variable que Ansible descubre automáticamente sobre el host gestionado. La primera tarea se ejecutará porque nuestro entorno es RHEL, y la segunda tarea se omitirá.

---
- name: Install tools and run conditional tasks
  hosts: localhost
  become: yes
  tasks:
    - name: Install specified packages
      ansible.builtin.dnf:
        name: "{{ item }}"
        state: present
      loop:
        - git
        - tree
        - wget

    - name: Show message on Red Hat systems
      ansible.builtin.debug:
        msg: "This system is a Red Hat family distribution."
      when: ansible_facts['distribution'] == "RedHat"

    - name: Show message on other systems
      ansible.builtin.debug:
        msg: "This system is NOT a Red Hat family distribution."
      when: ansible_facts['distribution'] != "RedHat"

Guarde y salga del editor. Ejecute el playbook actualizado:

ansible-playbook -i inventory playbook.yml

Observe la salida cuidadosamente. La tarea de instalación de paquetes probablemente informará ok para todos los elementos, ya que ya están instalados. Más importante aún, verá que se imprime el primer mensaje de depuración, mientras que el segundo se marca como skipping.

PLAY [Install tools and run conditional tasks] *********************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [Install specified packages] **********************************************
ok: [localhost] => (item=git)
ok: [localhost] => (item=tree)
ok: [localhost] => (item=wget)

TASK [Show message on Red Hat systems] *****************************************
ok: [localhost] => {
    "msg": "This system is a Red Hat family distribution."
}

TASK [Show message on other systems] *******************************************
skipping: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0    skipped=1    rescued=0    ignored=0

Ha escrito y ejecutado con éxito un playbook de Ansible que utiliza tanto bucles para realizar acciones repetitivas como condicionales para controlar la ejecución de tareas basándose en los hechos del sistema.

Implementar Handlers para Disparar Reinicios de Servicio

En este paso, aprenderá sobre los handlers de Ansible. Los handlers son tareas especiales que solo se ejecutan cuando son "notificados" por otra tarea. Típicamente se utilizan para acciones que solo deben ocurrir cuando se ha realizado un cambio, como reiniciar un servicio después de que su archivo de configuración haya sido actualizado. Este enfoque es más eficiente que reiniciar un servicio en cada ejecución del playbook, ya que asegura que la acción solo se tome cuando sea necesario.

Crearemos un playbook que instale el servidor web Nginx, despliegue una página de inicio personalizada y utilice un handler para recargar Nginx solo cuando el contenido de la página de inicio cambie.

Primero, creemos un nuevo directorio para este ejercicio para mantener nuestro proyecto organizado.

cd ~/project
mkdir control-handlers-lab
cd control-handlers-lab

Como antes, necesitamos un archivo de inventario para indicarle a Ansible dónde ejecutar el playbook.

nano inventory

Agregue la siguiente línea para especificar la máquina local.

localhost ansible_connection=local

Guarde y salga del editor (Ctrl+X, Y, Enter).

A continuación, necesitamos un archivo que sirva como página de inicio de nuestro servidor web. Crearemos un directorio files para almacenarlo.

mkdir files

Ahora, cree un archivo simple index.html dentro del directorio files.

nano files/index.html

Agregue el siguiente contenido HTML:

<h1>Welcome to the Ansible Handler Lab!</h1>

Guarde y salga del editor.

Ahora, creará el playbook deploy_nginx.yml. Este playbook realizará tres acciones principales: instalar Nginx, copiar el archivo index.html y definir un handler para recargar Nginx.

nano deploy_nginx.yml

Ingrese el siguiente contenido. Preste mucha atención a la palabra clave notify en la tarea "Copy homepage" y a la sección handlers correspondiente al final. La directiva become: yes le dice a Ansible que ejecute las tareas con privilegios de sudo, lo cual es necesario para instalar paquetes y gestionar servicios.

---
- name: Deploy Nginx with a handler
  hosts: localhost
  become: yes
  tasks:
    - name: Ensure Nginx is installed
      ansible.builtin.dnf:
        name: nginx
        state: present

    - name: Start and enable Nginx service
      ansible.builtin.systemd:
        name: nginx
        state: started
        enabled: yes

    - name: Copy homepage
      ansible.builtin.copy:
        src: files/index.html
        dest: /usr/share/nginx/html/index.html
      notify: reload nginx

  handlers:
    - name: reload nginx
      ansible.builtin.systemd:
        name: nginx
        state: reloaded

Guarde y salga del editor.

Ahora, ejecute el playbook por primera vez.

ansible-playbook -i inventory deploy_nginx.yml

Verá una salida que muestra que Nginx fue instalado (o ya estaba presente), el servicio Nginx se inició y habilitó, el archivo index.html se copió (estado changed), y lo que es importante, el handler fue notificado y ejecutado al final del play.

...
TASK [Copy homepage] ***********************************************************
changed: [localhost]

RUNNING HANDLER [reload nginx] *************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=3    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Puede verificar que el servidor web se está ejecutando y sirviendo su página personalizada usando curl.

curl http://localhost

La salida debe ser el contenido de su archivo index.html.

<h1>Welcome to the Ansible Handler Lab!</h1>

Ahora, ejecute exactamente el mismo playbook nuevamente sin realizar ningún cambio.

ansible-playbook -i inventory deploy_nginx.yml

Esta vez, observe la salida. La tarea "Copy homepage" informará ok en lugar de changed porque el archivo en el destino ya coincide con la fuente. La tarea "Start and enable Nginx service" también informará ok ya que el servicio ya se está ejecutando y habilitado. Debido a que ninguna tarea notificó al handler, el handler no se ejecutó.

...
TASK [Copy homepage] ***********************************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Para ver el handler en acción nuevamente, modifiquemos el archivo fuente index.html.

nano files/index.html

Cambie el contenido a lo siguiente:

<h1>The Handler Ran Again!</h1>

Guarde y salga. Ahora, ejecute el playbook una vez más.

ansible-playbook -i inventory deploy_nginx.yml

Debido a que el archivo fuente cambió, la tarea "Copy homepage" informará nuevamente changed, lo que a su vez notifica y ejecuta el handler reload nginx.

...
TASK [Copy homepage] ***********************************************************
changed: [localhost]

RUNNING HANDLER [reload nginx] *************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Verifique el cambio con curl una última vez.

curl http://localhost

Debería ver el mensaje actualizado.

<h1>The Handler Ran Again!</h1>

Este ejercicio demuestra el poder y la eficiencia de los handlers para gestionar el estado del servicio en respuesta a cambios de configuración.

Gestionar Fallos de Tareas con Block y Rescue

En este paso, aprenderá cómo manejar errores de forma elegante en sus playbooks de Ansible. Por defecto, si alguna tarea falla, Ansible deja de ejecutar todo el playbook en ese host. Si bien este es un comportamiento predeterminado seguro, a veces necesita más control. Explorará dos métodos para el manejo de errores: la directiva simple ignore_errors y la estructura más potente block, rescue y always, que proporciona una forma de intentar tareas y definir acciones de recuperación si fallan.

Primero, creemos un nuevo directorio para este ejercicio.

cd ~/project
mkdir control-errors-lab
cd control-errors-lab

Cree el archivo inventory estándar para localhost.

nano inventory

Agregue el siguiente contenido:

localhost ansible_connection=local

Guarde y salga del editor (Ctrl+X, Y, Enter).

Ahora, creemos un playbook llamado playbook.yml que está diseñado para fallar. La primera tarea intentará instalar un paquete que no existe.

nano playbook.yml

Ingrese el siguiente contenido. Este playbook intenta instalar un paquete falso httpd-fake y luego un paquete real, mariadb-server.

---
- name: Demonstrate Task Failure
  hosts: localhost
  become: yes
  tasks:
    - name: Attempt to install a non-existent package
      ansible.builtin.dnf:
        name: httpd-fake
        state: present

    - name: Install MariaDB server
      ansible.builtin.dnf:
        name: mariadb-server
        state: present

Guarde y salga del editor. Ahora, ejecute el playbook.

ansible-playbook -i inventory playbook.yml

Verá que la primera tarea falla con un mensaje de error porque no se puede encontrar el paquete httpd-fake. Crucialmente, Ansible se detendrá y la segunda tarea, "Install MariaDB server", no se ejecutará.

...
TASK [Attempt to install a non-existent package] *******************************
fatal: [localhost]: FAILED! => {"changed": false, "msg": "No match for argument: httpd-fake", "rc": 1, "results": []}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

Ahora, usemos block y rescue para manejar este fallo de manera más elegante. La palabra clave block agrupa un conjunto de tareas. Si alguna tarea dentro del block falla, Ansible omite el resto de las tareas en el block y ejecuta las tareas en la sección rescue. La sección always se ejecutará independientemente de si las secciones block o rescue tuvieron éxito o fallaron.

Modifique playbook.yml para usar esta estructura.

nano playbook.yml

Reemplace todo el contenido con lo siguiente. Aquí, intentamos instalar el paquete falso en el block. Cuando falla, la sección rescue se ejecutará, instalando mariadb-server como un paso de recuperación. La sección always imprimirá un mensaje al final.

---
- name: Handle Task Failure with Block and Rescue
  hosts: localhost
  become: yes
  tasks:
    - name: Attempt primary task, with recovery
      block:
        - name: Attempt to install a non-existent package
          ansible.builtin.dnf:
            name: httpd-fake
            state: present
        - name: This task will be skipped
          ansible.builtin.debug:
            msg: "This message will not appear because the previous task fails."
      rescue:
        - name: Install MariaDB server on failure
          ansible.builtin.dnf:
            name: mariadb-server
            state: present
      always:
        - name: This always runs
          ansible.builtin.debug:
            msg: "The block has completed, either by success or rescue."

Guarde y salga. Ejecute el playbook nuevamente.

ansible-playbook -i inventory playbook.yml

Observe la salida. La primera tarea en el block falla como se esperaba. La segunda tarea en el block se omite. Ansible luego pasa a la sección rescue e instala mariadb-server con éxito. Finalmente, se ejecuta la sección always.

...
TASK [Attempt to install a non-existent package] *******************************
fatal: [localhost]: FAILED! => ...

TASK [This task will be skipped] ***********************************************
skipping: [localhost]

RESCUE START *******************************************************************

TASK [Install MariaDB server on failure] ***************************************
changed: [localhost]

ALWAYS START *******************************************************************

TASK [This always runs] ********************************************************
ok: [localhost] => {
    "msg": "The block has completed, either by success or rescue."
}

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=0    skipped=1    rescued=1    ignored=0

Ahora, veamos qué sucede cuando el block tiene éxito. Edite el playbook y corrija el nombre del paquete.

nano playbook.yml

Cambie httpd-fake a un paquete real, httpd.

## ... (rest of the playbook)
block:
  - name: Attempt to install a valid package
    ansible.builtin.dnf:
      name: httpd ## Corrected from httpd-fake
      state: present
  - name: This task will now run
    ansible.builtin.debug:
      msg: "This message will now appear because the previous task succeeds."
## ... (rest of the playbook)

Guarde y salga. Ejecute el playbook una última vez.

ansible-playbook -i inventory playbook.yml

Esta vez, ambas tareas en el block tienen éxito. Debido a que el block se completó sin errores, la sección rescue se omite por completo. La sección always todavía se ejecuta, como su nombre indica.

...
TASK [Attempt to install a valid package] **************************************
changed: [localhost]

TASK [This task will now run] **************************************************
ok: [localhost] => {
    "msg": "This message will now appear because the previous task succeeds."
}

RESCUE START *******************************************************************
skipping rescue

ALWAYS START *******************************************************************

TASK [This always runs] ********************************************************
ok: [localhost] => {
    "msg": "The block has completed, either by success or rescue."
}

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Ahora ha utilizado con éxito la estructura block/rescue/always para crear un playbook robusto que puede manejar fallos y realizar acciones de recuperación.

Controlar el Estado de las Tareas con changed_when y failed_when

En este paso, obtendrá un control más preciso sobre cómo Ansible interpreta el resultado de sus tareas. Aprenderá sobre dos directivas potentes: changed_when y failed_when.

  • changed_when: Por defecto, módulos como ansible.builtin.command o ansible.builtin.shell casi siempre informan un estado de "cambiado", incluso si el comando que ejecutaron no alteró el sistema. changed_when le permite definir una condición personalizada que determina si una tarea debe informarse como "cambiada". Esto es crucial para escribir playbooks idempotentes y para disparar handlers de manera precisa.
  • failed_when: A veces, un comando puede salir con un código de estado distinto de cero (que Ansible considera un fallo) incluso cuando el resultado es aceptable. failed_when le permite anular las condiciones de fallo predeterminadas, permitiendo que su playbook continúe basándose en criterios más inteligentes, como la salida del comando o un código de salida específico.

Comencemos configurando un nuevo directorio de proyecto.

cd ~/project
mkdir control-state-lab
cd control-state-lab

Cree el archivo inventory estándar para localhost.

nano inventory

Agregue el siguiente contenido:

localhost ansible_connection=local

Guarde y salga del editor (Ctrl+X, Y, Enter).

Usando changed_when

Primero, veamos cómo se comporta una tarea de comando por defecto. Crearemos un playbook que ejecute el comando date. Este comando simplemente imprime la fecha y no cambia el sistema, pero el módulo command lo informará como un cambio.

Cree un nuevo playbook llamado playbook.yml.

nano playbook.yml

Ingrese el siguiente contenido:

---
- name: Control Task State
  hosts: localhost
  tasks:
    - name: Check local time (default behavior)
      ansible.builtin.command: date

Guarde y salga. Ahora, ejecute el playbook.

ansible-playbook -i inventory playbook.yml

Observe en la salida que la tarea se informa como changed=1, a pesar de que nada en el sistema fue modificado.

...
TASK [Check local time (default behavior)] *************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    ...

Ahora, usemos changed_when para decirle a Ansible que este comando nunca cambia el sistema. Modifique playbook.yml.

nano playbook.yml

Agregue changed_when: false a la tarea.

---
- name: Control Task State
  hosts: localhost
  tasks:
    - name: Check local time (with changed_when)
      ansible.builtin.command: date
      changed_when: false

Guarde y salga. Ejecute el playbook nuevamente.

ansible-playbook -i inventory playbook.yml

Esta vez, la tarea informa ok y el resumen final muestra changed=0. Ha anulado con éxito el comportamiento predeterminado.

...
TASK [Check local time (with changed_when)] ************************************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    ...

Usando failed_when

A continuación, exploremos failed_when. Crearemos una tarea que verifique la existencia de un archivo que no está allí. El comando "fallará" por defecto.

Primero, cree un archivo ficticio para buscar dentro.

echo "System is running" > status.txt

Ahora, modifique playbook.yml para buscar la palabra "ERROR" en este archivo. El comando grep saldrá con un código de estado de 1 porque la palabra no se encuentra, lo que Ansible interpreta como un fallo.

nano playbook.yml

Reemplace el contenido con lo siguiente:

---
- name: Control Task State
  hosts: localhost
  tasks:
    - name: Check for ERROR in status file (will fail)
      ansible.builtin.command: grep ERROR status.txt

Guarde y salga. Ejecute el playbook.

ansible-playbook -i inventory playbook.yml

Como se esperaba, la ejecución del playbook se detiene con un mensaje FAILED!.

...
TASK [Check for ERROR in status file (will fail)] ******************************
fatal: [localhost]: FAILED! => {"changed": true, "cmd": ["grep", "ERROR", "status.txt"], "delta": "...", "end": "...", "msg": "non-zero return code", "rc": 1, ...}
...

Esto no es lo que queremos. La ausencia de "ERROR" es una condición de éxito para nosotros. Podemos usar failed_when para redefinir qué constituye un fallo. Le diremos a Ansible que solo falle si el código de retorno del comando es mayor que 1. Un código de retorno de 1 (patrón no encontrado) ahora se considerará un éxito. También necesitamos register el resultado de la tarea para inspeccionar su código de retorno (rc).

Modifique playbook.yml una última vez.

nano playbook.yml

Actualice el playbook con register y failed_when.

---
- name: Control Task State
  hosts: localhost
  tasks:
    - name: Check for ERROR in status file (with failed_when)
      ansible.builtin.command: grep ERROR status.txt
      register: grep_result
      failed_when: grep_result.rc > 1
      changed_when: false

También agregamos changed_when: false porque grep es una operación de solo lectura y no cambia el sistema.

Guarde y salga. Ejecute el playbook final.

ansible-playbook -i inventory playbook.yml

¡Éxito! La tarea ahora informa ok porque su código de retorno fue 1, lo que no cumple nuestra nueva condición de fallo (rc > 1). El playbook se completa con éxito.

...
TASK [Check for ERROR in status file (with failed_when)] ***********************
ok: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    ...

Ahora ha aprendido a usar changed_when y failed_when para definir con precisión los estados de éxito, cambio y fallo de sus tareas, lo que lleva a una automatización más robusta e inteligente.

Desplegar un Servidor Web Seguro Usando Control de Tareas

En este paso final, combinará todos los conceptos que ha aprendido (bucles, condicionales, handlers y manejo de errores) para construir un playbook único y robusto. El objetivo es desplegar el servidor web Apache (httpd), asegurarlo con mod_ssl, generar un certificado SSL autofirmado y desplegar una página de inicio personalizada. Este ejercicio práctico imita una tarea de automatización del mundo real.

Primero, configuremos el directorio del proyecto para este ejercicio culminante.

cd ~/project
mkdir control-review-lab
cd control-review-lab

Como siempre, cree un archivo inventory para definir su host de destino.

nano inventory

Agregue la entrada localhost:

localhost ansible_connection=local

Guarde y salga del editor (Ctrl+X, Y, Enter).

A continuación, necesitamos un directorio para almacenar los archivos que desplegará nuestro playbook.

mkdir files

Ahora, cree una página de inicio personalizada, index.html, dentro del directorio files.

nano files/index.html

Agregue el siguiente contenido HTML. Esta será la página servida por nuestro servidor web seguro.

<h1>Secure Web Server Deployed by Ansible!</h1>
<p>This page is served over HTTPS.</p>

Guarde y salga del editor.

Ahora es el momento de construir el playbook principal, deploy_secure_web.yml. Este playbook será más complejo que los anteriores, integrando múltiples conceptos.

nano deploy_secure_web.yml

Ingrese el siguiente playbook completo. Lea los comentarios dentro del código para comprender cómo cada parte contribuye al objetivo general.

---
- name: Deploy a Secure Apache Web Server
  hosts: localhost
  become: yes
  vars:
    packages_to_install:
      - httpd
      - mod_ssl
    ssl_cert_path: /etc/pki/tls/certs/localhost.crt
    ssl_key_path: /etc/pki/tls/private/localhost.key

  tasks:
    - name: Stop nginx to free port 80
      ansible.builtin.systemd:
        name: nginx
        state: stopped
      ignore_errors: yes

    - name: Install httpd and mod_ssl packages
      ansible.builtin.dnf:
        name: "{{ packages_to_install }}"
        state: present

    - name: Generate self-signed SSL certificate if it does not exist
      ansible.builtin.command: >
        openssl req -new -nodes -x509
        -subj "/C=US/ST=None/L=None/O=LabEx/CN=localhost"
        -keyout {{ ssl_key_path }}
        -out {{ ssl_cert_path }}
      args:
        creates: "{{ ssl_cert_path }}"

    - name: Deploy custom index.html
      ansible.builtin.copy:
        src: files/index.html
        dest: /var/www/html/index.html
      notify: restart httpd

    - name: Start and enable httpd service
      ansible.builtin.systemd:
        name: httpd
        state: started
        enabled: yes

  handlers:
    - name: restart httpd
      ansible.builtin.systemd:
        name: httpd
        state: restarted

Analicemos lo que hace este playbook:

  • vars: Define variables para los paquetes a instalar y las rutas para el certificado y la clave SSL, lo que hace que el playbook sea más fácil de leer y mantener.
  • Tarea de Detener Nginx: Detiene el servicio nginx del paso de laboratorio anterior para liberar el puerto 80 para Apache. Utiliza ignore_errors: yes en caso de que nginx no se esté ejecutando.
  • Tarea de Instalación: Utiliza la variable packages_to_install para instalar tanto httpd como mod_ssl.
  • Tarea de Generación de Certificado: Esta es una tarea clave. Utiliza el comando openssl para crear un certificado autofirmado. La directiva args: { creates: ... } hace que esta tarea sea idempotente. El comando solo se ejecutará si el archivo del certificado (/etc/pki/tls/certs/localhost.crt) aún no existe.
  • Tarea de Despliegue de Página de Inicio: Copia su index.html personalizado. Crucialmente, utiliza notify: restart httpd para activar el handler si el archivo se modifica.
  • Tarea de Inicio de Servicio: Utiliza el módulo systemd para iniciar y habilitar el servicio httpd después de que toda la configuración esté en su lugar, asegurando que se inicie al arrancar.
  • Handler: El handler restart httpd realiza un reinicio de Apache usando systemd, que solo se activa cuando cambia un archivo de configuración o contenido.

Guarde y salga del editor. Ahora, ejecute su playbook completo.

ansible-playbook -i inventory deploy_secure_web.yml

En la primera ejecución, debería ver varias tareas informando changed, incluyendo la detención de nginx, la instalación de paquetes, la generación de certificados, la copia de archivos y el inicio del servicio.

...
TASK [Start and enable httpd service] ******************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost                  : ok=6    changed=5    unreachable=0    failed=0    ...

Finalmente, verifique que su servidor web seguro esté funcionando. Primero pruebe la versión HTTP, luego la versión HTTPS con la bandera -k para ignorar las advertencias sobre el certificado autofirmado.

curl http://localhost

Debería ver el contenido de su página de inicio personalizada.

<h1>Secure Web Server Deployed by Ansible!</h1>
<p>This page is served over HTTPS.</p>

También puede probar la versión HTTPS:

curl -k https://localhost

Si vuelve a ejecutar el playbook, verá que ninguna tarea informa changed, y el handler no se ejecuta, lo que demuestra que su playbook es idempotente.

¡Felicitaciones! Ha creado con éxito un playbook de Ansible práctico y robusto que combina bucles, variables, ejecución de comandos idempotente y handlers para desplegar una aplicación segura.

Resumen

En este laboratorio, aprendió a controlar la ejecución de playbooks de Ansible en un sistema RHEL. Comenzó configurando un entorno de proyecto básico, incluida la instalación de Ansible y la creación de un archivo de inventario. Luego exploró estructuras fundamentales de flujo de control, utilizando bucles para repetir eficientemente tareas con diferentes entradas y condicionales con la declaración when para ejecutar tareas solo bajo circunstancias específicas. Basándose en esto, implementó handlers para crear automatizaciones receptivas, como activar un reinicio de servicio solo cuando su archivo de configuración ha sido modificado.

El laboratorio también cubrió técnicas avanzadas para gestionar la ejecución de playbooks. Aprendió a crear playbooks más robustos utilizando cláusulas block y rescue para manejar fallos de tareas de manera elegante. Además, obtuvo un control detallado sobre los resultados de las tareas utilizando changed_when y failed_when para definir condiciones personalizadas de éxito y fallo. Finalmente, consolidó todas estas habilidades aplicándolas a un escenario práctico: el despliegue de un servidor web seguro, demostrando cómo combinar eficazmente bucles, condicionales, handlers y manejo de errores en un flujo de trabajo de automatización del mundo real.