Día 88 - Ansible Blockinfile Module (httpd + bloque marcado + owner/group/mode en un task)
Problema / Desafío
El equipo de Nautilus necesita un servidor web httpd simple en todos los app servers de Stratos DC, con una página de muestra desplegada únicamente con Ansible. La tarea:
- Crear
/home/thor/ansible/playbook.yml(el inventarioinventoryya existe en/home/thor/ansible) - Instalar
httpden todos los app servers y dejar el servicio arrancado y corriendo - Usando el módulo
blockinfile, escribir este contenido en/var/www/html/index.html:
Welcome to XfusionCorp!
This is Nautilus sample file, created using Ansible!
Please do not modify this file manually!
- El archivo
index.htmldebe tener owner y groupapacheen todos los app servers - Los permisos deben ser
0655
Restricciones de validación:
- Se corre con
ansible-playbook -i inventory playbook.yml— sin argumentos extra. Elbecomedebe vivir dentro del playbook. - No usar marker custom ni vacío en
blockinfile— hay que dejar los marcadores por defecto.
Cierra la línea de Ansible (Días 82-87). Si el Día 87 instalaba un paquete suelto, hoy se hace el flujo completo de provisioning: instalar servicio + arrancarlo + desplegar contenido con permisos, todo en un solo playbook.
Conceptos clave
El módulo blockinfile — insertar/gestionar un bloque de líneas
lineinfile (Día relacionado) gestiona una línea. blockinfile gestiona un bloque de varias líneas como una sola unidad, delimitado por marcadores que el propio módulo inserta:
- name: write content
ansible.builtin.blockinfile:
path: /var/www/html/index.html
create: yes
block: |
Welcome to XfusionCorp!
This is Nautilus sample file, created using Ansible!
Please do not modify this file manually!
Resultado en el archivo:
# BEGIN ANSIBLE MANAGED BLOCK
Welcome to XfusionCorp!
This is Nautilus sample file, created using Ansible!
Please do not modify this file manually!
# END ANSIBLE MANAGED BLOCK
| Parámetro | Función |
|---|---|
path: |
Archivo a editar (antes dest:) |
block: |
El contenido multilínea a insertar (se usa \| para bloque YAML literal) |
create: |
Si el archivo no existe, créalo (default no → falla si no existe) |
marker: |
Plantilla de los comentarios delimitadores (default # {mark} ANSIBLE MANAGED BLOCK) |
insertafter:/insertbefore: |
Dónde colocar el bloque (regex, EOF, BOF) |
state: |
present (default, inserta/actualiza) o absent (borra el bloque) |
owner:/group:/mode: |
Heredados del módulo file — fijan dueño y permisos en el mismo task |
Los marcadores: el corazón de la idempotencia
blockinfile envuelve el bloque entre dos comentarios:
# BEGIN ANSIBLE MANAGED BLOCK ← marker con {mark} = BEGIN
...contenido...
# END ANSIBLE MANAGED BLOCK ← marker con {mark} = END
En cada corrida, Ansible busca esos marcadores en el archivo:
- Si los encuentra → reemplaza lo que hay entre ellos (no duplica)
- Si no los encuentra → inserta el bloque entero
Por eso una segunda corrida da ok y no changed: el bloque ya está delimitado y su contenido no cambió.
La restricción del lab — "no usar marker custom ni vacío" — existe justamente para no romper esto. Si se pone
marker: ""o un marker raro, la validación que busca el bloque por defecto no lo encuentra, o el módulo pierde la capacidad de reidentificar su bloque y empieza a duplicar contenido.
create: yes — por qué es obligatorio aquí
Un httpd recién instalado no trae /var/www/html/index.html. La página "Testing 123 / It works!" que se ve viene de /etc/httpd/conf.d/welcome.conf, no de un index. Es decir: el archivo destino no existe cuando corre el task.
blockinfile con create: no (el default) falla si el archivo no existe. Con create: yes, lo crea y le aplica owner/group/mode. Apenas existe el index.html, Apache deja de mostrar el welcome y sirve nuestro contenido.
owner / group / mode en el mismo task — herencia del módulo file
Muchos módulos que tocan archivos (copy, template, blockinfile, lineinfile) heredan los parámetros del módulo file. Eso permite fijar dueño y permisos sin un task file aparte:
blockinfile:
path: /var/www/html/index.html
block: |
...
owner: apache # ← dueño
group: apache # ← grupo
mode: '0655' # ← permisos
Esto cubre los requisitos 4 y 5 del lab en una sola pasada. El usuario/grupo apache es el que crea el paquete httpd al instalarse — es el dueño "natural" del docroot.
Los permisos 0655 — qué significan exactamente
0 6 5 5
│ │ │ └── otros: r-x (4+1 = 5) → leer + ejecutar
│ │ └────── grupo: r-x (4+1 = 5) → leer + ejecutar
│ └────────── dueño: rw- (4+2 = 6) → leer + escribir
└───────────── sin bits especiales (setuid/setgid/sticky)
0655 es inusual: el dueño puede escribir (6) pero no ejecutar, mientras grupo y otros sí pueden ejecutar (5) pero no escribir. Para un archivo HTML el bit de ejecución es irrelevante — lo que importa es la lectura, presente para los tres. El lab pide exactamente 0655, así que se respeta al pie de la letra; comillas obligatorias ('0655') para que YAML no lo interprete como número octal/decimal.
El servicio: state: started + enabled: yes
- name: Start service & enable
ansible.builtin.service:
name: httpd
state: started # ← arrancado AHORA
enabled: yes # ← arranca solo al bootear
| Parámetro | Garantiza |
|---|---|
state: started |
El servicio está corriendo en este momento |
enabled: yes |
El servicio arranca automáticamente al reiniciar |
El lab solo pide "up and running" (→ state: started). Agregar enabled: yes es buena práctica: sin él, un reboot dejaría el web server caído.
El playbook completo — anatomía
- hosts: all # ← los 3 app servers del inventario
become: true # ← root: instalar y tocar /var/www requiere privilegios
tasks:
- name: install packages
ansible.builtin.yum:
name: httpd
state: present
- name: Start service & enable
ansible.builtin.service:
name: httpd
state: started
enabled: yes
- name: write content on /var/www/html/index.html
ansible.builtin.blockinfile:
path: /var/www/html/index.html
create: yes
block: |
Welcome to XfusionCorp!
This is Nautilus sample file, created using Ansible!
Please do not modify this file manually!
owner: apache
group: apache
mode: '0655'
become: true está en el play, no en la línea de comando — clave para que la validación corra sin --become.
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 los 3 tasks (yum → service → blockinfile) - Correr
ansible-playbook -i inventory playbook.yml - Validar:
curl <app-server>o revisar permisos/owner delindex.html
Comandos / Código
1. Validar conectividad
stapp01 | SUCCESS => { ... "ping": "pong" }
stapp02 | SUCCESS => { ... "ping": "pong" }
stapp03 | SUCCESS => { ... "ping": "pong" }
2. El 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 service & enable
service:
name: httpd
state: started
enabled: yes
- name: write content on /var/www/html/index.html
ansible.builtin.blockinfile:
path: /var/www/html/index.html
create: yes
block: |
Welcome to XfusionCorp!
This is Nautilus sample file, created using Ansible!
Please do not modify this file manually!
owner: apache
group: apache
mode: '0655'
3. Ejecutar el playbook
Output real del lab (recortado):
TASK [install packages] ********************************
ok: [stapp02]
ok: [stapp01]
ok: [stapp03]
TASK [Start service & enable] **************************
changed: [stapp02]
changed: [stapp01]
changed: [stapp03]
TASK [write content on /var/www/html/index.html] *******
ok: [stapp02]
ok: [stapp03]
ok: [stapp01]
PLAY RECAP *********************************************
stapp01 : ok=6 changed=2 unreachable=0 failed=0
stapp02 : ok=6 changed=2 unreachable=0 failed=0
stapp03 : ok=6 changed=2 unreachable=0 failed=0
httpdsalióok(ya estabapresenten la imagen del lab); el servicio salióchanged(lo arrancó). El bloque salióokporque ya se había escrito en una corrida previa — si fuera la primera vez seríachanged.
4. Verificación
Opción A — desde un task de debug dentro del propio playbook (como en el lab):
- name: test service
ansible.builtin.shell: curl localhost
register: server_content
- name: server ok
ansible.builtin.debug:
var: server_content.stdout
"server_content.stdout": "# BEGIN ANSIBLE MANAGED BLOCK\nWelcome to XfusionCorp!\nThis is Nautilus sample file, created using Ansible!\nPlease do not modify this file manually!\n# END ANSIBLE MANAGED BLOCK"
Opción B — ad-hoc, verificar permisos y owner en los 3 hosts:
ansible -i inventory all -m shell -a "ls -l /var/www/html/index.html" --become
# esperado: -rw-r-xr-x. 1 apache apache ... index.html
Opción C — confirmar contenido servido:
Variantes (referencia)
Controlar dónde se inserta el bloque
- name: insertar bloque al inicio del archivo
ansible.builtin.blockinfile:
path: /etc/motd
insertbefore: BOF # Beginning Of File (también: EOF, o un regex)
block: |
Acceso monitoreado.
Múltiples bloques en el mismo archivo (requiere marker distinto)
Cuando se necesita más de un bloque gestionado en un mismo archivo, sí se usa marker custom — pero ojo, este lab lo prohíbe:
- ansible.builtin.blockinfile:
path: /etc/ssh/sshd_config
marker: "# {mark} BLOQUE HARDENING"
block: |
PermitRootLogin no
Borrar un bloque gestionado
- ansible.builtin.blockinfile:
path: /var/www/html/index.html
state: absent # elimina el bloque entre marcadores
blockinfile vs copy/template — cuándo cada uno
| Módulo | Para |
|---|---|
blockinfile |
Insertar/gestionar un fragmento dentro de un archivo existente |
lineinfile |
Gestionar una sola línea (regex match + replace) |
copy |
Poner un archivo completo desde el control node |
template |
Archivo completo con variables Jinja2 ({{ }}) |
Para un index.html que es todo el contenido, copy/template serían más naturales; el lab pide blockinfile específicamente para practicar la gestión de fragmentos marcados.
Troubleshooting
| Problema | Causa | Solución |
|---|---|---|
Path /var/www/html/index.html does not exist |
httpd no crea index.html; falta create: yes |
Agregar create: yes al task de blockinfile |
| El bloque se duplica en cada corrida | Marker custom/vacío rompe la reidentificación del bloque | Dejar el marker por defecto (la restricción del lab) |
chown failed: failed to look up user apache |
httpd aún no instalado → el usuario apache no existe |
Asegurar que el task yum corre antes que el blockinfile |
mode: 655 interpretado mal / permisos raros |
YAML lee 0655 sin comillas como número |
Usar comillas: mode: '0655' |
Servicio instalado pero curl no responde |
Falta state: started o firewall |
state: started en el task service; revisar firewalld |
Funciona con --become pero no sin args |
La validación corre sin --become |
Poner become: true dentro del play, no en la CLI |
failed to transfer file ... curl: command not found |
El task de verificación usa curl y el host no lo tiene |
Usar el módulo uri en vez de shell: curl para verificar |
Segunda corrida da changed en el bloque sin haber tocado nada |
El block: tiene espacios/saltos distintos al archivo |
Mantener el block: idéntico; revisar trailing whitespace |
Conexión con días anteriores
- Día 87 (
yum): ahí se instalaba un paquete suelto; hoyyumes solo el primer paso de un flujo completo instalar → arrancar → desplegar. - Días 84-85 (
copy,file):blockinfilehereda los parámetrosowner/group/modedel módulofile— el mismo patrón de permisos, ahora en un módulo de edición. - Día 86 (passwordless SSH): el pre-requisito que deja correr el playbook sin password; el
pingde hoy confirma que sigue funcionando. - Día 68 (Install Jenkins a mano): el contraste imperativo — ahí se instaló y arrancó un servicio web por SSH manual sobre un host; hoy Ansible hace instalar + arrancar + configurar contenido declarativamente sobre toda la flota.