Día 89 - Ansible Manage Services (módulo service: started vs restarted, enabled, idempotencia)
Problema / Desafío
Los desarrolladores necesitan dependencias instaladas y corriendo en los app servers de Stratos DC. Como ahora la instalación de paquetes y la gestión de servicios se hace con Ansible, hay que crear y probar el playbook. La tarea:
- Crear
/home/thor/ansible/playbook.ymlque instalehttpden todos los app servers - Tras la instalación, arrancar y habilitar el servicio
httpden todos - El inventario
/home/thor/ansible/inventoryya existe - El user
thordebe poder correr el playbook desde el jump host
Restricción de validación: se corre con ansible-playbook -i inventory playbook.yml — sin argumentos extra. El become debe vivir dentro del playbook.
Nota sobre el solape con el Día 88: este lab es casi un subconjunto del anterior (instalar
httpd+ arrancar servicio), pero sin el paso deblockinfile. Por eso este día se enfoca en el móduloserviceen profundidad — el verdadero protagonista de la tarea. Elcurl localhostde la verificación devuelve la página de prueba por defecto de Apache (<!DOCTYPE html>...) justamente porque hoy no se crea ningúnindex.html— eso es lo que hacía elblockinfiledel Día 88.
Conceptos clave
El módulo service — gestionar el ciclo de vida de un servicio
El módulo ansible.builtin.service controla el estado de un servicio del sistema de forma declarativa e idempotente:
| Parámetro | Función |
|---|---|
name: |
Nombre del servicio (httpd, nginx, sshd, …) |
state: |
Estado deseado: started, stopped, restarted, reloaded |
enabled: |
yes/no — si arranca automáticamente al bootear |
daemon_reload: |
Recargar systemd antes de actuar (tras cambiar un unit file) |
Los state del módulo service — qué hace cada uno
state |
Acción | ¿Idempotente? |
|---|---|---|
started |
Arranca el servicio si no está corriendo | Sí — no toca uno ya corriendo |
stopped |
Detiene el servicio si está corriendo | Sí |
restarted |
Siempre detiene y vuelve a arrancar | No — bouncea en cada corrida |
reloaded |
Recarga la config sin matar el proceso (si el servicio lo soporta) | No (siempre actúa) |
La distinción clave del día:
state: started # "que esté corriendo" — declarativo, idempotente
state: restarted # "reinícialo" — imperativo, corre SIEMPRE
started: el correcto para "asegurar que el servicio esté arriba". En un segundo run, si ya corre, daok(nochanged). No interrumpe el servicio.restarted: bouncea el servicio en cada ejecución del playbook — útil tras un cambio de config, pero un error si solo se quería garantizar que esté arriba (causa downtime innecesario en cada run).
Regla práctica: usar
startedpara garantizar disponibilidad; reservarrestartedpara cuando una config cambió y el servicio debe releer (idealmente vía handler, ver más abajo).
state vs enabled — dos ejes ortogonales
Son dos preguntas independientes:
state: started → ¿está corriendo AHORA (este boot)?
enabled: yes → ¿arrancará SOLO en el PRÓXIMO boot?
state |
enabled |
Resultado |
|---|---|---|
started |
yes |
Corre ahora y sobrevive reboots ← lo que pide el lab |
started |
no |
Corre ahora pero muere tras un reboot |
stopped |
yes |
Apagado ahora, pero arrancará en el próximo boot |
stopped |
no |
Apagado ahora y no arranca al bootear |
El lab pide explícitamente "start and enable" → ambos en el mismo task. Olvidar enabled: yes es un bug silencioso: pasa la validación inicial pero el servidor queda caído tras el primer reinicio.
Idempotencia: la prueba está en el segundo run
# Primera corrida (httpd recién instalado, servicio parado):
TASK [start & enabled]
changed: [stapp01] ← lo arrancó + habilitó
# Segunda corrida (ya corre + ya habilitado):
TASK [start & enabled]
ok: [stapp01] ← no hizo nada, ya estaba en el estado deseado
El changed=3 del output del lab confirma que fue la primera vez que se arrancó en los tres servers. Un started idempotente nunca causa downtime en runs repetidos — a diferencia de restarted.
service vs systemd vs systemd_service — elegir el módulo
| Módulo | Para |
|---|---|
ansible.builtin.service |
Genérico — detecta el init system (systemd, SysV, upstart, BSD) |
ansible.builtin.systemd_service (alias systemd) |
Específico systemd — expone daemon_reload, masked, scopes |
service es el wrapper portable: en un host con systemd (RHEL 7+), delega en systemd por debajo. Se usa service cuando solo se necesita start/stop/enable básico; se baja a systemd cuando hace falta algo específico de systemd:
- name: recargar unit + reiniciar
ansible.builtin.systemd:
name: httpd
state: restarted
daemon_reload: true # ← releer unit files tras editarlos (solo systemd)
El patrón idiomático que falta hoy: handlers (notify)
Hoy se arranca el servicio en un task normal. Pero el patrón correcto para "reiniciar un servicio cuando su config cambió" es el handler:
tasks:
- name: desplegar config de httpd
ansible.builtin.copy:
src: httpd.conf
dest: /etc/httpd/conf/httpd.conf
notify: restart httpd # ← solo dispara si este task hizo 'changed'
handlers:
- name: restart httpd
ansible.builtin.service:
name: httpd
state: restarted
Diferencia clave:
- Un task con
state: restartedsiempre reinicia. - Un handler con
notifysolo reinicia si el task que lo notifica cambió algo, y corre una sola vez al final del play (aunque lo notifiquen varios tasks).
Esto evita el downtime innecesario de reiniciar en cada run. El lab de hoy no lo requiere (solo "start + enable"), pero es la evolución natural de la gestión de servicios — vale tenerlo en el radar.
El playbook completo — anatomía
- hosts: all # los 3 app servers
become: true # root: instalar y gestionar servicios requiere privilegios
tasks:
- name: install packages
ansible.builtin.yum: # ← Día 87
name: httpd
state: present
- name: start & enabled
ansible.builtin.service: # ← protagonista de hoy
name: httpd
state: started
enabled: yes
become: true está en el play — clave para que la validación corra sin --become. Gestionar servicios (start/stop/enable) es una operación privilegiada igual que instalar paquetes.
Pasos
- Login al jump host como
thor;cd /home/thor/ansible - Validar conectividad:
ansible -i inventory all -m ping - Crear
/home/thor/ansible/playbook.ymlcon dos tasks:yum(instalar) →service(arrancar + habilitar) - Correr
ansible-playbook -i inventory playbook.yml - Validar que el servicio está
activeyenableden los 3 hosts
Comandos / Código
Playbook (solución utilizada)
# /home/thor/ansible/playbook.yml
- hosts: all
become: true
tasks:
- name: install packages
ansible.builtin.yum:
name: httpd
state: present
- name: start & enabled
ansible.builtin.service:
name: httpd
state: started
enabled: yes
- name: probe httpd
ansible.builtin.shell: |
curl localhost
register: server_content
- name: server ok
ansible.builtin.debug:
var: server_content.stdout[:30]
Ejecutar el playbook
Output real del lab (recortado):
TASK [install packages] ********************************
changed: [stapp01]
changed: [stapp02]
changed: [stapp03]
TASK [start & enabled] *********************************
changed: [stapp03]
changed: [stapp01]
changed: [stapp02]
TASK [probe httpd] *************************************
changed: [stapp01]
changed: [stapp02]
changed: [stapp03]
TASK [server ok] **************************************
ok: [stapp01] => {
"server_content.stdout[:30]": "<!DOCTYPE html>\n<html lang=\"en"
}
PLAY RECAP ********************************************
stapp01 : ok=5 changed=3 unreachable=0 failed=0
stapp02 : ok=5 changed=3 unreachable=0 failed=0
stapp03 : ok=5 changed=3 unreachable=0 failed=0
changed=3 (yum + service + probe). El <!DOCTYPE html> es la página de prueba por defecto de Apache — no hay index.html propio (eso era el Día 88).
El
[:30]enserver_content.stdout[:30]es slicing de Python/Jinja2: toma los primeros 30 caracteres del output para no inundar el log con todo el HTML. Truco útil para verificaciones de debug.
Verificación (alternativas)
# A. Estado del servicio en los 3 de una vez
ansible -i inventory all -m shell -a "systemctl is-active httpd" --become
ansible -i inventory all -m shell -a "systemctl is-enabled httpd" --become
# esperado: active / enabled
# B. Con el propio módulo service en check mode (no cambia nada, reporta estado)
ansible -i inventory all -m ansible.builtin.service \
-a "name=httpd state=started enabled=yes" --become --check
# C. Confirmar que responde HTTP
ansible -i inventory all -m uri -a "url=http://localhost" --become
Variantes (referencia)
Reiniciar solo cuando cambia la config (handler) — el patrón correcto
tasks:
- name: desplegar config
ansible.builtin.template:
src: httpd.conf.j2
dest: /etc/httpd/conf/httpd.conf
notify: restart httpd
handlers:
- name: restart httpd
ansible.builtin.service:
name: httpd
state: restarted
Recargar sin downtime (si el servicio lo soporta)
- name: reload httpd
ansible.builtin.service:
name: httpd
state: reloaded # relee config sin matar el proceso (graceful)
Específico de systemd + daemon_reload
- name: tras editar un unit file
ansible.builtin.systemd:
name: httpd
state: restarted
daemon_reload: true
enabled: true
Gestionar varios servicios con loop
- name: arrancar + habilitar varios servicios
ansible.builtin.service:
name: "{{ item }}"
state: started
enabled: yes
loop:
- httpd
- firewalld
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
Could not find the requested service httpd: host |
El servicio no existe → httpd no está instalado todavía |
Asegurar que el task yum corre antes que el service |
Interactive authentication required / permisos |
Falta become: true — gestionar servicios requiere root |
Agregar become: true al play (no en la CLI) |
| El servicio se reinicia en cada corrida del playbook | Se usó state: restarted en un task normal |
Usar state: started (idempotente); reservar restarted para handlers |
| Servidor caído tras un reboot, aunque el playbook pasó | Falta enabled: yes — arrancó pero no quedó habilitado |
Agregar enabled: yes al task service |
Unable to start service httpd: Job for httpd.service failed |
Config de httpd inválida o puerto 80 ocupado | journalctl -u httpd / httpd -t para ver el error real |
curl localhost devuelve la página "Testing 123" o el default |
No hay index.html propio — es normal en este lab |
No es error; el contenido propio era el Día 88 (blockinfile) |
Funciona con --become pero no sin args |
La validación corre sin --become |
Poner become: true dentro del play |
daemon-reload necesario y el servicio no toma cambios |
Se editó un unit file sin recargar systemd | Usar el módulo systemd con daemon_reload: true |
Conexión con días anteriores
- Día 88 (blockinfile): hoy es casi el mismo stack (
httpd+service) pero sin el despliegue de contenido. La verificación lo muestra:curldevuelve la página default de Apache porque falta elindex.htmlque ayer creaba elblockinfile. - Día 87 (
yum): el primer task de hoy es idéntico — instalar el paquete. Hoy se construye sobre eso para gestionar su servicio. - Días 84-85 (
copy,file): gestionaban archivos; Día 87 paquetes; hoy servicios — la tercera categoría del trío clásico de provisioning: paquete → archivo de config → servicio. - Día 68 (Install Jenkins a mano): el contraste imperativo — ahí se arrancó un servicio con
systemctl start jenkinspor SSH; hoy Ansible declara el estado deseado (started+enabled) sobre toda la flota, idempotentemente.